Compare commits
43 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2cf0475066 | |||
| 0923cd6509 | |||
| 330908c49d | |||
| 644140b617 | |||
| d302a0fbc7 | |||
| ce8f7da9ca | |||
| f1a4811030 | |||
| a439ffcafc | |||
| 5eeab103c2 | |||
| 78ffd9d56e | |||
| 96213900ac | |||
| 85e30ef6ca | |||
| f38df69983 | |||
| 64e9515293 | |||
| 67e676d4ae | |||
| ef36a9c28c | |||
| 513bcbb80d | |||
| 3d5952ebbd | |||
| 1d55a1bec4 | |||
| 962344f5fc | |||
| ba6bcc82b6 | |||
| 6659935f3d | |||
| ccca9e8828 | |||
| b4cce2b3e0 | |||
| 9737d847fd | |||
| f180c6a07c | |||
| 024c2d4ce0 | |||
| 17731f3904 | |||
| e2dadd4213 | |||
| b4fedf9a87 | |||
| beaf6284fd | |||
| 3300eb0e79 | |||
| 3599526fde | |||
| 8b6a0ad891 | |||
| cf99ee73f5 | |||
| bbd3e3c29c | |||
| 972579bbec | |||
| 4044b0897e | |||
| 5e6c0bbc14 | |||
| a8c6474f5e | |||
| 820279634e | |||
| ce7577a2b4 | |||
| 31376e5a52 |
@@ -1,17 +1,17 @@
|
||||
1. **Before reporting a new issue, take a look at the [FAQ](https://tachiyomi.org/help/faq/), the [changelog](https://github.com/inorichi/tachiyomi/releases) and the already opened [issues](https://github.com/inorichi/tachiyomi/issues).**
|
||||
1. **Before reporting a new issue, take a look at the [FAQ](https://tachiyomi.org/help/faq/), the [changelog](https://github.com/tachiyomiorg/tachiyomi/releases) and the already opened [issues](https://github.com/tachiyomiorg/tachiyomi/issues).**
|
||||
2. If you are unsure, ask here: [](https://discord.gg/tachiyomi)
|
||||
3. What is your type of issue?
|
||||
* [Catalogue request](#catalogue-requests)
|
||||
* [Bugs](#bugs)
|
||||
* [Feature requests](#feature-requests)
|
||||
* [Translations](https://tachiyomi.org/help/contribution/#translation)
|
||||
4. After following 1. and 3. you can [open your issue](https://github.com/inorichi/tachiyomi/issues/new)
|
||||
4. After following 1. and 3. you can [open your issue](https://github.com/tachiyomiorg/tachiyomi/issues/new)
|
||||
|
||||
***
|
||||
|
||||
# Catalogue requests
|
||||
|
||||
* Catalogue requests should be created at https://github.com/inorichi/tachiyomi-extensions#readme, not here
|
||||
* Catalogue requests should be created at https://github.com/tachiyomiorg/tachiyomi-extensions#readme, not here
|
||||
|
||||
# Bugs
|
||||
* Include version (More > About > Version)
|
||||
@@ -23,9 +23,9 @@
|
||||
* For large logs use http://pastebin.com/ (or similar)
|
||||
* Don't group unrelated requests into one issue
|
||||
|
||||
DO: https://github.com/inorichi/tachiyomi/issues/24 https://github.com/inorichi/tachiyomi/issues/71
|
||||
DO: https://github.com/tachiyomiorg/tachiyomi/issues/24 https://github.com/tachiyomiorg/tachiyomi/issues/71
|
||||
|
||||
DON'T: https://github.com/inorichi/tachiyomi/issues/75
|
||||
DON'T: https://github.com/tachiyomiorg/tachiyomi/issues/75
|
||||
|
||||
# Feature requests
|
||||
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
I acknowledge that:
|
||||
|
||||
- I have updated to the latest version of the app (stable is v1.4.0)
|
||||
- I have updated to the latest version of the app (stable is v1.4.1)
|
||||
- I have updated all extensions
|
||||
- If this is an issue with an extension, that I should be opening an issue in https://github.com/inorichi/tachiyomi-extensions
|
||||
- If this is an issue with an extension, that I should be opening an issue in https://github.com/tachiyomiorg/tachiyomi-extensions
|
||||
|
||||
**DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT**
|
||||
|
||||
|
||||
@@ -9,9 +9,9 @@ labels: "bug"
|
||||
|
||||
I acknowledge that:
|
||||
|
||||
- I have updated to the latest version of the app (stable is v1.4.0)
|
||||
- I have updated to the latest version of the app (stable is v1.4.1)
|
||||
- I have updated all extensions
|
||||
- If this is an issue with an extension, that I should be opening an issue in https://github.com/inorichi/tachiyomi-extensions
|
||||
- If this is an issue with an extension, that I should be opening an issue in https://github.com/tachiyomiorg/tachiyomi-extensions
|
||||
|
||||
**DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT**
|
||||
|
||||
|
||||
@@ -4,5 +4,5 @@ contact_links:
|
||||
url: https://tachiyomi.org/help/
|
||||
about: Common questions are answered here.
|
||||
- name: Tachiyomi extensions GitHub repository
|
||||
url: https://github.com/inorichi/tachiyomi-extensions
|
||||
url: https://github.com/tachiyomiorg/tachiyomi-extensions
|
||||
about: Issues about an extension/source/catalogue should be opened here instead.
|
||||
|
||||
@@ -9,9 +9,9 @@ labels: "feature"
|
||||
|
||||
I acknowledge that:
|
||||
|
||||
- I have updated to the latest version of the app (stable is v1.4.0)
|
||||
- I have updated to the latest version of the app (stable is v1.4.1)
|
||||
- I have updated all extensions
|
||||
- If this is an issue with an extension, that I should be opening an issue in https://github.com/inorichi/tachiyomi-extensions
|
||||
- If this is an issue with an extension, that I should be opening an issue in https://github.com/tachiyomiorg/tachiyomi-extensions
|
||||
|
||||
**DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT**
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
---
|
||||
name: "Extension/source/catalogue issue"
|
||||
about: "Do not open an issue here. See https://github.com/inorichi/tachiyomi-extensions"
|
||||
title: "THIS ISSUE IS IN THE WRONG REPO; SEE https://github.com/inorichi/tachiyomi-extensions"
|
||||
about: "Do not open an issue here. See https://github.com/tachiyomiorg/tachiyomi-extensions"
|
||||
title: "THIS ISSUE IS IN THE WRONG REPO; SEE https://github.com/tachiyomiorg/tachiyomi-extensions"
|
||||
labels: "catalog, invalid"
|
||||
---
|
||||
|
||||
DO NOT OPEN AN ISSUE IN THIS REPO. SEE https://github.com/inorichi/tachiyomi-extensions
|
||||
DO NOT OPEN AN ISSUE IN THIS REPO. SEE https://github.com/tachiyomiorg/tachiyomi-extensions
|
||||
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
uses: gradle/wrapper-validation-action@v1
|
||||
|
||||
build:
|
||||
name: Build app release
|
||||
name: Build app
|
||||
needs: check_wrapper
|
||||
if: "!startsWith(github.event.head_commit.message, '[SKIP CI]')"
|
||||
runs-on: ubuntu-latest
|
||||
@@ -60,21 +60,16 @@ jobs:
|
||||
dependencies-cache-enabled: true
|
||||
configuration-cache-enabled: true
|
||||
|
||||
- name: Sign Android Release
|
||||
- name: Sign APK
|
||||
uses: r0adkll/sign-android-release@v1
|
||||
with:
|
||||
# The directory to find your release to sign
|
||||
releaseDirectory: app/build/outputs/apk/standard/release
|
||||
# The key used to sign your release in base64 encoded format
|
||||
signingKeyBase64: ${{ secrets.SIGNING_KEY }}
|
||||
# The key alias
|
||||
alias: ${{ secrets.ALIAS }}
|
||||
# The password to the keystore
|
||||
keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}
|
||||
# The password for the key
|
||||
keyPassword: ${{ secrets.KEY_PASSWORD }}
|
||||
|
||||
- name: Create Release
|
||||
- name: Create release
|
||||
id: create_release
|
||||
uses: actions/create-release@v1
|
||||
env:
|
||||
@@ -85,7 +80,7 @@ jobs:
|
||||
draft: true
|
||||
prerelease: false
|
||||
|
||||
- name: Upload Release APK
|
||||
- name: Upload APK to release
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -11,7 +11,7 @@ Tachiyomi is a free and open source manga reader for Android 5.0 and above. This
|
||||
## Features
|
||||
|
||||
Features of Tachiyomi(original) include:
|
||||
* Online reading from sources such as MangaDex, MangaSee, Mangakakalot, [and more](https://github.com/inorichi/tachiyomi-extensions)
|
||||
* Online reading from sources such as MangaDex, MangaSee, Mangakakalot, [and more](https://github.com/tachiyomiorg/tachiyomi-extensions)
|
||||
* Local reading of downloaded manga
|
||||
* A configurable reader with multiple viewers, reading directions and other settings.
|
||||
* [MyAnimeList](https://myanimelist.net/), [AniList](https://anilist.co/), [Kitsu](https://kitsu.io/), [Shikimori](https://shikimori.one), and [Bangumi](https://bgm.tv/) support
|
||||
@@ -52,18 +52,19 @@ Features of TachiyomiSY include:
|
||||
* Enhanced views for internal and integrated sources
|
||||
* Enhanced usability for internal and delegated sources
|
||||
|
||||
* Custom sources:
|
||||
* * E-Hentai/ExHentai
|
||||
* Additional features for some extensions, features include custom description, opening in app, batch add to library, and a bunch of other things based on the source:
|
||||
* * 8Muses (EroMuse)
|
||||
* * HBrowse
|
||||
* * HentaiCafe (inside Foolside)
|
||||
* * Hitomi.la
|
||||
* * Mangadex
|
||||
* * NHentai
|
||||
* * PervEden (EN and IT)
|
||||
* * Puruin
|
||||
* * Tsumino
|
||||
Custom sources:
|
||||
* E-Hentai/ExHentai
|
||||
|
||||
Additional features for some extensions, features include custom description, opening in app, batch add to library, and a bunch of other things based on the source:
|
||||
* 8Muses (EroMuse)
|
||||
* HBrowse
|
||||
* HentaiCafe (inside Foolside)
|
||||
* Hitomi.la
|
||||
* Mangadex
|
||||
* NHentai
|
||||
* PervEden (EN and IT)
|
||||
* Puruin
|
||||
* Tsumino
|
||||
|
||||
## Download
|
||||
Get the app from our [releases page](https://github.com/jobobby04/tachiyomisy/releases/latest).
|
||||
@@ -92,9 +93,9 @@ Please make sure to read the full guidelines. Your issue may be closed without w
|
||||
* For large logs use http://pastebin.com/ (or similar)
|
||||
* Don't group unrelated requests into one issue
|
||||
|
||||
DO: https://github.com/inorichi/tachiyomi/issues/24 https://github.com/inorichi/tachiyomi/issues/71
|
||||
DO: https://github.com/tachiyomiorg/tachiyomi/issues/24 https://github.com/tachiyomiorg/tachiyomi/issues/71
|
||||
|
||||
DON'T: https://github.com/inorichi/tachiyomi/issues/75
|
||||
DON'T: https://github.com/tachiyomiorg/tachiyomi/issues/75
|
||||
|
||||
</details>
|
||||
|
||||
@@ -103,7 +104,7 @@ DON'T: https://github.com/inorichi/tachiyomi/issues/75
|
||||
* Write a detailed issue, explaining what it should do or how. Avoid writing just "like X app does"
|
||||
* Include screenshot (if needed)
|
||||
|
||||
Source requests should be created at https://github.com/inorichi/tachiyomi-extensions, they do not belong in this repository.
|
||||
Source requests should be created at https://github.com/tachiyomiorg/tachiyomi-extensions, they do not belong in this repository.
|
||||
</details>
|
||||
|
||||
## FAQ
|
||||
|
||||
+4
-9
@@ -5,8 +5,8 @@ import java.text.SimpleDateFormat
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'com.mikepenz.aboutlibraries.plugin'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlin-android-extensions'
|
||||
apply plugin: 'kotlin-kapt'
|
||||
apply plugin: 'kotlin-parcelize'
|
||||
apply plugin: 'kotlinx-serialization'
|
||||
apply plugin: 'com.github.zellius.shortcut-helper'
|
||||
// Realm (EH)
|
||||
@@ -44,8 +44,8 @@ android {
|
||||
minSdkVersion AndroidConfig.minSdk
|
||||
targetSdkVersion AndroidConfig.targetSdk
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
versionCode 10
|
||||
versionName "1.4.0"
|
||||
versionCode 11
|
||||
versionName "1.4.1"
|
||||
|
||||
buildConfigField "String", "COMMIT_COUNT", "\"${getCommitCount()}\""
|
||||
buildConfigField "String", "COMMIT_SHA", "\"${getGitSha()}\""
|
||||
@@ -133,10 +133,6 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
androidExtensions {
|
||||
experimental = true
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
// Source models and interfaces from Tachiyomi 1.x
|
||||
@@ -253,7 +249,6 @@ dependencies {
|
||||
implementation 'com.github.dmytrodanylyk.android-process-button:library:1.0.4'
|
||||
implementation 'eu.davidea:flexible-adapter:5.1.0'
|
||||
implementation 'eu.davidea:flexible-adapter-ui:1.0.0'
|
||||
implementation 'com.nononsenseapps:filepicker:2.5.2'
|
||||
implementation 'com.nightlynexus.viewstatepageradapter:viewstatepageradapter:1.1.0'
|
||||
implementation 'com.github.chrisbanes:PhotoView:2.3.0'
|
||||
implementation 'com.github.carlosesco:DirectionalViewPager:a844dbca0a'
|
||||
@@ -294,7 +289,7 @@ dependencies {
|
||||
|
||||
implementation "org.jetbrains.kotlin:kotlin-reflect:$BuildPluginsVersion.KOTLIN"
|
||||
|
||||
final coroutines_version = '1.4.1'
|
||||
final coroutines_version = '1.4.2'
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
|
||||
|
||||
|
||||
@@ -80,10 +80,6 @@
|
||||
<activity
|
||||
android:name=".ui.webview.WebViewActivity"
|
||||
android:configChanges="uiMode|orientation|screenSize" />
|
||||
<activity
|
||||
android:name=".widget.CustomLayoutPickerActivity"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/FilePickerTheme" />
|
||||
<activity
|
||||
android:name=".ui.setting.track.AnilistLoginActivity"
|
||||
android:label="Anilist">
|
||||
|
||||
@@ -10,6 +10,7 @@ import eu.kanade.tachiyomi.data.track.TrackManager
|
||||
import eu.kanade.tachiyomi.data.updater.UpdaterJob
|
||||
import eu.kanade.tachiyomi.extension.ExtensionUpdateJob
|
||||
import eu.kanade.tachiyomi.ui.library.LibrarySort
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import eu.kanade.tachiyomi.widget.ExtendedNavigationView
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
@@ -120,7 +121,10 @@ object Migrations {
|
||||
|
||||
// Force MAL log out due to login flow change
|
||||
val trackManager = Injekt.get<TrackManager>()
|
||||
trackManager.myAnimeList.logout()
|
||||
if (trackManager.myAnimeList.isLogged) {
|
||||
trackManager.myAnimeList.logout()
|
||||
context.toast(R.string.myanimelist_relogin)
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -146,7 +146,7 @@ class BackupNotifier(private val context: Context) {
|
||||
val uri = destFile.getUriCompat(context)
|
||||
|
||||
addAction(
|
||||
R.drawable.nnf_ic_file_folder,
|
||||
R.drawable.ic_folder_24dp,
|
||||
context.getString(R.string.action_open_log),
|
||||
NotificationReceiver.openErrorLogPendingActivity(context, uri)
|
||||
)
|
||||
|
||||
@@ -137,7 +137,7 @@ class DownloadPendingDeleter(context: Context) {
|
||||
val id: Long,
|
||||
val url: String,
|
||||
val name: String,
|
||||
val scanlator: String?
|
||||
val scanlator: String? = null
|
||||
)
|
||||
|
||||
/**
|
||||
|
||||
@@ -110,7 +110,7 @@ class LibraryUpdateNotifier(private val context: Context) {
|
||||
|
||||
setContentIntent(errorLogIntent)
|
||||
addAction(
|
||||
R.drawable.nnf_ic_file_folder,
|
||||
R.drawable.ic_folder_24dp,
|
||||
context.getString(R.string.action_open_log),
|
||||
errorLogIntent
|
||||
)
|
||||
|
||||
@@ -39,6 +39,7 @@ import eu.kanade.tachiyomi.util.system.acquireWakeLock
|
||||
import eu.kanade.tachiyomi.util.system.isServiceRunning
|
||||
import exh.LIBRARY_UPDATE_EXCLUDED_SOURCES
|
||||
import exh.MERGED_SOURCE_ID
|
||||
import exh.mangaDexSourceIds
|
||||
import exh.md.utils.FollowStatus
|
||||
import exh.md.utils.MdUtil
|
||||
import exh.metadata.metadata.base.insertFlatMetadata
|
||||
@@ -283,7 +284,7 @@ class LibraryUpdateService(
|
||||
val trackingExtra = intent.getStringExtra(KEY_GROUP_EXTRA)?.toIntOrNull() ?: -1
|
||||
libraryManga.filter {
|
||||
val loggedServices = trackManager.services.filter { it.isLogged }
|
||||
val status: String = {
|
||||
val status: String = run {
|
||||
val tracks = db.getTracks(it).executeAsBlocking()
|
||||
val track = tracks.find { track ->
|
||||
loggedServices.any { it.id == track?.sync_id }
|
||||
@@ -294,7 +295,7 @@ class LibraryUpdateService(
|
||||
} else {
|
||||
"not tracked"
|
||||
}
|
||||
}()
|
||||
}
|
||||
trackManager.mapTrackingOrder(status, applicationContext) == trackingExtra
|
||||
}
|
||||
}
|
||||
@@ -537,7 +538,9 @@ class LibraryUpdateService(
|
||||
}
|
||||
|
||||
// SY -->
|
||||
// filter all follows from Mangadex and only add reading or rereading manga to library
|
||||
/**
|
||||
* filter all follows from Mangadex and only add reading or rereading manga to library
|
||||
*/
|
||||
private fun syncFollows(): Observable<LibraryManga> {
|
||||
val count = AtomicInteger(0)
|
||||
val mangaDex = MdUtil.getEnabledMangaDex(preferences, sourceManager) ?: return Observable.empty()
|
||||
@@ -582,7 +585,7 @@ class LibraryUpdateService(
|
||||
*/
|
||||
private fun pushFavorites(): Observable<LibraryManga> {
|
||||
val count = AtomicInteger(0)
|
||||
val listManga = db.getLibraryMangas().executeAsBlocking()
|
||||
val listManga = db.getFavoriteMangas().executeAsBlocking().filter { it.source in mangaDexSourceIds }
|
||||
|
||||
// filter all follows from Mangadex and only add reading or rereading manga to library
|
||||
return Observable.from(if (trackManager.mdList.isLogged) listManga else emptyList())
|
||||
|
||||
@@ -127,7 +127,9 @@ object PreferenceKeys {
|
||||
|
||||
const val automaticExtUpdates = "automatic_ext_updates"
|
||||
|
||||
const val allowNsfwSource = "allow_nsfw_source"
|
||||
const val showNsfwSource = "show_nsfw_source"
|
||||
const val showNsfwExtension = "show_nsfw_extension"
|
||||
const val labelNsfwExtension = "label_nsfw_extension"
|
||||
|
||||
const val startScreen = "start_screen"
|
||||
|
||||
@@ -330,4 +332,8 @@ object PreferenceKeys {
|
||||
const val createLegacyBackup = "create_legacy_backup"
|
||||
|
||||
const val dontDeleteFromCategories = "dont_delete_from_categories"
|
||||
|
||||
const val extensionRepos = "extension_repos"
|
||||
|
||||
const val cropBordersContinuesVertical = "crop_borders_continues_vertical"
|
||||
}
|
||||
|
||||
@@ -44,12 +44,6 @@ object PreferenceValues {
|
||||
BOTH
|
||||
}
|
||||
|
||||
enum class NsfwAllowance {
|
||||
ALLOWED,
|
||||
PARTIAL,
|
||||
BLOCKED
|
||||
}
|
||||
|
||||
// SY -->
|
||||
enum class GroupLibraryMode {
|
||||
GLOBAL,
|
||||
|
||||
@@ -10,7 +10,6 @@ import com.tfcporciuncula.flow.Preference
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.preference.PreferenceValues.DisplayMode
|
||||
import eu.kanade.tachiyomi.data.preference.PreferenceValues.NsfwAllowance
|
||||
import eu.kanade.tachiyomi.data.track.TrackService
|
||||
import eu.kanade.tachiyomi.data.track.anilist.Anilist
|
||||
import eu.kanade.tachiyomi.widget.ExtendedNavigationView
|
||||
@@ -231,7 +230,9 @@ class PreferencesHelper(val context: Context) {
|
||||
|
||||
fun automaticExtUpdates() = flowPrefs.getBoolean(Keys.automaticExtUpdates, true)
|
||||
|
||||
fun allowNsfwSource() = flowPrefs.getEnum(Keys.allowNsfwSource, NsfwAllowance.ALLOWED)
|
||||
fun showNsfwSource() = flowPrefs.getBoolean(Keys.showNsfwSource, true)
|
||||
fun showNsfwExtension() = flowPrefs.getBoolean(Keys.showNsfwExtension, true)
|
||||
fun labelNsfwExtension() = prefs.getBoolean(Keys.labelNsfwExtension, true)
|
||||
|
||||
fun extensionUpdatesCount() = flowPrefs.getInt("ext_updates_count", 0)
|
||||
|
||||
@@ -444,4 +445,8 @@ class PreferencesHelper(val context: Context) {
|
||||
fun sortTagsForLibrary() = flowPrefs.getStringSet(Keys.sortTagsForLibrary, mutableSetOf())
|
||||
|
||||
fun dontDeleteFromCategories() = flowPrefs.getStringSet(Keys.dontDeleteFromCategories, emptySet())
|
||||
|
||||
fun extensionRepos() = flowPrefs.getStringSet(Keys.extensionRepos, emptySet())
|
||||
|
||||
fun cropBordersContinuesVertical() = flowPrefs.getBoolean(Keys.cropBordersContinuesVertical, false)
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.net.Uri
|
||||
import androidx.core.net.toUri
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
@@ -18,9 +19,8 @@ import kotlinx.serialization.json.jsonPrimitive
|
||||
import kotlinx.serialization.json.long
|
||||
import kotlinx.serialization.json.put
|
||||
import kotlinx.serialization.json.putJsonObject
|
||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
@@ -30,7 +30,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
private val jsonMime = "application/json; charset=utf-8".toMediaTypeOrNull()
|
||||
private val jsonMime = "application/json; charset=utf-8".toMediaType()
|
||||
private val authClient = client.newBuilder().addInterceptor(interceptor).build()
|
||||
|
||||
fun addLibManga(track: Track): Observable<Track> {
|
||||
@@ -51,22 +51,18 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
||||
put("status", track.toAnilistStatus())
|
||||
}
|
||||
}
|
||||
val body = payload.toString().toRequestBody(jsonMime)
|
||||
val request = Request.Builder()
|
||||
.url(apiUrl)
|
||||
.post(body)
|
||||
.build()
|
||||
return authClient.newCall(request)
|
||||
return authClient.newCall(POST(apiUrl, body = payload.toString().toRequestBody(jsonMime)))
|
||||
.asObservableSuccess()
|
||||
.map { netResponse ->
|
||||
val responseBody = netResponse.body?.string().orEmpty()
|
||||
netResponse.close()
|
||||
if (responseBody.isEmpty()) {
|
||||
throw Exception("Null Response")
|
||||
netResponse.use {
|
||||
val responseBody = it.body?.string().orEmpty()
|
||||
if (responseBody.isEmpty()) {
|
||||
throw Exception("Null Response")
|
||||
}
|
||||
val response = json.decodeFromString<JsonObject>(responseBody)
|
||||
track.library_id = response["data"]!!.jsonObject["SaveMediaListEntry"]!!.jsonObject["id"]!!.jsonPrimitive.long
|
||||
track
|
||||
}
|
||||
val response = json.decodeFromString<JsonObject>(responseBody)
|
||||
track.library_id = response["data"]!!.jsonObject["SaveMediaListEntry"]!!.jsonObject["id"]!!.jsonPrimitive.long
|
||||
track
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,12 +86,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
||||
put("score", track.score.toInt())
|
||||
}
|
||||
}
|
||||
val body = payload.toString().toRequestBody(jsonMime)
|
||||
val request = Request.Builder()
|
||||
.url(apiUrl)
|
||||
.post(body)
|
||||
.build()
|
||||
return authClient.newCall(request)
|
||||
return authClient.newCall(POST(apiUrl, body = payload.toString().toRequestBody(jsonMime)))
|
||||
.asObservableSuccess()
|
||||
.map {
|
||||
track
|
||||
@@ -134,24 +125,21 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
||||
put("query", search)
|
||||
}
|
||||
}
|
||||
val body = payload.toString().toRequestBody(jsonMime)
|
||||
val request = Request.Builder()
|
||||
.url(apiUrl)
|
||||
.post(body)
|
||||
.build()
|
||||
return authClient.newCall(request)
|
||||
return authClient.newCall(POST(apiUrl, body = payload.toString().toRequestBody(jsonMime)))
|
||||
.asObservableSuccess()
|
||||
.map { netResponse ->
|
||||
val responseBody = netResponse.body?.string().orEmpty()
|
||||
if (responseBody.isEmpty()) {
|
||||
throw Exception("Null Response")
|
||||
netResponse.use {
|
||||
val responseBody = it.body?.string().orEmpty()
|
||||
if (responseBody.isEmpty()) {
|
||||
throw Exception("Null Response")
|
||||
}
|
||||
val response = json.decodeFromString<JsonObject>(responseBody)
|
||||
val data = response["data"]!!.jsonObject
|
||||
val page = data["Page"]!!.jsonObject
|
||||
val media = page["media"]!!.jsonArray
|
||||
val entries = media.map { jsonToALManga(it.jsonObject) }
|
||||
entries.map { it.toTrack() }
|
||||
}
|
||||
val response = json.decodeFromString<JsonObject>(responseBody)
|
||||
val data = response["data"]!!.jsonObject
|
||||
val page = data["Page"]!!.jsonObject
|
||||
val media = page["media"]!!.jsonArray
|
||||
val entries = media.map { jsonToALManga(it.jsonObject) }
|
||||
entries.map { it.toTrack() }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -194,24 +182,21 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
||||
put("manga_id", track.media_id)
|
||||
}
|
||||
}
|
||||
val body = payload.toString().toRequestBody(jsonMime)
|
||||
val request = Request.Builder()
|
||||
.url(apiUrl)
|
||||
.post(body)
|
||||
.build()
|
||||
return authClient.newCall(request)
|
||||
return authClient.newCall(POST(apiUrl, body = payload.toString().toRequestBody(jsonMime)))
|
||||
.asObservableSuccess()
|
||||
.map { netResponse ->
|
||||
val responseBody = netResponse.body?.string().orEmpty()
|
||||
if (responseBody.isEmpty()) {
|
||||
throw Exception("Null Response")
|
||||
netResponse.use {
|
||||
val responseBody = it.body?.string().orEmpty()
|
||||
if (responseBody.isEmpty()) {
|
||||
throw Exception("Null Response")
|
||||
}
|
||||
val response = json.decodeFromString<JsonObject>(responseBody)
|
||||
val data = response["data"]!!.jsonObject
|
||||
val page = data["Page"]!!.jsonObject
|
||||
val media = page["mediaList"]!!.jsonArray
|
||||
val entries = media.map { jsonToALUserManga(it.jsonObject) }
|
||||
entries.firstOrNull()?.toTrack()
|
||||
}
|
||||
val response = json.decodeFromString<JsonObject>(responseBody)
|
||||
val data = response["data"]!!.jsonObject
|
||||
val page = data["Page"]!!.jsonObject
|
||||
val media = page["mediaList"]!!.jsonArray
|
||||
val entries = media.map { jsonToALUserManga(it.jsonObject) }
|
||||
entries.firstOrNull()?.toTrack()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -239,22 +224,22 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
||||
val payload = buildJsonObject {
|
||||
put("query", query)
|
||||
}
|
||||
val body = payload.toString().toRequestBody(jsonMime)
|
||||
val request = Request.Builder()
|
||||
.url(apiUrl)
|
||||
.post(body)
|
||||
.build()
|
||||
return authClient.newCall(request)
|
||||
return authClient.newCall(POST(apiUrl, body = payload.toString().toRequestBody(jsonMime)))
|
||||
.asObservableSuccess()
|
||||
.map { netResponse ->
|
||||
val responseBody = netResponse.body?.string().orEmpty()
|
||||
if (responseBody.isEmpty()) {
|
||||
throw Exception("Null Response")
|
||||
netResponse.use {
|
||||
val responseBody = it.body?.string().orEmpty()
|
||||
if (responseBody.isEmpty()) {
|
||||
throw Exception("Null Response")
|
||||
}
|
||||
val response = json.decodeFromString<JsonObject>(responseBody)
|
||||
val data = response["data"]!!.jsonObject
|
||||
val viewer = data["Viewer"]!!.jsonObject
|
||||
Pair(
|
||||
viewer["id"]!!.jsonPrimitive.int,
|
||||
viewer["mediaListOptions"]!!.jsonObject["scoreFormat"]!!.jsonPrimitive.content
|
||||
)
|
||||
}
|
||||
val response = json.decodeFromString<JsonObject>(responseBody)
|
||||
val data = response["data"]!!.jsonObject
|
||||
val viewer = data["Viewer"]!!.jsonObject
|
||||
Pair(viewer["id"]!!.jsonPrimitive.int, viewer["mediaListOptions"]!!.jsonObject["scoreFormat"]!!.jsonPrimitive.content)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -298,7 +283,6 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
||||
|
||||
companion object {
|
||||
private const val clientId = "385"
|
||||
private const val clientUrl = "tachiyomi://anilist-auth"
|
||||
private const val apiUrl = "https://graphql.anilist.co/"
|
||||
private const val baseUrl = "https://anilist.co/api/v2/"
|
||||
private const val baseMangaUrl = "https://anilist.co/manga/"
|
||||
|
||||
@@ -63,7 +63,7 @@ data class ALUserManga(
|
||||
"DROPPED" -> Anilist.DROPPED
|
||||
"PLANNING" -> Anilist.PLANNING
|
||||
"REPEATING" -> Anilist.REPEATING
|
||||
else -> throw NotImplementedError("Unknown status")
|
||||
else -> throw NotImplementedError("Unknown status: $list_status")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,7 +74,7 @@ fun Track.toAnilistStatus() = when (status) {
|
||||
Anilist.DROPPED -> "DROPPED"
|
||||
Anilist.PLANNING -> "PLANNING"
|
||||
Anilist.REPEATING -> "REPEATING"
|
||||
else -> throw NotImplementedError("Unknown status")
|
||||
else -> throw NotImplementedError("Unknown status: $status")
|
||||
}
|
||||
|
||||
private val preferences: PreferencesHelper by injectLazy()
|
||||
@@ -102,5 +102,5 @@ fun Track.toAnilistScore(): String = when (preferences.anilistScoreType().get())
|
||||
}
|
||||
// 10 point decimal
|
||||
"POINT_10_DECIMAL" -> (score / 10).toString()
|
||||
else -> throw Exception("Unknown score type")
|
||||
else -> throw NotImplementedError("Unknown score type")
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import androidx.core.net.toUri
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||
import kotlinx.serialization.decodeFromString
|
||||
@@ -34,11 +35,7 @@ class BangumiApi(private val client: OkHttpClient, interceptor: BangumiIntercept
|
||||
.add("rating", track.score.toInt().toString())
|
||||
.add("status", track.toBangumiStatus())
|
||||
.build()
|
||||
val request = Request.Builder()
|
||||
.url("$apiUrl/collection/${track.media_id}/update")
|
||||
.post(body)
|
||||
.build()
|
||||
return authClient.newCall(request)
|
||||
return authClient.newCall(POST("$apiUrl/collection/${track.media_id}/update", body = body))
|
||||
.asObservableSuccess()
|
||||
.map {
|
||||
track
|
||||
@@ -46,29 +43,20 @@ class BangumiApi(private val client: OkHttpClient, interceptor: BangumiIntercept
|
||||
}
|
||||
|
||||
fun updateLibManga(track: Track): Observable<Track> {
|
||||
// chapter update
|
||||
val body = FormBody.Builder()
|
||||
.add("watched_eps", track.last_chapter_read.toString())
|
||||
.build()
|
||||
val request = Request.Builder()
|
||||
.url("$apiUrl/subject/${track.media_id}/update/watched_eps")
|
||||
.post(body)
|
||||
.build()
|
||||
|
||||
// read status update
|
||||
val sbody = FormBody.Builder()
|
||||
.add("status", track.toBangumiStatus())
|
||||
.build()
|
||||
val srequest = Request.Builder()
|
||||
.url("$apiUrl/collection/${track.media_id}/update")
|
||||
.post(sbody)
|
||||
.build()
|
||||
return authClient.newCall(srequest)
|
||||
return authClient.newCall(POST("$apiUrl/collection/${track.media_id}/update", body = sbody))
|
||||
.asObservableSuccess()
|
||||
.map {
|
||||
track
|
||||
}.flatMap {
|
||||
authClient.newCall(request)
|
||||
// chapter update
|
||||
val body = FormBody.Builder()
|
||||
.add("watched_eps", track.last_chapter_read.toString())
|
||||
.build()
|
||||
authClient.newCall(POST("$apiUrl/subject/${track.media_id}/update/watched_eps", body = body))
|
||||
.asObservableSuccess()
|
||||
.map {
|
||||
track
|
||||
@@ -82,11 +70,7 @@ class BangumiApi(private val client: OkHttpClient, interceptor: BangumiIntercept
|
||||
.buildUpon()
|
||||
.appendQueryParameter("max_results", "20")
|
||||
.build()
|
||||
val request = Request.Builder()
|
||||
.url(url.toString())
|
||||
.get()
|
||||
.build()
|
||||
return authClient.newCall(request)
|
||||
return authClient.newCall(GET(url.toString()))
|
||||
.asObservableSuccess()
|
||||
.map { netResponse ->
|
||||
var responseBody = netResponse.body?.string().orEmpty()
|
||||
@@ -126,13 +110,7 @@ class BangumiApi(private val client: OkHttpClient, interceptor: BangumiIntercept
|
||||
}
|
||||
|
||||
fun findLibManga(track: Track): Observable<Track?> {
|
||||
val urlMangas = "$apiUrl/subject/${track.media_id}"
|
||||
val requestMangas = Request.Builder()
|
||||
.url(urlMangas)
|
||||
.get()
|
||||
.build()
|
||||
|
||||
return authClient.newCall(requestMangas)
|
||||
return authClient.newCall(GET("$apiUrl/subject/${track.media_id}"))
|
||||
.asObservableSuccess()
|
||||
.map { netResponse ->
|
||||
// get comic info
|
||||
@@ -162,13 +140,17 @@ class BangumiApi(private val client: OkHttpClient, interceptor: BangumiIntercept
|
||||
}
|
||||
|
||||
fun accessToken(code: String): Observable<OAuth> {
|
||||
return client.newCall(accessTokenRequest(code)).asObservableSuccess().map { netResponse ->
|
||||
val responseBody = netResponse.body?.string().orEmpty()
|
||||
if (responseBody.isEmpty()) {
|
||||
throw Exception("Null Response")
|
||||
return client.newCall(accessTokenRequest(code))
|
||||
.asObservableSuccess()
|
||||
.map { netResponse ->
|
||||
netResponse.use {
|
||||
val responseBody = it.body?.string().orEmpty()
|
||||
if (responseBody.isEmpty()) {
|
||||
throw Exception("Null Response")
|
||||
}
|
||||
json.decodeFromString<OAuth>(responseBody)
|
||||
}
|
||||
}
|
||||
json.decodeFromString<OAuth>(responseBody)
|
||||
}
|
||||
}
|
||||
|
||||
private fun accessTokenRequest(code: String) = POST(
|
||||
|
||||
@@ -8,7 +8,7 @@ fun Track.toBangumiStatus() = when (status) {
|
||||
Bangumi.ON_HOLD -> "on_hold"
|
||||
Bangumi.DROPPED -> "dropped"
|
||||
Bangumi.PLANNING -> "wish"
|
||||
else -> throw NotImplementedError("Unknown status")
|
||||
else -> throw NotImplementedError("Unknown status: $status")
|
||||
}
|
||||
|
||||
fun toTrackStatus(status: String) = when (status) {
|
||||
@@ -17,6 +17,5 @@ fun toTrackStatus(status: String) = when (status) {
|
||||
"on_hold" -> Bangumi.ON_HOLD
|
||||
"dropped" -> Bangumi.DROPPED
|
||||
"wish" -> Bangumi.PLANNING
|
||||
|
||||
else -> throw Exception("Unknown status")
|
||||
else -> throw NotImplementedError("Unknown status: $status")
|
||||
}
|
||||
|
||||
@@ -111,7 +111,7 @@ class MyAnimeList(private val context: Context, id: Int) : TrackService(id) {
|
||||
|
||||
fun ensureLoggedIn() {
|
||||
if (isAuthorized) return
|
||||
if (!isLogged) throw Exception("MAL login credentials not found")
|
||||
if (!isLogged) throw Exception(context.getString(R.string.myanimelist_creds_missing))
|
||||
}
|
||||
|
||||
override fun logout() {
|
||||
|
||||
@@ -12,7 +12,7 @@ import eu.kanade.tachiyomi.util.lang.toCalendar
|
||||
import eu.kanade.tachiyomi.util.selectInt
|
||||
import eu.kanade.tachiyomi.util.selectText
|
||||
import okhttp3.FormBody
|
||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.RequestBody
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
@@ -282,7 +282,7 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
|
||||
.put("score", track.score)
|
||||
.put("num_read_chapters", track.last_chapter_read)
|
||||
|
||||
return body.toString().toRequestBody("application/json; charset=utf-8".toMediaTypeOrNull())
|
||||
return body.toString().toRequestBody("application/json; charset=utf-8".toMediaType())
|
||||
}
|
||||
|
||||
private fun mangaEditPostBody(track: MyAnimeListEditData): RequestBody {
|
||||
|
||||
@@ -19,9 +19,8 @@ import kotlinx.serialization.json.jsonPrimitive
|
||||
import kotlinx.serialization.json.put
|
||||
import kotlinx.serialization.json.putJsonObject
|
||||
import okhttp3.FormBody
|
||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
@@ -30,7 +29,7 @@ class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInter
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
private val jsonime = "application/json; charset=utf-8".toMediaTypeOrNull()
|
||||
private val jsonMime = "application/json; charset=utf-8".toMediaType()
|
||||
private val authClient = client.newBuilder().addInterceptor(interceptor).build()
|
||||
|
||||
fun addLibManga(track: Track, user_id: String): Observable<Track> {
|
||||
@@ -44,12 +43,7 @@ class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInter
|
||||
put("status", track.toShikimoriStatus())
|
||||
}
|
||||
}
|
||||
val body = payload.toString().toRequestBody(jsonime)
|
||||
val request = Request.Builder()
|
||||
.url("$apiUrl/v2/user_rates")
|
||||
.post(body)
|
||||
.build()
|
||||
return authClient.newCall(request)
|
||||
return authClient.newCall(POST("$apiUrl/v2/user_rates", body = payload.toString().toRequestBody(jsonMime)))
|
||||
.asObservableSuccess()
|
||||
.map {
|
||||
track
|
||||
@@ -64,11 +58,7 @@ class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInter
|
||||
.appendQueryParameter("search", search)
|
||||
.appendQueryParameter("limit", "20")
|
||||
.build()
|
||||
val request = Request.Builder()
|
||||
.url(url.toString())
|
||||
.get()
|
||||
.build()
|
||||
return authClient.newCall(request)
|
||||
return authClient.newCall(GET(url.toString()))
|
||||
.asObservableSuccess()
|
||||
.map { netResponse ->
|
||||
val responseBody = netResponse.body?.string().orEmpty()
|
||||
@@ -107,30 +97,21 @@ class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInter
|
||||
}
|
||||
|
||||
fun findLibManga(track: Track, user_id: String): Observable<Track?> {
|
||||
val url = "$apiUrl/v2/user_rates".toUri().buildUpon()
|
||||
.appendQueryParameter("user_id", user_id)
|
||||
.appendQueryParameter("target_id", track.media_id.toString())
|
||||
.appendQueryParameter("target_type", "Manga")
|
||||
.build()
|
||||
val request = Request.Builder()
|
||||
.url(url.toString())
|
||||
.get()
|
||||
.build()
|
||||
|
||||
val urlMangas = "$apiUrl/mangas".toUri().buildUpon()
|
||||
.appendPath(track.media_id.toString())
|
||||
.build()
|
||||
val requestMangas = Request.Builder()
|
||||
.url(urlMangas.toString())
|
||||
.get()
|
||||
.build()
|
||||
return authClient.newCall(requestMangas)
|
||||
return authClient.newCall(GET(urlMangas.toString()))
|
||||
.asObservableSuccess()
|
||||
.map { netResponse ->
|
||||
val responseBody = netResponse.body?.string().orEmpty()
|
||||
json.decodeFromString<JsonObject>(responseBody)
|
||||
}.flatMap { mangas ->
|
||||
authClient.newCall(request)
|
||||
val url = "$apiUrl/v2/user_rates".toUri().buildUpon()
|
||||
.appendQueryParameter("user_id", user_id)
|
||||
.appendQueryParameter("target_id", track.media_id.toString())
|
||||
.appendQueryParameter("target_type", "Manga")
|
||||
.build()
|
||||
authClient.newCall(GET(url.toString()))
|
||||
.asObservableSuccess()
|
||||
.map { netResponse ->
|
||||
val responseBody = netResponse.body?.string().orEmpty()
|
||||
@@ -155,13 +136,17 @@ class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInter
|
||||
}
|
||||
|
||||
fun accessToken(code: String): Observable<OAuth> {
|
||||
return client.newCall(accessTokenRequest(code)).asObservableSuccess().map { netResponse ->
|
||||
val responseBody = netResponse.body?.string().orEmpty()
|
||||
if (responseBody.isEmpty()) {
|
||||
throw Exception("Null Response")
|
||||
return client.newCall(accessTokenRequest(code))
|
||||
.asObservableSuccess()
|
||||
.map { netResponse ->
|
||||
netResponse.use {
|
||||
val responseBody = it.body?.string().orEmpty()
|
||||
if (responseBody.isEmpty()) {
|
||||
throw Exception("Null Response")
|
||||
}
|
||||
json.decodeFromString<OAuth>(responseBody)
|
||||
}
|
||||
}
|
||||
json.decodeFromString<OAuth>(responseBody)
|
||||
}
|
||||
}
|
||||
|
||||
private fun accessTokenRequest(code: String) = POST(
|
||||
|
||||
@@ -9,7 +9,7 @@ fun Track.toShikimoriStatus() = when (status) {
|
||||
Shikimori.DROPPED -> "dropped"
|
||||
Shikimori.PLANNING -> "planned"
|
||||
Shikimori.REPEATING -> "rewatching"
|
||||
else -> throw NotImplementedError("Unknown status")
|
||||
else -> throw NotImplementedError("Unknown status: $status")
|
||||
}
|
||||
|
||||
fun toTrackStatus(status: String) = when (status) {
|
||||
@@ -19,6 +19,5 @@ fun toTrackStatus(status: String) = when (status) {
|
||||
"dropped" -> Shikimori.DROPPED
|
||||
"planned" -> Shikimori.PLANNING
|
||||
"rewatching" -> Shikimori.REPEATING
|
||||
|
||||
else -> throw Exception("Unknown status")
|
||||
else -> throw NotImplementedError("Unknown status: $status")
|
||||
}
|
||||
|
||||
@@ -37,11 +37,11 @@ class GithubUpdateChecker {
|
||||
val newVersion = versionTag.replace("[^\\d.]".toRegex(), "")
|
||||
|
||||
return if (BuildConfig.DEBUG) {
|
||||
// Preview builds: based on releases in "tachiyomiorg/android-app-preview" repo
|
||||
// Preview builds: based on releases in "tachiyomiorg/tachiyomi-preview" repo
|
||||
// tagged as something like "r1234"
|
||||
newVersion.toInt() > BuildConfig.COMMIT_COUNT.toInt()
|
||||
} else {
|
||||
// Release builds: based on releases in "inorichi/tachiyomi" repo
|
||||
// Release builds: based on releases in "tachiyomiorg/tachiyomi" repo
|
||||
// tagged as something like "v0.1.2"
|
||||
newVersion != BuildConfig.VERSION_NAME
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.extension
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.drawable.Drawable
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.elvishew.xlog.XLog
|
||||
import com.jakewharton.rxrelay.BehaviorRelay
|
||||
import eu.kanade.tachiyomi.R
|
||||
@@ -76,9 +77,9 @@ class ExtensionManager(
|
||||
|
||||
// SY -->
|
||||
return when (source.id) {
|
||||
EH_SOURCE_ID -> context.getDrawable(R.mipmap.ic_ehentai_source)
|
||||
EXH_SOURCE_ID -> context.getDrawable(R.mipmap.ic_ehentai_source)
|
||||
MERGED_SOURCE_ID -> context.getDrawable(R.mipmap.ic_merged_source)
|
||||
EH_SOURCE_ID -> ContextCompat.getDrawable(context, R.mipmap.ic_ehentai_source)
|
||||
EXH_SOURCE_ID -> ContextCompat.getDrawable(context, R.mipmap.ic_ehentai_source)
|
||||
MERGED_SOURCE_ID -> ContextCompat.getDrawable(context, R.mipmap.ic_merged_source)
|
||||
else -> null
|
||||
}
|
||||
// SY <--
|
||||
@@ -142,8 +143,7 @@ class ExtensionManager(
|
||||
.map { it.extension }
|
||||
installedExtensions
|
||||
.flatMap { it.sources }
|
||||
// overwrite is needed until the bundled sources are removed
|
||||
.forEach { sourceManager.registerSource(it, true) }
|
||||
.forEach { sourceManager.registerSource(it) }
|
||||
|
||||
untrustedExtensions = extensions
|
||||
.filterIsInstance<LoadResult.Untrusted>()
|
||||
|
||||
@@ -25,7 +25,12 @@ internal class ExtensionGithubApi {
|
||||
return withContext(Dispatchers.IO) {
|
||||
val response = service.getRepo()
|
||||
parseResponse(response)
|
||||
} /* SY --> */ + preferences.extensionRepos().get().flatMap {
|
||||
val url = "$BASE_URL$it/repo/"
|
||||
val response = service.getRepo("${url}index.min.json")
|
||||
parseResponse(response, url)
|
||||
}
|
||||
// SY <--
|
||||
}
|
||||
|
||||
suspend fun checkForUpdates(context: Context): List<Extension.Installed> {
|
||||
@@ -58,7 +63,7 @@ internal class ExtensionGithubApi {
|
||||
return extensionsWithUpdate
|
||||
}
|
||||
|
||||
private fun parseResponse(json: JsonArray): List<Extension.Available> {
|
||||
private fun parseResponse(json: JsonArray /* SY --> */, repoUrl: String = REPO_URL_PREFIX /* SY <-- */): List<Extension.Available> {
|
||||
return json
|
||||
.filter { element ->
|
||||
val versionName = element.jsonObject["version"]!!.jsonPrimitive.content
|
||||
@@ -73,20 +78,21 @@ internal class ExtensionGithubApi {
|
||||
val versionCode = element.jsonObject["code"]!!.jsonPrimitive.int
|
||||
val lang = element.jsonObject["lang"]!!.jsonPrimitive.content
|
||||
val nsfw = element.jsonObject["nsfw"]!!.jsonPrimitive.int == 1
|
||||
val icon = "$REPO_URL_PREFIX/icon/${apkName.replace(".apk", ".png")}"
|
||||
// SY -->
|
||||
val icon = "$repoUrl/icon/${apkName.replace(".apk", ".png")}"
|
||||
// SY <--
|
||||
|
||||
Extension.Available(name, pkgName, versionName, versionCode, lang, nsfw, apkName, icon)
|
||||
Extension.Available(name, pkgName, versionName, versionCode, lang, nsfw, apkName, icon /* SY --> */, repoUrl /* SY <-- */)
|
||||
}
|
||||
}
|
||||
|
||||
fun getApkUrl(extension: Extension.Available): String {
|
||||
return "$REPO_URL_PREFIX/apk/${extension.apkName}"
|
||||
return /* SY --> */ "${extension.repoUrl}/apk/${extension.apkName}" /* SY <-- */
|
||||
}
|
||||
|
||||
// SY -->
|
||||
fun Extension.isBlacklisted(
|
||||
blacklistEnabled: Boolean =
|
||||
preferences.enableSourceBlacklist().get()
|
||||
private fun Extension.isBlacklisted(
|
||||
blacklistEnabled: Boolean = preferences.enableSourceBlacklist().get()
|
||||
): Boolean {
|
||||
return pkgName in BlacklistedSources.BLACKLISTED_EXTENSIONS && blacklistEnabled
|
||||
}
|
||||
@@ -94,6 +100,6 @@ internal class ExtensionGithubApi {
|
||||
|
||||
companion object {
|
||||
const val BASE_URL = "https://raw.githubusercontent.com/"
|
||||
const val REPO_URL_PREFIX = "${BASE_URL}inorichi/tachiyomi-extensions/repo/"
|
||||
const val REPO_URL_PREFIX = "${BASE_URL}tachiyomiorg/tachiyomi-extensions/repo/"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import kotlinx.serialization.json.JsonArray
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Url
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
/**
|
||||
@@ -27,6 +28,8 @@ interface ExtensionGithubService {
|
||||
}
|
||||
}
|
||||
|
||||
@GET("${ExtensionGithubApi.REPO_URL_PREFIX}index.min.json")
|
||||
suspend fun getRepo(): JsonArray
|
||||
// SY -->
|
||||
@GET
|
||||
suspend fun getRepo(@Url url: String = "${ExtensionGithubApi.REPO_URL_PREFIX}index.min.json"): JsonArray
|
||||
// SY <--
|
||||
}
|
||||
|
||||
@@ -35,7 +35,10 @@ sealed class Extension {
|
||||
override val lang: String,
|
||||
override val isNsfw: Boolean,
|
||||
val apkName: String,
|
||||
val iconUrl: String
|
||||
val iconUrl: String,
|
||||
// SY -->
|
||||
val repoUrl: String
|
||||
// SY <--
|
||||
) : Extension()
|
||||
|
||||
data class Untrusted(
|
||||
|
||||
@@ -6,7 +6,6 @@ import android.content.pm.PackageInfo
|
||||
import android.content.pm.PackageManager
|
||||
import dalvik.system.PathClassLoader
|
||||
import eu.kanade.tachiyomi.annotations.Nsfw
|
||||
import eu.kanade.tachiyomi.data.preference.PreferenceValues
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.extension.model.Extension
|
||||
import eu.kanade.tachiyomi.extension.model.LoadResult
|
||||
@@ -26,8 +25,8 @@ import uy.kohesive.injekt.injectLazy
|
||||
internal object ExtensionLoader {
|
||||
|
||||
private val preferences: PreferencesHelper by injectLazy()
|
||||
private val allowNsfwSource by lazy {
|
||||
preferences.allowNsfwSource().get()
|
||||
private val loadNsfwSource by lazy {
|
||||
preferences.showNsfwSource().get()
|
||||
}
|
||||
|
||||
private const val EXTENSION_FEATURE = "tachiyomi.extension"
|
||||
@@ -133,7 +132,7 @@ internal object ExtensionLoader {
|
||||
}
|
||||
|
||||
val isNsfw = appInfo.metaData.getInt(METADATA_NSFW) == 1
|
||||
if (allowNsfwSource == PreferenceValues.NsfwAllowance.BLOCKED && isNsfw) {
|
||||
if (!loadNsfwSource && isNsfw) {
|
||||
return LoadResult.Error("NSFW extension $pkgName not allowed")
|
||||
}
|
||||
|
||||
@@ -218,7 +217,7 @@ internal object ExtensionLoader {
|
||||
* Checks whether a Source or SourceFactory is annotated with @Nsfw.
|
||||
*/
|
||||
private fun isSourceNsfw(clazz: Any): Boolean {
|
||||
if (allowNsfwSource == PreferenceValues.NsfwAllowance.ALLOWED) {
|
||||
if (loadNsfwSource) {
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
@@ -62,14 +62,18 @@ interface Source : tachiyomi.source.Source {
|
||||
/**
|
||||
* [1.x API] Get the updated details for a manga.
|
||||
*/
|
||||
@Suppress("DEPRECATION")
|
||||
override suspend fun getMangaDetails(manga: MangaInfo): MangaInfo {
|
||||
return fetchMangaDetails(manga.toSManga()).awaitSingle()
|
||||
.toMangaInfo()
|
||||
val sManga = manga.toSManga()
|
||||
val networkManga = fetchMangaDetails(sManga).awaitSingle()
|
||||
sManga.copyFrom(networkManga)
|
||||
return sManga.toMangaInfo()
|
||||
}
|
||||
|
||||
/**
|
||||
* [1.x API] Get all the available chapters for a manga.
|
||||
*/
|
||||
@Suppress("DEPRECATION")
|
||||
override suspend fun getChapterList(manga: MangaInfo): List<ChapterInfo> {
|
||||
return fetchChapterList(manga.toSManga()).awaitSingle()
|
||||
.map { it.toChapterInfo() }
|
||||
@@ -78,6 +82,7 @@ interface Source : tachiyomi.source.Source {
|
||||
/**
|
||||
* [1.x API] Get the list of pages a chapter has.
|
||||
*/
|
||||
@Suppress("DEPRECATION")
|
||||
override suspend fun getPageList(chapter: ChapterInfo): List<tachiyomi.source.model.Page> {
|
||||
return fetchPageList(chapter.toSChapter()).awaitSingle()
|
||||
.map { it.toPageUrl() }
|
||||
|
||||
@@ -103,7 +103,7 @@ open class SourceManager(private val context: Context) {
|
||||
}
|
||||
// SY <--
|
||||
|
||||
internal fun registerSource(source: Source, overwrite: Boolean = false) {
|
||||
internal fun registerSource(source: Source) {
|
||||
// EXH -->
|
||||
val sourceQName = source::class.qualifiedName
|
||||
val factories = DELEGATED_SOURCES.entries.filter { it.value.factory }.map { it.value.originalSourceQualifiedClassName }
|
||||
@@ -130,7 +130,7 @@ open class SourceManager(private val context: Context) {
|
||||
}
|
||||
// EXH <--
|
||||
|
||||
if (overwrite || !sourcesMap.containsKey(source.id)) {
|
||||
if (!sourcesMap.containsKey(source.id)) {
|
||||
sourcesMap[source.id] = newSource
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,10 +69,10 @@ class MergedSource : SuspendHttpSource() {
|
||||
val mergedManga = db.getManga(manga.url, id).await() ?: throw Exception("merged manga not in db")
|
||||
val mangaReferences = mergedManga.id?.let { withContext(Dispatchers.IO) { db.getMergedMangaReferences(it).await() } } ?: throw Exception("merged manga id is null")
|
||||
if (mangaReferences.isEmpty()) throw IllegalArgumentException("Manga references are empty, info unavailable, merge is likely corrupted")
|
||||
if (mangaReferences.size == 1 || {
|
||||
if (mangaReferences.size == 1 || run {
|
||||
val mangaReference = mangaReferences.firstOrNull()
|
||||
mangaReference == null || (mangaReference.mangaSourceId == MERGED_SOURCE_ID)
|
||||
}()
|
||||
}
|
||||
) throw IllegalArgumentException("Manga references contain only the merged reference, merge is likely corrupted")
|
||||
|
||||
emit(
|
||||
|
||||
+1
-24
@@ -4,25 +4,15 @@ import android.content.res.Configuration
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.viewbinding.ViewBinding
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate
|
||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import eu.kanade.tachiyomi.data.preference.PreferenceValues as Values
|
||||
|
||||
abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
|
||||
abstract class BaseThemedActivity : AppCompatActivity() {
|
||||
|
||||
val preferences: PreferencesHelper by injectLazy()
|
||||
|
||||
val scope = lifecycleScope
|
||||
lateinit var binding: VB
|
||||
|
||||
@Suppress("LeakingThis")
|
||||
private val secureActivityDelegate = SecureActivityDelegate(this)
|
||||
|
||||
private val isDarkMode: Boolean by lazy {
|
||||
val themeMode = preferences.themeMode().get()
|
||||
(themeMode == Values.ThemeMode.dark) ||
|
||||
@@ -63,11 +53,6 @@ abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
@Suppress("LeakingThis")
|
||||
LocaleHelper.updateConfiguration(this)
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
setTheme(
|
||||
when {
|
||||
@@ -77,13 +62,5 @@ abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
|
||||
)
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
secureActivityDelegate.onCreate()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
secureActivityDelegate.onResume()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package eu.kanade.tachiyomi.ui.base.activity
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.viewbinding.ViewBinding
|
||||
import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate
|
||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||
|
||||
abstract class BaseViewBindingActivity<VB : ViewBinding> : BaseThemedActivity() {
|
||||
|
||||
val scope = lifecycleScope
|
||||
lateinit var binding: VB
|
||||
|
||||
@Suppress("LeakingThis")
|
||||
private val secureActivityDelegate = SecureActivityDelegate(this)
|
||||
|
||||
init {
|
||||
@Suppress("LeakingThis")
|
||||
LocaleHelper.updateConfiguration(this)
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
secureActivityDelegate.onCreate()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
secureActivityDelegate.onResume()
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,6 @@ import com.bluelinelabs.conductor.ControllerChangeHandler
|
||||
import com.bluelinelabs.conductor.ControllerChangeType
|
||||
import com.bluelinelabs.conductor.RestoreViewOnCreateController
|
||||
import kotlinx.android.extensions.LayoutContainer
|
||||
import kotlinx.android.synthetic.clearFindViewByIdCache
|
||||
import timber.log.Timber
|
||||
|
||||
abstract class BaseController<VB : ViewBinding>(bundle: Bundle? = null) :
|
||||
@@ -54,11 +53,6 @@ abstract class BaseController<VB : ViewBinding>(bundle: Bundle? = null) :
|
||||
return inflateView(inflater, container)
|
||||
}
|
||||
|
||||
override fun onDestroyView(view: View) {
|
||||
super.onDestroyView(view)
|
||||
clearFindViewByIdCache()
|
||||
}
|
||||
|
||||
abstract fun inflateView(inflater: LayoutInflater, container: ViewGroup): View
|
||||
|
||||
open fun onViewCreated(view: View) {}
|
||||
|
||||
@@ -5,11 +5,14 @@ import android.view.View
|
||||
import eu.davidea.viewholders.FlexibleViewHolder
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.glide.GlideApp
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.databinding.ExtensionCardItemBinding
|
||||
import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi
|
||||
import eu.kanade.tachiyomi.extension.model.Extension
|
||||
import eu.kanade.tachiyomi.extension.model.InstallStep
|
||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
class ExtensionHolder(view: View, val adapter: ExtensionAdapter) :
|
||||
@@ -17,6 +20,10 @@ class ExtensionHolder(view: View, val adapter: ExtensionAdapter) :
|
||||
|
||||
private val binding = ExtensionCardItemBinding.bind(view)
|
||||
|
||||
private val shouldLabelNsfw by lazy {
|
||||
Injekt.get<PreferencesHelper>().labelNsfwExtension()
|
||||
}
|
||||
|
||||
init {
|
||||
binding.extButton.setOnClickListener {
|
||||
adapter.buttonClickListener.onButtonClick(bindingAdapterPosition)
|
||||
@@ -35,9 +42,9 @@ class ExtensionHolder(view: View, val adapter: ExtensionAdapter) :
|
||||
extension is Extension.Installed && extension.isUnofficial -> itemView.context.getString(R.string.ext_unofficial)
|
||||
// SY -->
|
||||
extension is Extension.Installed && extension.isRedundant -> itemView.context.getString(R.string.ext_redundant)
|
||||
extension.isNsfw && shouldLabelNsfw -> itemView.context.getString(R.string.ext_nsfw_short).plusRepo(extension)
|
||||
else -> "".plusRepo(extension)
|
||||
// SY <--
|
||||
extension.isNsfw -> itemView.context.getString(R.string.ext_nsfw_short)
|
||||
else -> ""
|
||||
}.toUpperCase()
|
||||
|
||||
GlideApp.with(itemView.context).clear(binding.image)
|
||||
@@ -51,6 +58,23 @@ class ExtensionHolder(view: View, val adapter: ExtensionAdapter) :
|
||||
bindButton(item)
|
||||
}
|
||||
|
||||
// SY -->
|
||||
private fun String.plusRepo(extension: Extension): String {
|
||||
return if (extension is Extension.Available) {
|
||||
when (extension.repoUrl) {
|
||||
ExtensionGithubApi.REPO_URL_PREFIX -> this
|
||||
else -> {
|
||||
this + if (this.isEmpty()) {
|
||||
""
|
||||
} else {
|
||||
" • "
|
||||
} + itemView.context.getString(R.string.repo_source)
|
||||
}
|
||||
}
|
||||
} else this
|
||||
}
|
||||
// SY <--
|
||||
|
||||
@Suppress("ResourceType")
|
||||
fun bindButton(item: ExtensionItem) = with(binding.extButton) {
|
||||
isEnabled = true
|
||||
|
||||
@@ -3,7 +3,6 @@ package eu.kanade.tachiyomi.ui.browse.extension
|
||||
import android.app.Application
|
||||
import android.os.Bundle
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.preference.PreferenceValues
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.extension.ExtensionManager
|
||||
import eu.kanade.tachiyomi.extension.model.Extension
|
||||
@@ -56,7 +55,7 @@ open class ExtensionPresenter(
|
||||
private fun toItems(tuple: ExtensionTuple): List<ExtensionItem> {
|
||||
val context = Injekt.get<Application>()
|
||||
val activeLangs = preferences.enabledLanguages().get()
|
||||
val showNsfwExtensions = preferences.allowNsfwSource().get() != PreferenceValues.NsfwAllowance.BLOCKED
|
||||
val showNsfwExtensions = preferences.showNsfwExtension().get()
|
||||
|
||||
val (installed, untrusted, available) = tuple
|
||||
|
||||
|
||||
-1
@@ -635,7 +635,6 @@ open class BrowseSourceController(bundle: Bundle) :
|
||||
val adapter = adapter ?: return
|
||||
|
||||
preferences.sourceDisplayMode().set(mode)
|
||||
presenter.refreshDisplayMode()
|
||||
activity?.invalidateOptionsMenu()
|
||||
setupRecycler(view)
|
||||
|
||||
|
||||
+26
-49
@@ -2,7 +2,6 @@ package eu.kanade.tachiyomi.ui.browse.source.browse
|
||||
|
||||
import android.os.Bundle
|
||||
import eu.davidea.flexibleadapter.items.IFlexible
|
||||
import eu.davidea.flexibleadapter.items.ISectionable
|
||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
import eu.kanade.tachiyomi.data.database.models.Category
|
||||
@@ -38,10 +37,13 @@ import eu.kanade.tachiyomi.util.lang.launchUI
|
||||
import eu.kanade.tachiyomi.util.removeCovers
|
||||
import exh.savedsearches.EXHSavedSearch
|
||||
import exh.savedsearches.JsonSavedSearch
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asFlow
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
@@ -127,11 +129,6 @@ open class BrowseSourcePresenter(
|
||||
private val filterSerializer = FilterSerializer()
|
||||
// SY <--
|
||||
|
||||
/**
|
||||
* Job to initialize manga details.
|
||||
*/
|
||||
private var initializerJob: Job? = null
|
||||
|
||||
override fun onCreate(savedState: Bundle?) {
|
||||
super.onCreate(savedState)
|
||||
|
||||
@@ -169,8 +166,6 @@ open class BrowseSourcePresenter(
|
||||
this.query = query
|
||||
this.appliedFilters = filters
|
||||
|
||||
initializeManga()
|
||||
|
||||
// Create a new pager.
|
||||
pager = createPager(query, filters)
|
||||
|
||||
@@ -226,27 +221,6 @@ open class BrowseSourcePresenter(
|
||||
return pager.hasNextPage
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribes to the initializer of manga details and updates the view if needed.
|
||||
*/
|
||||
private fun initializeManga() {
|
||||
initializerJob?.cancel()
|
||||
initializerJob = launchIO {
|
||||
mangaDetailsFlow
|
||||
.onEach { mangas ->
|
||||
if (!isActive) return@onEach
|
||||
|
||||
try {
|
||||
mangas.filter { it.thumbnail_url == null && !it.initialized }
|
||||
.map { getMangaDetails(it) }
|
||||
.forEach { launchUI { view?.onMangaInitialized(it) } }
|
||||
} catch (error: Exception) {
|
||||
launchUI { Timber.e(error) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a manga from the database for the given manga from network. It creates a new entry
|
||||
* if the manga is not yet in the database.
|
||||
@@ -272,7 +246,19 @@ open class BrowseSourcePresenter(
|
||||
* @param mangas the list of manga to initialize.
|
||||
*/
|
||||
fun initializeMangas(mangas: List<Manga>) {
|
||||
launchIO { mangaDetailsFlow.emit(mangas) }
|
||||
launchIO {
|
||||
mangas.asFlow()
|
||||
.filter { it.thumbnail_url == null && !it.initialized }
|
||||
.map { getMangaDetails(it) }
|
||||
.onEach {
|
||||
launchUI {
|
||||
@Suppress("DEPRECATION")
|
||||
view?.onMangaInitialized(it)
|
||||
}
|
||||
}
|
||||
.catch { e -> Timber.e(e) }
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -282,17 +268,15 @@ open class BrowseSourcePresenter(
|
||||
* @return the initialized manga
|
||||
*/
|
||||
private suspend fun getMangaDetails(manga: Manga): Manga {
|
||||
return try {
|
||||
source.getMangaDetails(manga.toMangaInfo())
|
||||
.let { networkManga ->
|
||||
manga.copyFrom(networkManga.toSManga())
|
||||
manga.initialized = true
|
||||
db.insertManga(manga).executeAsBlocking()
|
||||
manga
|
||||
}
|
||||
try {
|
||||
val networkManga = source.getMangaDetails(manga.toMangaInfo())
|
||||
manga.copyFrom(networkManga.toSManga())
|
||||
manga.initialized = true
|
||||
db.insertManga(manga).executeAsBlocking()
|
||||
} catch (e: Exception) {
|
||||
manga
|
||||
Timber.e(e)
|
||||
}
|
||||
return manga
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -316,13 +300,6 @@ open class BrowseSourcePresenter(
|
||||
db.insertManga(manga).executeAsBlocking()
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes the active display mode.
|
||||
*/
|
||||
fun refreshDisplayMode() {
|
||||
initializeManga()
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the filter states for the current source.
|
||||
*
|
||||
@@ -363,7 +340,7 @@ open class BrowseSourcePresenter(
|
||||
is Filter.AutoComplete -> AutoCompleteSectionItem(it)
|
||||
// SY <--
|
||||
else -> null
|
||||
} as? ISectionable<*, *>
|
||||
}
|
||||
}
|
||||
subItems.forEach { it.header = group }
|
||||
group.subItems = subItems
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package eu.kanade.tachiyomi.ui.category.repos
|
||||
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
|
||||
/**
|
||||
* Custom adapter for repos.
|
||||
*
|
||||
* @param controller The containing controller.
|
||||
*/
|
||||
class RepoAdapter(controller: RepoController) :
|
||||
FlexibleAdapter<RepoItem>(null, controller, true) {
|
||||
|
||||
/**
|
||||
* Clears the active selections from the list and the model.
|
||||
*/
|
||||
override fun clearSelection() {
|
||||
super.clearSelection()
|
||||
(0 until itemCount).forEach { getItem(it)?.isSelected = false }
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles the selection of the given position.
|
||||
*
|
||||
* @param position The position to toggle.
|
||||
*/
|
||||
override fun toggleSelection(position: Int) {
|
||||
super.toggleSelection(position)
|
||||
getItem(position)?.isSelected = isSelected(position)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,320 @@
|
||||
package eu.kanade.tachiyomi.ui.category.repos
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.view.ActionMode
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.SelectableAdapter
|
||||
import eu.davidea.flexibleadapter.helpers.UndoHelper
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.databinding.CategoriesControllerBinding
|
||||
import eu.kanade.tachiyomi.ui.base.controller.FabController
|
||||
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
|
||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import eu.kanade.tachiyomi.util.view.shrinkOnScroll
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import reactivecircus.flowbinding.android.view.clicks
|
||||
|
||||
/**
|
||||
* Controller to manage the categories for the users' library.
|
||||
*/
|
||||
class RepoController :
|
||||
NucleusController<CategoriesControllerBinding, RepoPresenter>(),
|
||||
FabController,
|
||||
ActionMode.Callback,
|
||||
FlexibleAdapter.OnItemClickListener,
|
||||
FlexibleAdapter.OnItemLongClickListener,
|
||||
RepoCreateDialog.Listener,
|
||||
UndoHelper.OnActionListener {
|
||||
|
||||
/**
|
||||
* Object used to show ActionMode toolbar.
|
||||
*/
|
||||
private var actionMode: ActionMode? = null
|
||||
|
||||
/**
|
||||
* Adapter containing repo items.
|
||||
*/
|
||||
private var adapter: RepoAdapter? = null
|
||||
|
||||
private var actionFab: ExtendedFloatingActionButton? = null
|
||||
private var actionFabScrollListener: RecyclerView.OnScrollListener? = null
|
||||
|
||||
/**
|
||||
* Undo helper used for restoring a deleted repo.
|
||||
*/
|
||||
private var undoHelper: UndoHelper? = null
|
||||
|
||||
/**
|
||||
* Creates the presenter for this controller. Not to be manually called.
|
||||
*/
|
||||
override fun createPresenter() = RepoPresenter()
|
||||
|
||||
/**
|
||||
* Returns the toolbar title to show when this controller is attached.
|
||||
*/
|
||||
override fun getTitle(): String? {
|
||||
return resources?.getString(R.string.action_edit_repos)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the view of this controller.
|
||||
*
|
||||
* @param inflater The layout inflater to create the view from XML.
|
||||
* @param container The parent view for this one.
|
||||
*/
|
||||
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
|
||||
binding = CategoriesControllerBinding.inflate(inflater)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
/**
|
||||
* Called after view inflation. Used to initialize the view.
|
||||
*
|
||||
* @param view The view of this controller.
|
||||
*/
|
||||
override fun onViewCreated(view: View) {
|
||||
super.onViewCreated(view)
|
||||
|
||||
adapter = RepoAdapter(this@RepoController)
|
||||
binding.recycler.layoutManager = LinearLayoutManager(view.context)
|
||||
binding.recycler.setHasFixedSize(true)
|
||||
binding.recycler.adapter = adapter
|
||||
adapter?.isPermanentDelete = false
|
||||
|
||||
actionFabScrollListener = actionFab?.shrinkOnScroll(binding.recycler)
|
||||
}
|
||||
|
||||
override fun configureFab(fab: ExtendedFloatingActionButton) {
|
||||
actionFab = fab
|
||||
fab.setText(R.string.action_add)
|
||||
fab.setIconResource(R.drawable.ic_add_24dp)
|
||||
fab.clicks()
|
||||
.onEach {
|
||||
RepoCreateDialog(this@RepoController).showDialog(router, null)
|
||||
}
|
||||
.launchIn(scope)
|
||||
}
|
||||
|
||||
override fun cleanupFab(fab: ExtendedFloatingActionButton) {
|
||||
actionFabScrollListener?.let { binding.recycler.removeOnScrollListener(it) }
|
||||
actionFab = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the view is being destroyed. Used to release references and remove callbacks.
|
||||
*
|
||||
* @param view The view of this controller.
|
||||
*/
|
||||
override fun onDestroyView(view: View) {
|
||||
// Manually call callback to delete repos if required
|
||||
undoHelper?.onDeleteConfirmed(Snackbar.Callback.DISMISS_EVENT_MANUAL)
|
||||
undoHelper = null
|
||||
actionMode = null
|
||||
adapter = null
|
||||
super.onDestroyView(view)
|
||||
}
|
||||
|
||||
/**
|
||||
* Called from the presenter when the repos are updated.
|
||||
*
|
||||
* @param repos The new list of repos to display.
|
||||
*/
|
||||
fun setRepos(repos: List<RepoItem>) {
|
||||
actionMode?.finish()
|
||||
adapter?.updateDataSet(repos)
|
||||
if (repos.isNotEmpty()) {
|
||||
binding.emptyView.hide()
|
||||
val selected = repos.filter { it.isSelected }
|
||||
if (selected.isNotEmpty()) {
|
||||
selected.forEach { onItemLongClick(repos.indexOf(it)) }
|
||||
}
|
||||
} else {
|
||||
binding.emptyView.show(R.string.information_empty_repos)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when action mode is first created. The menu supplied will be used to generate action
|
||||
* buttons for the action mode.
|
||||
*
|
||||
* @param mode ActionMode being created.
|
||||
* @param menu Menu used to populate action buttons.
|
||||
* @return true if the action mode should be created, false if entering this mode should be
|
||||
* aborted.
|
||||
*/
|
||||
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
|
||||
// Inflate menu.
|
||||
mode.menuInflater.inflate(R.menu.category_selection, menu)
|
||||
// Enable adapter multi selection.
|
||||
adapter?.mode = SelectableAdapter.Mode.MULTI
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Called to refresh an action mode's action menu whenever it is invalidated.
|
||||
*
|
||||
* @param mode ActionMode being prepared.
|
||||
* @param menu Menu used to populate action buttons.
|
||||
* @return true if the menu or action mode was updated, false otherwise.
|
||||
*/
|
||||
override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
|
||||
val adapter = adapter ?: return false
|
||||
val count = adapter.selectedItemCount
|
||||
mode.title = count.toString()
|
||||
|
||||
// Show edit button only when one item is selected
|
||||
val editItem = mode.menu.findItem(R.id.action_edit)
|
||||
editItem.isVisible = false
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Called to report a user click on an action button.
|
||||
*
|
||||
* @param mode The current ActionMode.
|
||||
* @param item The item that was clicked.
|
||||
* @return true if this callback handled the event, false if the standard MenuItem invocation
|
||||
* should continue.
|
||||
*/
|
||||
override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
|
||||
val adapter = adapter ?: return false
|
||||
|
||||
when (item.itemId) {
|
||||
R.id.action_delete -> {
|
||||
undoHelper = UndoHelper(adapter, this)
|
||||
undoHelper?.start(
|
||||
adapter.selectedPositions,
|
||||
(activity as? MainActivity)?.binding?.rootCoordinator!!,
|
||||
R.string.snack_repo_deleted,
|
||||
R.string.action_undo,
|
||||
3000
|
||||
)
|
||||
|
||||
mode.finish()
|
||||
}
|
||||
else -> return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when an action mode is about to be exited and destroyed.
|
||||
*
|
||||
* @param mode The current ActionMode being destroyed.
|
||||
*/
|
||||
override fun onDestroyActionMode(mode: ActionMode) {
|
||||
// Reset adapter to single selection
|
||||
adapter?.mode = SelectableAdapter.Mode.IDLE
|
||||
adapter?.clearSelection()
|
||||
actionMode = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when an item in the list is clicked.
|
||||
*
|
||||
* @param position The position of the clicked item.
|
||||
* @return true if this click should enable selection mode.
|
||||
*/
|
||||
override fun onItemClick(view: View, position: Int): Boolean {
|
||||
// Check if action mode is initialized and selected item exist.
|
||||
return if (actionMode != null && position != RecyclerView.NO_POSITION) {
|
||||
toggleSelection(position)
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when an item in the list is long clicked.
|
||||
*
|
||||
* @param position The position of the clicked item.
|
||||
*/
|
||||
override fun onItemLongClick(position: Int) {
|
||||
val activity = activity as? AppCompatActivity ?: return
|
||||
|
||||
// Check if action mode is initialized.
|
||||
if (actionMode == null) {
|
||||
// Initialize action mode
|
||||
actionMode = activity.startSupportActionMode(this)
|
||||
}
|
||||
|
||||
// Set item as selected
|
||||
toggleSelection(position)
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle the selection state of an item.
|
||||
* If the item was the last one in the selection and is unselected, the ActionMode is finished.
|
||||
*
|
||||
* @param position The position of the item to toggle.
|
||||
*/
|
||||
private fun toggleSelection(position: Int) {
|
||||
val adapter = adapter ?: return
|
||||
|
||||
// Mark the position selected
|
||||
adapter.toggleSelection(position)
|
||||
|
||||
if (adapter.selectedItemCount == 0) {
|
||||
actionMode?.finish()
|
||||
} else {
|
||||
actionMode?.invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the undo action is clicked in the snackbar.
|
||||
*
|
||||
* @param action The action performed.
|
||||
*/
|
||||
override fun onActionCanceled(action: Int, positions: MutableList<Int>?) {
|
||||
adapter?.restoreDeletedItems()
|
||||
undoHelper = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the time to restore the items expires.
|
||||
*
|
||||
* @param action The action performed.
|
||||
* @param event The event that triggered the action
|
||||
*/
|
||||
override fun onActionConfirmed(action: Int, event: Int) {
|
||||
val adapter = adapter ?: return
|
||||
presenter.deleteRepos(adapter.deletedItems.map { it.repo })
|
||||
undoHelper = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new repo with the given name.
|
||||
*
|
||||
* @param name The name of the new repo.
|
||||
*/
|
||||
override fun createRepo(name: String) {
|
||||
presenter.createRepo(name)
|
||||
}
|
||||
|
||||
/**
|
||||
* Called from the presenter when a repo already exists.
|
||||
*/
|
||||
fun onRepoExistsError() {
|
||||
activity?.toast(R.string.error_repo_exists)
|
||||
}
|
||||
|
||||
/**
|
||||
* Called from the presenter when a invalid repo is made
|
||||
*/
|
||||
fun onRepoInvalidNameError() {
|
||||
activity?.toast(R.string.invalid_repo_name)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package eu.kanade.tachiyomi.ui.category.repos
|
||||
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import com.afollestad.materialdialogs.MaterialDialog
|
||||
import com.afollestad.materialdialogs.input.input
|
||||
import com.bluelinelabs.conductor.Controller
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
||||
|
||||
/**
|
||||
* Dialog to create a new repo for the library.
|
||||
*/
|
||||
class RepoCreateDialog<T>(bundle: Bundle? = null) : DialogController(bundle)
|
||||
where T : Controller, T : RepoCreateDialog.Listener {
|
||||
|
||||
/**
|
||||
* Name of the new repo. Value updated with each input from the user.
|
||||
*/
|
||||
private var currentName = ""
|
||||
|
||||
constructor(target: T) : this() {
|
||||
targetController = target
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when creating the dialog for this controller.
|
||||
*
|
||||
* @param savedViewState The saved state of this dialog.
|
||||
* @return a new dialog instance.
|
||||
*/
|
||||
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
|
||||
return MaterialDialog(activity!!)
|
||||
.title(R.string.action_add_repo)
|
||||
.message(R.string.action_add_repo_message)
|
||||
.negativeButton(android.R.string.cancel)
|
||||
.input(
|
||||
hint = resources?.getString(R.string.name),
|
||||
prefill = currentName
|
||||
) { _, input ->
|
||||
currentName = input.toString()
|
||||
}
|
||||
.positiveButton(android.R.string.ok) {
|
||||
(targetController as? Listener)?.createRepo(currentName)
|
||||
}
|
||||
}
|
||||
|
||||
interface Listener {
|
||||
fun createRepo(name: String)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package eu.kanade.tachiyomi.ui.category.repos
|
||||
|
||||
import android.view.View
|
||||
import androidx.core.view.isVisible
|
||||
import eu.davidea.viewholders.FlexibleViewHolder
|
||||
import eu.kanade.tachiyomi.databinding.CategoriesItemBinding
|
||||
|
||||
/**
|
||||
* Holder used to display repo items.
|
||||
*
|
||||
* @param view The view used by repo items.
|
||||
* @param adapter The adapter containing this holder.
|
||||
*/
|
||||
class RepoHolder(view: View, val adapter: RepoAdapter) : FlexibleViewHolder(view, adapter) {
|
||||
|
||||
private val binding = CategoriesItemBinding.bind(view)
|
||||
|
||||
/**
|
||||
* Binds this holder with the given category.
|
||||
*
|
||||
* @param category The category to bind.
|
||||
*/
|
||||
fun bind(category: String) {
|
||||
// Set capitalized title.
|
||||
binding.title.text = category
|
||||
binding.reorder.isVisible = false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package eu.kanade.tachiyomi.ui.category.repos
|
||||
|
||||
import android.view.View
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
|
||||
import eu.davidea.flexibleadapter.items.IFlexible
|
||||
import eu.kanade.tachiyomi.R
|
||||
|
||||
/**
|
||||
* Repo item for a recycler view.
|
||||
*/
|
||||
class RepoItem(val repo: String) : AbstractFlexibleItem<RepoHolder>() {
|
||||
|
||||
/**
|
||||
* Whether this item is currently selected.
|
||||
*/
|
||||
var isSelected = false
|
||||
|
||||
/**
|
||||
* Returns the layout resource for this item.
|
||||
*/
|
||||
override fun getLayoutRes(): Int {
|
||||
return R.layout.categories_item
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new view holder for this item.
|
||||
*
|
||||
* @param view The view of this item.
|
||||
* @param adapter The adapter of this item.
|
||||
*/
|
||||
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): RepoHolder {
|
||||
return RepoHolder(view, adapter as RepoAdapter)
|
||||
}
|
||||
|
||||
/**
|
||||
* Binds the given view holder with this item.
|
||||
*
|
||||
* @param adapter The adapter of this item.
|
||||
* @param holder The holder to bind.
|
||||
* @param position The position of this item in the adapter.
|
||||
* @param payloads List of partial changes.
|
||||
*/
|
||||
override fun bindViewHolder(
|
||||
adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>,
|
||||
holder: RepoHolder,
|
||||
position: Int,
|
||||
payloads: List<Any?>?
|
||||
) {
|
||||
holder.bind(repo)
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
return false
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return repo.hashCode()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package eu.kanade.tachiyomi.ui.category.repos
|
||||
|
||||
import android.os.Bundle
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import rx.Observable
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
/**
|
||||
* Presenter of [RepoController]. Used to manage the repos for the extensions.
|
||||
*/
|
||||
class RepoPresenter(
|
||||
private val preferences: PreferencesHelper = Injekt.get()
|
||||
) : BasePresenter<RepoController>() {
|
||||
val scope = CoroutineScope(Job() + Dispatchers.Main)
|
||||
|
||||
/**
|
||||
* List containing repos.
|
||||
*/
|
||||
private var repos: List<String> = emptyList()
|
||||
|
||||
/**
|
||||
* Called when the presenter is created.
|
||||
*
|
||||
* @param savedState The saved state of this presenter.
|
||||
*/
|
||||
override fun onCreate(savedState: Bundle?) {
|
||||
super.onCreate(savedState)
|
||||
|
||||
preferences.extensionRepos().asFlow().onEach { repos ->
|
||||
this.repos = repos.toList().sortedBy { it.toLowerCase() }
|
||||
|
||||
Observable.just(this.repos)
|
||||
.map { it.map(::RepoItem) }
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeLatestCache(RepoController::setRepos)
|
||||
}.launchIn(scope)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and adds a new repo to the database.
|
||||
*
|
||||
* @param name The name of the repo to create.
|
||||
*/
|
||||
fun createRepo(name: String) {
|
||||
// Do not allow duplicate repos.
|
||||
if (repoExists(name)) {
|
||||
Observable.just(Unit).subscribeFirst({ view, _ -> view.onRepoExistsError() })
|
||||
return
|
||||
}
|
||||
|
||||
// Do not allow invalid formats
|
||||
if (!name.matches(repoRegex)) {
|
||||
Observable.just(Unit).subscribeFirst({ view, _ -> view.onRepoInvalidNameError() })
|
||||
return
|
||||
}
|
||||
|
||||
preferences.extensionRepos().set((repos + name).toSet())
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the given repos from the database.
|
||||
*
|
||||
* @param repos The list of repos to delete.
|
||||
*/
|
||||
fun deleteRepos(repos: List<String>) {
|
||||
preferences.extensionRepos().set(
|
||||
this.repos.filterNot { it in repos }.toSet()
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if a repo with the given name already exists.
|
||||
*/
|
||||
private fun repoExists(name: String): Boolean {
|
||||
return repos.any { it.equals(name, true) }
|
||||
}
|
||||
|
||||
companion object {
|
||||
val repoRegex = """^[a-zA-Z-_.]*?\/[a-zA-Z-_.]*?$""".toRegex()
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
package eu.kanade.tachiyomi.ui.library
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Category
|
||||
import eu.kanade.tachiyomi.databinding.LibraryCategoryBinding
|
||||
import eu.kanade.tachiyomi.util.view.inflate
|
||||
import eu.kanade.tachiyomi.widget.RecyclerViewPagerAdapter
|
||||
|
||||
@@ -34,8 +35,9 @@ class LibraryAdapter(private val controller: LibraryController) : RecyclerViewPa
|
||||
* @return a new view.
|
||||
*/
|
||||
override fun createView(container: ViewGroup): View {
|
||||
val view = container.inflate(R.layout.library_category) as LibraryCategoryView
|
||||
view.onCreate(controller)
|
||||
val binding = LibraryCategoryBinding.inflate(LayoutInflater.from(container.context), container, false)
|
||||
val view: LibraryCategoryView = binding.root
|
||||
view.onCreate(controller, binding)
|
||||
return view
|
||||
}
|
||||
|
||||
|
||||
@@ -16,14 +16,13 @@ import eu.kanade.tachiyomi.data.library.LibraryUpdateService
|
||||
import eu.kanade.tachiyomi.data.preference.PreferenceValues
|
||||
import eu.kanade.tachiyomi.data.preference.PreferenceValues.DisplayMode
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.databinding.LibraryCategoryBinding
|
||||
import eu.kanade.tachiyomi.ui.category.CategoryAdapter
|
||||
import eu.kanade.tachiyomi.util.lang.plusAssign
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import eu.kanade.tachiyomi.util.view.inflate
|
||||
import eu.kanade.tachiyomi.widget.AutofitRecyclerView
|
||||
import exh.ui.LoadingHandle
|
||||
import kotlinx.android.synthetic.main.library_category.view.fast_scroller
|
||||
import kotlinx.android.synthetic.main.library_category.view.swipe_refresh
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
@@ -96,15 +95,15 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
|
||||
}
|
||||
// EXH <--
|
||||
|
||||
fun onCreate(controller: LibraryController) {
|
||||
fun onCreate(controller: LibraryController, binding: LibraryCategoryBinding) {
|
||||
this.controller = controller
|
||||
|
||||
recycler = if (preferences.libraryDisplayMode().get() == DisplayMode.LIST) {
|
||||
(swipe_refresh.inflate(R.layout.library_list_recycler) as RecyclerView).apply {
|
||||
(binding.swipeRefresh.inflate(R.layout.library_list_recycler) as RecyclerView).apply {
|
||||
layoutManager = LinearLayoutManager(context)
|
||||
}
|
||||
} else {
|
||||
(swipe_refresh.inflate(R.layout.library_grid_recycler) as AutofitRecyclerView).apply {
|
||||
(binding.swipeRefresh.inflate(R.layout.library_grid_recycler) as AutofitRecyclerView).apply {
|
||||
spanCount = controller.mangaPerRow
|
||||
}
|
||||
}
|
||||
@@ -113,21 +112,21 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
|
||||
|
||||
recycler.setHasFixedSize(true)
|
||||
recycler.adapter = adapter
|
||||
swipe_refresh.addView(recycler)
|
||||
adapter.fastScroller = fast_scroller
|
||||
binding.swipeRefresh.addView(recycler)
|
||||
adapter.fastScroller = binding.fastScroller
|
||||
|
||||
recycler.scrollStateChanges()
|
||||
.onEach {
|
||||
// Disable swipe refresh when view is not at the top
|
||||
val firstPos = (recycler.layoutManager as LinearLayoutManager)
|
||||
.findFirstCompletelyVisibleItemPosition()
|
||||
swipe_refresh.isEnabled = firstPos <= 0
|
||||
binding.swipeRefresh.isEnabled = firstPos <= 0
|
||||
}
|
||||
.launchIn(scope)
|
||||
|
||||
// Double the distance required to trigger sync
|
||||
swipe_refresh.setDistanceToTriggerSync((2 * 64 * resources.displayMetrics.density).toInt())
|
||||
swipe_refresh.refreshes()
|
||||
binding.swipeRefresh.setDistanceToTriggerSync((2 * 64 * resources.displayMetrics.density).toInt())
|
||||
binding.swipeRefresh.refreshes()
|
||||
.onEach {
|
||||
// SY -->
|
||||
if (LibraryUpdateService.start(context, if (controller.presenter.groupType == LibraryGroup.BY_DEFAULT) category else null, group = controller.presenter.groupType, groupExtra = getGroupExtra())) {
|
||||
@@ -147,7 +146,7 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
|
||||
// SY <--
|
||||
|
||||
// It can be a very long operation, so we disable swipe refresh and show a toast.
|
||||
swipe_refresh.isRefreshing = false
|
||||
binding.swipeRefresh.isRefreshing = false
|
||||
}
|
||||
.launchIn(scope)
|
||||
}
|
||||
|
||||
@@ -174,7 +174,7 @@ class LibraryController(
|
||||
if (preferences.categoryTabs().get()) {
|
||||
currentTitle = resources?.getString(R.string.label_library)
|
||||
} else {
|
||||
adapter?.categories?.get(binding.libraryPager.currentItem)?.let {
|
||||
adapter?.categories?.getOrNull(binding.libraryPager.currentItem)?.let {
|
||||
currentTitle = it.name
|
||||
}
|
||||
}
|
||||
|
||||
@@ -684,7 +684,7 @@ class LibraryPresenter(
|
||||
libraryManga.forEach { libraryItem ->
|
||||
when (groupType) {
|
||||
LibraryGroup.BY_TRACK_STATUS -> {
|
||||
val status: String = {
|
||||
val status: String = run {
|
||||
val tracks = db.getTracks(libraryItem.manga).executeAsBlocking()
|
||||
val track = tracks.find { track ->
|
||||
loggedServices.any { it.id == track?.sync_id }
|
||||
@@ -695,30 +695,30 @@ class LibraryPresenter(
|
||||
} else {
|
||||
"not tracked"
|
||||
}
|
||||
}()
|
||||
}
|
||||
val group = grouping.find { it.first == trackManager.mapTrackingOrder(status, context).toString() }
|
||||
if (group != null) {
|
||||
map[group.second]?.plusAssign(libraryItem) ?: map.put(group.second, mutableListOf(libraryItem))
|
||||
map.getOrPut(group.second) { mutableListOf() } += libraryItem
|
||||
} else {
|
||||
map[7]?.plusAssign(libraryItem) ?: map.put(7, mutableListOf(libraryItem))
|
||||
map.getOrPut(7) { mutableListOf() } += libraryItem
|
||||
}
|
||||
}
|
||||
LibraryGroup.BY_SOURCE -> {
|
||||
val group = grouping.find { it.first.toLongOrNull() == libraryItem.manga.source }
|
||||
if (group != null) {
|
||||
map[group.second]?.plusAssign(libraryItem) ?: map.put(group.second, mutableListOf(libraryItem))
|
||||
map.getOrPut(group.second) { mutableListOf() } += libraryItem
|
||||
} else {
|
||||
if (grouping.all { it.second != Int.MAX_VALUE }) grouping += Triple(Int.MAX_VALUE.toString(), Int.MAX_VALUE, context.getString(R.string.unknown))
|
||||
map[Int.MAX_VALUE]?.plusAssign(libraryItem) ?: map.put(Int.MAX_VALUE, mutableListOf(libraryItem))
|
||||
map.getOrPut(Int.MAX_VALUE) { mutableListOf() } += libraryItem
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
val group = grouping.find { it.second == libraryItem.manga.status }
|
||||
if (group != null) {
|
||||
map[group.second]?.plusAssign(libraryItem) ?: map.put(group.second, mutableListOf(libraryItem))
|
||||
map.getOrPut(group.second) { mutableListOf() } += libraryItem
|
||||
} else {
|
||||
if (grouping.all { it.second != Int.MAX_VALUE }) grouping += Triple(Int.MAX_VALUE.toString(), Int.MAX_VALUE, context.getString(R.string.unknown))
|
||||
map[Int.MAX_VALUE]?.plusAssign(libraryItem) ?: map.put(Int.MAX_VALUE, mutableListOf(libraryItem))
|
||||
map.getOrPut(Int.MAX_VALUE) { mutableListOf() } += libraryItem
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -730,11 +730,12 @@ class LibraryPresenter(
|
||||
LibraryGroup.BY_TRACK_STATUS, LibraryGroup.BY_STATUS -> grouping.filter { it.second in map.keys }
|
||||
else -> grouping
|
||||
}
|
||||
).map {
|
||||
val category = Category.create(it.third)
|
||||
category.id = it.second
|
||||
category
|
||||
}
|
||||
)
|
||||
.map {
|
||||
val category = Category.create(it.third)
|
||||
category.id = it.second
|
||||
category
|
||||
}
|
||||
|
||||
return map to categories
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ import eu.kanade.tachiyomi.data.notification.NotificationReceiver
|
||||
import eu.kanade.tachiyomi.data.preference.asImmediateFlow
|
||||
import eu.kanade.tachiyomi.databinding.MainActivityBinding
|
||||
import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi
|
||||
import eu.kanade.tachiyomi.ui.base.activity.BaseActivity
|
||||
import eu.kanade.tachiyomi.ui.base.activity.BaseViewBindingActivity
|
||||
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
||||
import eu.kanade.tachiyomi.ui.base.controller.FabController
|
||||
import eu.kanade.tachiyomi.ui.base.controller.NoToolbarElevationController
|
||||
@@ -54,7 +54,7 @@ import java.util.Date
|
||||
import java.util.LinkedList
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class MainActivity : BaseActivity<MainActivityBinding>() {
|
||||
class MainActivity : BaseViewBindingActivity<MainActivityBinding>() {
|
||||
|
||||
private lateinit var router: Router
|
||||
|
||||
|
||||
@@ -62,17 +62,13 @@ class EditMangaDialog : DialogController {
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
|
||||
binding = EditMangaDialogBinding.inflate(activity!!.layoutInflater)
|
||||
val dialog = MaterialDialog(activity!!).apply {
|
||||
customView(viewRes = R.layout.edit_manga_dialog, scrollable = true)
|
||||
customView(view = binding.root, scrollable = true)
|
||||
negativeButton(android.R.string.cancel)
|
||||
positiveButton(R.string.action_save) { onPositiveButtonClick() }
|
||||
}
|
||||
binding = EditMangaDialogBinding.bind(dialog.view.contentLayout.customView!!)
|
||||
onViewCreated()
|
||||
dialog.setOnShowListener {
|
||||
val dView = (it as? MaterialDialog)?.view
|
||||
dView?.contentLayout?.scrollView?.scrollTo(0, 0)
|
||||
}
|
||||
return dialog
|
||||
}
|
||||
|
||||
|
||||
@@ -1333,7 +1333,7 @@ class MangaController :
|
||||
private fun markPreviousAsRead(chapters: List<ChapterItem>) {
|
||||
val adapter = chaptersAdapter ?: return
|
||||
val prevChapters = if (presenter.sortDescending()) adapter.items.reversed() else adapter.items
|
||||
val chapterPos = prevChapters.indexOf(chapters.last())
|
||||
val chapterPos = prevChapters.indexOf(chapters.lastOrNull())
|
||||
if (chapterPos != -1) {
|
||||
markAsRead(prevChapters.take(chapterPos))
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package eu.kanade.tachiyomi.ui.manga.merged
|
||||
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.recyclerview.widget.ConcatAdapter
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.afollestad.materialdialogs.MaterialDialog
|
||||
@@ -19,9 +18,6 @@ import exh.merged.sql.models.MergedMangaReference
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
class EditMergedSettingsDialog : DialogController, EditMergedMangaAdapter.EditMergedMangaItemListener {
|
||||
|
||||
private var dialogView: View? = null
|
||||
|
||||
private val manga: Manga
|
||||
|
||||
val mergedMangas: MutableList<Pair<Manga?, MergedMangaReference>> = mutableListOf()
|
||||
@@ -55,22 +51,17 @@ class EditMergedSettingsDialog : DialogController, EditMergedMangaAdapter.EditMe
|
||||
private var mergedMangaAdapter: EditMergedMangaAdapter? = null
|
||||
|
||||
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
|
||||
val dialog = MaterialDialog(activity!!).apply {
|
||||
customView(viewRes = R.layout.edit_merged_settings_dialog, scrollable = true)
|
||||
negativeButton(android.R.string.cancel)
|
||||
positiveButton(R.string.action_save) { onPositiveButtonClick() }
|
||||
}
|
||||
dialogView = dialog.view
|
||||
onViewCreated(dialog.view)
|
||||
dialog.setOnShowListener {
|
||||
val dView = (it as? MaterialDialog)?.view
|
||||
dView?.contentLayout?.scrollView?.scrollTo(0, 0)
|
||||
}
|
||||
binding = EditMergedSettingsDialogBinding.inflate(activity!!.layoutInflater)
|
||||
val dialog = MaterialDialog(activity!!)
|
||||
.customView(view = binding.root, scrollable = true)
|
||||
.negativeButton(android.R.string.cancel)
|
||||
.positiveButton(R.string.action_save) { onPositiveButtonClick() }
|
||||
|
||||
onViewCreated()
|
||||
return dialog
|
||||
}
|
||||
|
||||
fun onViewCreated(view: View) {
|
||||
binding = EditMergedSettingsDialogBinding.bind(view)
|
||||
fun onViewCreated() {
|
||||
val mergedManga = db.getMergedMangas(manga.id!!).executeAsBlocking()
|
||||
val mergedReferences = db.getMergedMangaReferences(manga.id!!).executeAsBlocking()
|
||||
if (mergedReferences.isEmpty() || mergedReferences.size == 1) {
|
||||
@@ -86,18 +77,13 @@ class EditMergedSettingsDialog : DialogController, EditMergedMangaAdapter.EditMe
|
||||
mergedHeaderAdapter = EditMergedSettingsHeaderAdapter(this, mergedMangaAdapter!!)
|
||||
|
||||
binding.recycler.adapter = ConcatAdapter(mergedHeaderAdapter, mergedMangaAdapter)
|
||||
binding.recycler.layoutManager = LinearLayoutManager(view.context)
|
||||
binding.recycler.layoutManager = LinearLayoutManager(activity!!)
|
||||
|
||||
mergedMangaAdapter?.isHandleDragEnabled = isPriorityOrder
|
||||
|
||||
mergedMangaAdapter?.updateDataSet(mergedMangas.map { it.toModel() })
|
||||
}
|
||||
|
||||
override fun onDestroyView(view: View) {
|
||||
super.onDestroyView(view)
|
||||
dialogView = null
|
||||
}
|
||||
|
||||
private fun onPositiveButtonClick() {
|
||||
mangaController.presenter.updateMergeSettings(mergeReference, mergedMangas.map { it.second })
|
||||
}
|
||||
@@ -115,7 +101,7 @@ class EditMergedSettingsDialog : DialogController, EditMergedMangaAdapter.EditMe
|
||||
val mergedMangaAdapter = mergedMangaAdapter ?: return
|
||||
val mergeMangaReference = mergedMangaAdapter.currentItems.getOrNull(position)?.mergedMangaReference ?: return
|
||||
|
||||
MaterialDialog(dialogView!!.context)
|
||||
MaterialDialog(activity!!)
|
||||
.title(R.string.delete_merged_manga)
|
||||
.message(R.string.delete_merged_manga_desc)
|
||||
.positiveButton(android.R.string.ok) {
|
||||
@@ -128,7 +114,7 @@ class EditMergedSettingsDialog : DialogController, EditMergedMangaAdapter.EditMe
|
||||
}
|
||||
|
||||
override fun onToggleChapterUpdatesClicked(position: Int) {
|
||||
MaterialDialog(dialogView!!.context)
|
||||
MaterialDialog(activity!!)
|
||||
.title(R.string.chapter_updates_merged_manga)
|
||||
.message(R.string.chapter_updates_merged_manga_desc)
|
||||
.positiveButton(android.R.string.ok) {
|
||||
@@ -152,7 +138,7 @@ class EditMergedSettingsDialog : DialogController, EditMergedMangaAdapter.EditMe
|
||||
}
|
||||
|
||||
override fun onToggleChapterDownloadsClicked(position: Int) {
|
||||
MaterialDialog(dialogView!!.context)
|
||||
MaterialDialog(activity!!)
|
||||
.title(R.string.download_merged_manga)
|
||||
.message(R.string.download_merged_manga_desc)
|
||||
.positiveButton(android.R.string.ok) {
|
||||
|
||||
+6
-6
@@ -42,7 +42,7 @@ class EditMergedSettingsHeaderAdapter(private val controller: EditMergedSettings
|
||||
android.R.layout.simple_spinner_item,
|
||||
listOf(
|
||||
"No dedupe",
|
||||
"Dedupe by priority",
|
||||
/*"Dedupe by priority",*/
|
||||
"Show source with most chapters",
|
||||
"Show source with highest chapter number"
|
||||
)
|
||||
@@ -54,8 +54,8 @@ class EditMergedSettingsHeaderAdapter(private val controller: EditMergedSettings
|
||||
when (it.chapterSortMode) {
|
||||
MergedMangaReference.CHAPTER_SORT_NO_DEDUPE -> 0
|
||||
MergedMangaReference.CHAPTER_SORT_PRIORITY -> 1
|
||||
MergedMangaReference.CHAPTER_SORT_MOST_CHAPTERS -> 2
|
||||
MergedMangaReference.CHAPTER_SORT_HIGHEST_CHAPTER_NUMBER -> 3
|
||||
MergedMangaReference.CHAPTER_SORT_MOST_CHAPTERS -> 1
|
||||
MergedMangaReference.CHAPTER_SORT_HIGHEST_CHAPTER_NUMBER -> 2
|
||||
else -> 0
|
||||
}
|
||||
)
|
||||
@@ -69,9 +69,9 @@ class EditMergedSettingsHeaderAdapter(private val controller: EditMergedSettings
|
||||
) {
|
||||
controller.mergeReference?.chapterSortMode = when (position) {
|
||||
0 -> MergedMangaReference.CHAPTER_SORT_NO_DEDUPE
|
||||
1 -> MergedMangaReference.CHAPTER_SORT_PRIORITY
|
||||
2 -> MergedMangaReference.CHAPTER_SORT_MOST_CHAPTERS
|
||||
3 -> MergedMangaReference.CHAPTER_SORT_HIGHEST_CHAPTER_NUMBER
|
||||
99 -> MergedMangaReference.CHAPTER_SORT_PRIORITY
|
||||
1 -> MergedMangaReference.CHAPTER_SORT_MOST_CHAPTERS
|
||||
2 -> MergedMangaReference.CHAPTER_SORT_HIGHEST_CHAPTER_NUMBER
|
||||
else -> MergedMangaReference.CHAPTER_SORT_NO_DEDUPE
|
||||
}
|
||||
XLog.d(controller.mergeReference?.chapterSortMode)
|
||||
|
||||
@@ -35,9 +35,6 @@ import java.util.TimeZone
|
||||
|
||||
class AboutController : SettingsController() {
|
||||
|
||||
/**
|
||||
* Checks for new releases
|
||||
*/
|
||||
private val updateChecker by lazy { GithubUpdateChecker() }
|
||||
|
||||
private val dateFormat: DateFormat = preferences.dateFormat()
|
||||
@@ -119,7 +116,7 @@ class AboutController : SettingsController() {
|
||||
preference {
|
||||
key = "pref_about_label_original_tachiyomi_github"
|
||||
title = "Original Tachiyomi GitHub "
|
||||
val url = "https://github.com/inorichi/tachiyomi"
|
||||
val url = "https://github.com/tachiyomiorg/tachiyomi"
|
||||
summary = url
|
||||
onClick {
|
||||
val intent = Intent(Intent.ACTION_VIEW, url.toUri())
|
||||
@@ -130,7 +127,7 @@ class AboutController : SettingsController() {
|
||||
preference {
|
||||
key = "pref_about_label_extensions"
|
||||
titleRes = R.string.label_extensions
|
||||
val url = "https://github.com/inorichi/tachiyomi-extensions"
|
||||
val url = "https://github.com/tachiyomiorg/tachiyomi-extensions"
|
||||
summary = url
|
||||
onClick {
|
||||
val intent = Intent(Intent.ACTION_VIEW, url.toUri())
|
||||
|
||||
@@ -105,6 +105,7 @@ class ReaderSettingsSheet(private val activity: ReaderActivity) : BottomSheetDia
|
||||
binding.webtoonSidePadding.bindToIntPreference(preferences.webtoonSidePadding(), R.array.webtoon_side_padding_values)
|
||||
// SY -->
|
||||
binding.zoomOutWebtoon.bindToPreference(preferences.webtoonEnableZoomOut())
|
||||
binding.cropBordersContinuesVertical.bindToPreference(preferences.cropBordersContinuesVertical())
|
||||
// SY <--
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ class DirectoryPageLoader(val file: File) : PageLoader() {
|
||||
override fun getPages(): Observable<List<ReaderPage>> {
|
||||
return file.listFiles()
|
||||
.filter { !it.isDirectory && ImageUtil.isImage(it.name) { FileInputStream(it) } }
|
||||
.sortedWith(Comparator<File> { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) })
|
||||
.sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) }
|
||||
.mapIndexed { i, file ->
|
||||
val streamFn = { FileInputStream(file) }
|
||||
ReaderPage(i).apply {
|
||||
|
||||
@@ -8,7 +8,6 @@ import eu.kanade.tachiyomi.util.system.ImageUtil
|
||||
import rx.Observable
|
||||
import java.io.File
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipFile
|
||||
|
||||
/**
|
||||
@@ -40,7 +39,7 @@ class ZipPageLoader(file: File) : PageLoader() {
|
||||
override fun getPages(): Observable<List<ReaderPage>> {
|
||||
return zip.entries().toList()
|
||||
.filter { !it.isDirectory && ImageUtil.isImage(it.name) { zip.getInputStream(it) } }
|
||||
.sortedWith(Comparator<ZipEntry> { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) })
|
||||
.sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) }
|
||||
.mapIndexed { i, entry ->
|
||||
val streamFn = { zip.getInputStream(entry) }
|
||||
ReaderPage(i).apply {
|
||||
|
||||
@@ -185,7 +185,7 @@ abstract class PagerViewer(val activity: ReaderActivity) : BaseViewer {
|
||||
* activity of the change and requests the preload of the next chapter if this is the last page.
|
||||
*/
|
||||
private fun onReaderPageSelected(page: ReaderPage, allowPreload: Boolean) {
|
||||
val pages = page.chapter.pages!! // Won't be null because it's the loaded chapter
|
||||
val pages = page.chapter.pages ?: return
|
||||
Timber.d("onReaderPageSelected: ${page.number}/${pages.size}")
|
||||
activity.onPageSelected(page)
|
||||
|
||||
|
||||
@@ -19,6 +19,10 @@ class WebtoonConfig(preferences: PreferencesHelper = Injekt.get()) : ViewerConfi
|
||||
// SY -->
|
||||
var enableZoomOut = false
|
||||
private set
|
||||
|
||||
var continuesCropBorders = false
|
||||
private set
|
||||
|
||||
var zoomPropertyChangedListener: ((Boolean) -> Unit)? = null
|
||||
|
||||
// SY <--
|
||||
@@ -32,6 +36,9 @@ class WebtoonConfig(preferences: PreferencesHelper = Injekt.get()) : ViewerConfi
|
||||
// SY -->
|
||||
preferences.webtoonEnableZoomOut()
|
||||
.register({ enableZoomOut = it }, { zoomPropertyChangedListener?.invoke(it) })
|
||||
|
||||
preferences.cropBordersContinuesVertical()
|
||||
.register({ continuesCropBorders = it }, { imagePropertyChangedListener?.invoke() })
|
||||
// SY <--
|
||||
}
|
||||
}
|
||||
|
||||
@@ -361,7 +361,7 @@ class WebtoonPageHolder(
|
||||
setMinimumScaleType(SubsamplingScaleImageView.SCALE_TYPE_FIT_WIDTH)
|
||||
setMinimumDpi(90)
|
||||
setMinimumTileDpi(180)
|
||||
setCropBorders(config.imageCropBorders)
|
||||
setCropBorders(/* SY --> */ if (viewer.isContinuous) config.continuesCropBorders else /* SY <-- */ config.imageCropBorders)
|
||||
setOnImageEventListener(
|
||||
object : SubsamplingScaleImageView.DefaultOnImageEventListener() {
|
||||
override fun onReady() {
|
||||
|
||||
@@ -191,7 +191,7 @@ class WebtoonViewer(val activity: ReaderActivity, val isContinuous: Boolean = tr
|
||||
* activity of the change and requests the preload of the next chapter if this is the last page.
|
||||
*/
|
||||
private fun onPageSelected(page: ReaderPage, allowPreload: Boolean) {
|
||||
val pages = page.chapter.pages!! // Won't be null because it's the loaded chapter
|
||||
val pages = page.chapter.pages ?: return
|
||||
Timber.d("onPageSelected: ${page.number}/${pages.size}")
|
||||
activity.onPageSelected(page)
|
||||
|
||||
@@ -320,11 +320,11 @@ class WebtoonViewer(val activity: ReaderActivity, val isContinuous: Boolean = tr
|
||||
}
|
||||
KeyEvent.KEYCODE_MENU -> if (isUp) activity.toggleMenu()
|
||||
|
||||
KeyEvent.KEYCODE_DPAD_RIGHT,
|
||||
KeyEvent.KEYCODE_DPAD_LEFT,
|
||||
KeyEvent.KEYCODE_DPAD_UP,
|
||||
KeyEvent.KEYCODE_PAGE_UP -> if (isUp) scrollUp()
|
||||
|
||||
KeyEvent.KEYCODE_DPAD_LEFT,
|
||||
KeyEvent.KEYCODE_DPAD_RIGHT,
|
||||
KeyEvent.KEYCODE_DPAD_DOWN,
|
||||
KeyEvent.KEYCODE_PAGE_DOWN -> if (isUp) scrollDown()
|
||||
else -> return false
|
||||
|
||||
@@ -2,26 +2,26 @@ package eu.kanade.tachiyomi.ui.recent
|
||||
|
||||
import android.text.format.DateUtils
|
||||
import android.view.View
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.AbstractHeaderItem
|
||||
import eu.davidea.flexibleadapter.items.IFlexible
|
||||
import eu.davidea.viewholders.FlexibleViewHolder
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.databinding.RecentSectionItemBinding
|
||||
import java.util.Date
|
||||
|
||||
class DateSectionItem(val date: Date) : AbstractHeaderItem<DateSectionItem.Holder>() {
|
||||
class DateSectionItem(val date: Date) : AbstractHeaderItem<DateSectionItem.DateSectionItemHolder>() {
|
||||
|
||||
override fun getLayoutRes(): Int {
|
||||
return R.layout.recent_section_item
|
||||
}
|
||||
|
||||
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): Holder {
|
||||
return Holder(view, adapter)
|
||||
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): DateSectionItemHolder {
|
||||
return DateSectionItemHolder(view, adapter)
|
||||
}
|
||||
|
||||
override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>, holder: Holder, position: Int, payloads: List<Any?>?) {
|
||||
override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>, holder: DateSectionItemHolder, position: Int, payloads: List<Any?>?) {
|
||||
holder.bind(this)
|
||||
}
|
||||
|
||||
@@ -37,14 +37,14 @@ class DateSectionItem(val date: Date) : AbstractHeaderItem<DateSectionItem.Holde
|
||||
return date.hashCode()
|
||||
}
|
||||
|
||||
class Holder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter, true) {
|
||||
inner class DateSectionItemHolder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter, true) {
|
||||
|
||||
private val binding = RecentSectionItemBinding.bind(view)
|
||||
|
||||
private val now = Date().time
|
||||
|
||||
val section_text: TextView = view.findViewById(R.id.section_text)
|
||||
|
||||
fun bind(item: DateSectionItem) {
|
||||
section_text.text = DateUtils.getRelativeTimeSpanString(item.date.time, now, DateUtils.DAY_IN_MILLIS)
|
||||
binding.sectionText.text = DateUtils.getRelativeTimeSpanString(item.date.time, now, DateUtils.DAY_IN_MILLIS)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,7 +141,7 @@ class SettingsAdvancedController : SettingsController() {
|
||||
switchPreference {
|
||||
key = Keys.enableDoh
|
||||
titleRes = R.string.pref_dns_over_https
|
||||
summaryRes = R.string.pref_dns_over_https_summary
|
||||
summaryRes = R.string.requires_app_restart
|
||||
defaultValue = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,7 +39,6 @@ import eu.kanade.tachiyomi.util.preference.preferenceCategory
|
||||
import eu.kanade.tachiyomi.util.preference.summaryRes
|
||||
import eu.kanade.tachiyomi.util.preference.switchPreference
|
||||
import eu.kanade.tachiyomi.util.preference.titleRes
|
||||
import eu.kanade.tachiyomi.util.system.getFilePicker
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
@@ -125,13 +124,11 @@ class SettingsBackupController : SettingsController() {
|
||||
titleRes = R.string.pref_backup_directory
|
||||
|
||||
onClick {
|
||||
val currentDir = preferences.backupsDirectory().get()
|
||||
try {
|
||||
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
|
||||
startActivityForResult(intent, CODE_BACKUP_DIR)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
// Fall back to custom picker on error
|
||||
startActivityForResult(preferences.context.getFilePicker(currentDir), CODE_BACKUP_DIR)
|
||||
activity?.toast(R.string.file_picker_error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -205,7 +202,7 @@ class SettingsBackupController : SettingsController() {
|
||||
)
|
||||
}
|
||||
CODE_BACKUP_RESTORE -> {
|
||||
uri?.path?.let { path ->
|
||||
uri?.path?.let {
|
||||
val fileName = DocumentFile.fromSingleUri(activity, uri)!!.name!!
|
||||
when {
|
||||
fileName.endsWith(".proto.gz") -> {
|
||||
@@ -258,7 +255,6 @@ class SettingsBackupController : SettingsController() {
|
||||
|
||||
fun createBackup(flags: Int, type: Int) {
|
||||
backupFlags = flags
|
||||
val currentDir = preferences.backupsDirectory().get()
|
||||
val code = when (type) {
|
||||
BackupConst.BACKUP_TYPE_FULL -> CODE_FULL_BACKUP_CREATE
|
||||
else -> CODE_LEGACY_BACKUP_CREATE
|
||||
@@ -277,8 +273,7 @@ class SettingsBackupController : SettingsController() {
|
||||
|
||||
startActivityForResult(intent, code)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
// Handle errors where the Android ROM doesn't support the built in picker
|
||||
startActivityForResult(preferences.context.getFilePicker(currentDir), code)
|
||||
activity?.toast(R.string.file_picker_error)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,10 +2,13 @@ package eu.kanade.tachiyomi.ui.setting
|
||||
|
||||
import androidx.preference.PreferenceScreen
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.preference.asImmediateFlow
|
||||
import eu.kanade.tachiyomi.extension.ExtensionUpdateJob
|
||||
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
||||
import eu.kanade.tachiyomi.ui.category.repos.RepoController
|
||||
import eu.kanade.tachiyomi.ui.category.sources.SourceCategoryController
|
||||
import eu.kanade.tachiyomi.util.preference.defaultValue
|
||||
import eu.kanade.tachiyomi.util.preference.infoPreference
|
||||
import eu.kanade.tachiyomi.util.preference.onChange
|
||||
import eu.kanade.tachiyomi.util.preference.onClick
|
||||
import eu.kanade.tachiyomi.util.preference.preference
|
||||
@@ -13,6 +16,7 @@ import eu.kanade.tachiyomi.util.preference.preferenceCategory
|
||||
import eu.kanade.tachiyomi.util.preference.summaryRes
|
||||
import eu.kanade.tachiyomi.util.preference.switchPreference
|
||||
import eu.kanade.tachiyomi.util.preference.titleRes
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys
|
||||
|
||||
class SettingsBrowseController : SettingsController() {
|
||||
@@ -81,6 +85,19 @@ class SettingsBrowseController : SettingsController() {
|
||||
true
|
||||
}
|
||||
}
|
||||
// SY -->
|
||||
preference {
|
||||
key = "pref_edit_extension_repos"
|
||||
titleRes = R.string.action_edit_repos
|
||||
|
||||
val catCount = preferences.extensionRepos().get().count()
|
||||
summary = context.resources.getQuantityString(R.plurals.num_repos, catCount, catCount)
|
||||
|
||||
onClick {
|
||||
router.pushController(RepoController().withFadeTransaction())
|
||||
}
|
||||
}
|
||||
// SY <--
|
||||
}
|
||||
|
||||
preferenceCategory {
|
||||
@@ -93,27 +110,29 @@ class SettingsBrowseController : SettingsController() {
|
||||
}
|
||||
}
|
||||
|
||||
// preferenceCategory {
|
||||
// titleRes = R.string.pref_category_nsfw_content
|
||||
//
|
||||
// listPreference {
|
||||
// key = Keys.allowNsfwSource
|
||||
// titleRes = R.string.pref_allow_nsfw_sources
|
||||
// entriesRes = arrayOf(
|
||||
// R.string.pref_allow_nsfw_sources_allowed,
|
||||
// R.string.pref_allow_nsfw_sources_allowed_multisource,
|
||||
// R.string.pref_allow_nsfw_sources_blocked
|
||||
// )
|
||||
// entryValues = arrayOf(
|
||||
// PreferenceValues.NsfwAllowance.ALLOWED.name,
|
||||
// PreferenceValues.NsfwAllowance.PARTIAL.name,
|
||||
// PreferenceValues.NsfwAllowance.BLOCKED.name
|
||||
// )
|
||||
// defaultValue = PreferenceValues.NsfwAllowance.ALLOWED.name
|
||||
// summary = "%s"
|
||||
// }
|
||||
//
|
||||
// infoPreference(R.string.parental_controls_info)
|
||||
// }
|
||||
preferenceCategory {
|
||||
titleRes = R.string.pref_category_nsfw_content
|
||||
|
||||
switchPreference {
|
||||
key = Keys.showNsfwSource
|
||||
titleRes = R.string.pref_show_nsfw_source
|
||||
summaryRes = R.string.requires_app_restart
|
||||
defaultValue = true
|
||||
}
|
||||
switchPreference {
|
||||
key = Keys.showNsfwExtension
|
||||
titleRes = R.string.pref_show_nsfw_extension
|
||||
defaultValue = true
|
||||
}
|
||||
switchPreference {
|
||||
key = Keys.labelNsfwExtension
|
||||
titleRes = R.string.pref_label_nsfw_extension
|
||||
defaultValue = true
|
||||
|
||||
preferences.showNsfwExtension().asImmediateFlow { isVisible = it }.launchIn(scope)
|
||||
}
|
||||
|
||||
infoPreference(R.string.parental_controls_info)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ import eu.kanade.tachiyomi.util.preference.preference
|
||||
import eu.kanade.tachiyomi.util.preference.preferenceCategory
|
||||
import eu.kanade.tachiyomi.util.preference.switchPreference
|
||||
import eu.kanade.tachiyomi.util.preference.titleRes
|
||||
import eu.kanade.tachiyomi.util.system.getFilePicker
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import uy.kohesive.injekt.Injekt
|
||||
@@ -203,12 +203,12 @@ class SettingsDownloadController : SettingsController() {
|
||||
preferences.downloadsDirectory().set(path.toString())
|
||||
}
|
||||
|
||||
fun customDirectorySelected(currentDir: String) {
|
||||
fun customDirectorySelected() {
|
||||
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
|
||||
try {
|
||||
startActivityForResult(intent, DOWNLOAD_DIR)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
startActivityForResult(preferences.context.getFilePicker(currentDir), DOWNLOAD_DIR)
|
||||
activity?.toast(R.string.file_picker_error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -229,7 +229,7 @@ class SettingsDownloadController : SettingsController() {
|
||||
) { _, position, text ->
|
||||
val target = targetController as? SettingsDownloadController
|
||||
if (position == externalDirs.lastIndex) {
|
||||
target?.customDirectorySelected(currentDir)
|
||||
target?.customDirectorySelected()
|
||||
} else {
|
||||
target?.predefinedDirectorySelected(text.toString())
|
||||
}
|
||||
|
||||
@@ -317,6 +317,11 @@ class SettingsReaderController : SettingsController() {
|
||||
summaryRes = R.string.tap_scroll_page_summary
|
||||
defaultValue = false
|
||||
}
|
||||
switchPreference {
|
||||
key = Keys.cropBordersContinuesVertical
|
||||
titleRes = R.string.pref_crop_borders
|
||||
defaultValue = false
|
||||
}
|
||||
}
|
||||
// SY <--
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ package eu.kanade.tachiyomi.ui.setting
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import androidx.browser.customtabs.CustomTabsIntent
|
||||
import androidx.preference.PreferenceScreen
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||
@@ -20,7 +19,7 @@ import eu.kanade.tachiyomi.util.preference.onClick
|
||||
import eu.kanade.tachiyomi.util.preference.preferenceCategory
|
||||
import eu.kanade.tachiyomi.util.preference.switchPreference
|
||||
import eu.kanade.tachiyomi.util.preference.titleRes
|
||||
import eu.kanade.tachiyomi.util.system.getResourceColor
|
||||
import eu.kanade.tachiyomi.util.system.openInBrowser
|
||||
import eu.kanade.tachiyomi.widget.preference.LoginPreference
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys
|
||||
@@ -47,11 +46,9 @@ class SettingsTrackingController :
|
||||
startActivity(MyAnimeListLoginActivity.newIntent(activity!!))
|
||||
}
|
||||
trackPreference(trackManager.aniList) {
|
||||
val tabsIntent = CustomTabsIntent.Builder()
|
||||
.setToolbarColor(context.getResourceColor(R.attr.colorPrimary))
|
||||
.build()
|
||||
tabsIntent.intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY)
|
||||
tabsIntent.launchUrl(activity!!, AnilistApi.authUrl())
|
||||
activity?.openInBrowser(AnilistApi.authUrl(), trackManager.aniList.getLogoColor()) {
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY)
|
||||
}
|
||||
}
|
||||
trackPreference(trackManager.kitsu) {
|
||||
val dialog = TrackLoginDialog(trackManager.kitsu, R.string.email)
|
||||
@@ -59,18 +56,14 @@ class SettingsTrackingController :
|
||||
dialog.showDialog(router)
|
||||
}
|
||||
trackPreference(trackManager.shikimori) {
|
||||
val tabsIntent = CustomTabsIntent.Builder()
|
||||
.setToolbarColor(context.getResourceColor(R.attr.colorPrimary))
|
||||
.build()
|
||||
tabsIntent.intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY)
|
||||
tabsIntent.launchUrl(activity!!, ShikimoriApi.authUrl())
|
||||
activity?.openInBrowser(ShikimoriApi.authUrl(), trackManager.shikimori.getLogoColor()) {
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY)
|
||||
}
|
||||
}
|
||||
trackPreference(trackManager.bangumi) {
|
||||
val tabsIntent = CustomTabsIntent.Builder()
|
||||
.setToolbarColor(context.getResourceColor(R.attr.colorPrimary))
|
||||
.build()
|
||||
tabsIntent.intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY)
|
||||
tabsIntent.launchUrl(activity!!, BangumiApi.authUrl())
|
||||
activity?.openInBrowser(BangumiApi.authUrl(), trackManager.bangumi.getLogoColor()) {
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY)
|
||||
}
|
||||
}
|
||||
}
|
||||
preferenceCategory {
|
||||
|
||||
@@ -35,7 +35,7 @@ object SettingsSearchHelper {
|
||||
* All subclasses of `SettingsController` should be listed here, in order to have their preferences searchable.
|
||||
*/
|
||||
// SY -->
|
||||
private val settingControllersList: List<KClass<out SettingsController>> = {
|
||||
private val settingControllersList: List<KClass<out SettingsController>> = run {
|
||||
val controllers = mutableListOf(
|
||||
SettingsAdvancedController::class,
|
||||
SettingsBackupController::class,
|
||||
@@ -55,7 +55,7 @@ object SettingsSearchHelper {
|
||||
controllers += SettingsEhController::class
|
||||
}
|
||||
controllers
|
||||
}()
|
||||
}
|
||||
// SY <--
|
||||
|
||||
/**
|
||||
@@ -119,7 +119,7 @@ object SettingsSearchHelper {
|
||||
(pref.title != null) -> {
|
||||
// Is an actual preference
|
||||
val title = pref.title.toString()
|
||||
val summary = if (pref.summary != null) pref.summary.toString() else ""
|
||||
val summary = pref.summary?.toString() ?: ""
|
||||
val breadcrumbsStr = addLocalizedBreadcrumb(breadcrumbs, "${pref.title}")
|
||||
|
||||
prefSearchResultList.add(
|
||||
|
||||
@@ -1,30 +1,14 @@
|
||||
package eu.kanade.tachiyomi.ui.setting.track
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.Gravity.CENTER
|
||||
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ProgressBar
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||
import android.net.Uri
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import rx.schedulers.Schedulers
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
class AnilistLoginActivity : AppCompatActivity() {
|
||||
|
||||
private val trackManager: TrackManager by injectLazy()
|
||||
|
||||
override fun onCreate(savedState: Bundle?) {
|
||||
super.onCreate(savedState)
|
||||
|
||||
val view = ProgressBar(this)
|
||||
setContentView(view, FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT, CENTER))
|
||||
class AnilistLoginActivity : BaseOAuthLoginActivity() {
|
||||
|
||||
override fun handleResult(data: Uri?) {
|
||||
val regex = "(?:access_token=)(.*?)(?:&)".toRegex()
|
||||
val matchResult = regex.find(intent.data?.fragment.toString())
|
||||
val matchResult = regex.find(data?.fragment.toString())
|
||||
if (matchResult?.groups?.get(1) != null) {
|
||||
trackManager.aniList.login(matchResult.groups[1]!!.value)
|
||||
.subscribeOn(Schedulers.io())
|
||||
@@ -42,12 +26,4 @@ class AnilistLoginActivity : AppCompatActivity() {
|
||||
returnToSettings()
|
||||
}
|
||||
}
|
||||
|
||||
private fun returnToSettings() {
|
||||
finish()
|
||||
|
||||
val intent = Intent(this, MainActivity::class.java)
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
||||
startActivity(intent)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,29 +1,13 @@
|
||||
package eu.kanade.tachiyomi.ui.setting.track
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.Gravity.CENTER
|
||||
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ProgressBar
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||
import android.net.Uri
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import rx.schedulers.Schedulers
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
class BangumiLoginActivity : AppCompatActivity() {
|
||||
class BangumiLoginActivity : BaseOAuthLoginActivity() {
|
||||
|
||||
private val trackManager: TrackManager by injectLazy()
|
||||
|
||||
override fun onCreate(savedState: Bundle?) {
|
||||
super.onCreate(savedState)
|
||||
|
||||
val view = ProgressBar(this)
|
||||
setContentView(view, FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT, CENTER))
|
||||
|
||||
val code = intent.data?.getQueryParameter("code")
|
||||
override fun handleResult(data: Uri?) {
|
||||
val code = data?.getQueryParameter("code")
|
||||
if (code != null) {
|
||||
trackManager.bangumi.login(code)
|
||||
.subscribeOn(Schedulers.io())
|
||||
@@ -41,12 +25,4 @@ class BangumiLoginActivity : AppCompatActivity() {
|
||||
returnToSettings()
|
||||
}
|
||||
}
|
||||
|
||||
private fun returnToSettings() {
|
||||
finish()
|
||||
|
||||
val intent = Intent(this, MainActivity::class.java)
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
||||
startActivity(intent)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
package eu.kanade.tachiyomi.ui.setting.track
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.Gravity
|
||||
import android.view.ViewGroup
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ProgressBar
|
||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||
import eu.kanade.tachiyomi.ui.base.activity.BaseThemedActivity
|
||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
abstract class BaseOAuthLoginActivity : BaseThemedActivity() {
|
||||
|
||||
internal val trackManager: TrackManager by injectLazy()
|
||||
|
||||
abstract fun handleResult(data: Uri?)
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
val view = ProgressBar(this)
|
||||
setContentView(
|
||||
view,
|
||||
FrameLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||
Gravity.CENTER
|
||||
)
|
||||
)
|
||||
|
||||
handleResult(intent.data)
|
||||
}
|
||||
|
||||
internal fun returnToSettings() {
|
||||
finish()
|
||||
|
||||
val intent = Intent(this, MainActivity::class.java)
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
||||
startActivity(intent)
|
||||
}
|
||||
}
|
||||
@@ -1,29 +1,13 @@
|
||||
package eu.kanade.tachiyomi.ui.setting.track
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.Gravity.CENTER
|
||||
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ProgressBar
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||
import android.net.Uri
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import rx.schedulers.Schedulers
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
class ShikimoriLoginActivity : AppCompatActivity() {
|
||||
class ShikimoriLoginActivity : BaseOAuthLoginActivity() {
|
||||
|
||||
private val trackManager: TrackManager by injectLazy()
|
||||
|
||||
override fun onCreate(savedState: Bundle?) {
|
||||
super.onCreate(savedState)
|
||||
|
||||
val view = ProgressBar(this)
|
||||
setContentView(view, FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT, CENTER))
|
||||
|
||||
val code = intent.data?.getQueryParameter("code")
|
||||
override fun handleResult(data: Uri?) {
|
||||
val code = data?.getQueryParameter("code")
|
||||
if (code != null) {
|
||||
trackManager.shikimori.login(code)
|
||||
.subscribeOn(Schedulers.io())
|
||||
@@ -41,12 +25,4 @@ class ShikimoriLoginActivity : AppCompatActivity() {
|
||||
returnToSettings()
|
||||
}
|
||||
}
|
||||
|
||||
private fun returnToSettings() {
|
||||
finish()
|
||||
|
||||
val intent = Intent(this, MainActivity::class.java)
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
||||
startActivity(intent)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import androidx.core.view.isVisible
|
||||
import eu.kanade.tachiyomi.BuildConfig
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.databinding.WebviewActivityBinding
|
||||
import eu.kanade.tachiyomi.ui.base.activity.BaseActivity
|
||||
import eu.kanade.tachiyomi.ui.base.activity.BaseViewBindingActivity
|
||||
import eu.kanade.tachiyomi.util.system.WebViewUtil
|
||||
import eu.kanade.tachiyomi.util.system.setDefaultSettings
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
@@ -19,7 +19,7 @@ import kotlinx.coroutines.flow.onEach
|
||||
import reactivecircus.flowbinding.appcompat.navigationClicks
|
||||
import reactivecircus.flowbinding.swiperefreshlayout.refreshes
|
||||
|
||||
open class BaseWebViewActivity : BaseActivity<WebviewActivityBinding>() {
|
||||
open class BaseWebViewActivity : BaseViewBindingActivity<WebviewActivityBinding>() {
|
||||
|
||||
internal var bundle: Bundle? = null
|
||||
|
||||
@@ -31,15 +31,17 @@ open class BaseWebViewActivity : BaseActivity<WebviewActivityBinding>() {
|
||||
if (!WebViewUtil.supportsWebView(this)) {
|
||||
toast(R.string.information_webview_required, Toast.LENGTH_LONG)
|
||||
finish()
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
binding = WebviewActivityBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
} catch (e: Exception) {
|
||||
} catch (e: Throwable) {
|
||||
// Potentially throws errors like "Error inflating class android.webkit.WebView"
|
||||
toast(R.string.information_webview_required, Toast.LENGTH_LONG)
|
||||
finish()
|
||||
return
|
||||
}
|
||||
|
||||
title = intent.extras?.getString(TITLE_KEY)
|
||||
|
||||
@@ -181,7 +181,7 @@ private suspend fun <T> Observable<T>.awaitOne(): T = suspendCancellableCoroutin
|
||||
internal fun <T> CancellableContinuation<T>.unsubscribeOnCancellation(sub: Subscription) =
|
||||
invokeOnCancellation { sub.unsubscribe() }
|
||||
|
||||
fun <T : Any> Observable<T>.asFlow(): Flow<T> = callbackFlow {
|
||||
fun <T : Any?> Observable<T>.asFlow(): Flow<T> = callbackFlow {
|
||||
val observer = object : Observer<T> {
|
||||
override fun onNext(t: T) {
|
||||
offer(t)
|
||||
@@ -199,7 +199,7 @@ fun <T : Any> Observable<T>.asFlow(): Flow<T> = callbackFlow {
|
||||
awaitClose { subscription.unsubscribe() }
|
||||
}
|
||||
|
||||
fun <T : Any> Flow<T>.asObservable(backpressureMode: Emitter.BackpressureMode = Emitter.BackpressureMode.NONE): Observable<T> {
|
||||
fun <T : Any?> Flow<T>.asObservable(backpressureMode: Emitter.BackpressureMode = Emitter.BackpressureMode.NONE): Observable<T> {
|
||||
return Observable.create(
|
||||
{ emitter ->
|
||||
/*
|
||||
|
||||
@@ -13,12 +13,14 @@ import android.content.pm.PackageManager
|
||||
import android.content.res.Resources
|
||||
import android.graphics.Color
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.Uri
|
||||
import android.os.PowerManager
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.AttrRes
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.browser.customtabs.CustomTabColorSchemeParams
|
||||
import androidx.browser.customtabs.CustomTabsIntent
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
@@ -29,10 +31,8 @@ import androidx.core.graphics.green
|
||||
import androidx.core.graphics.red
|
||||
import androidx.core.net.toUri
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||
import com.nononsenseapps.filepicker.FilePickerActivity
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.util.lang.truncateCenter
|
||||
import eu.kanade.tachiyomi.widget.CustomLayoutPickerActivity
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -109,19 +109,6 @@ fun Context.notification(channelId: String, block: (NotificationCompat.Builder.(
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to construct an Intent to use a custom file picker.
|
||||
* @param currentDir the path the file picker will open with.
|
||||
* @return an Intent to start the file picker activity.
|
||||
*/
|
||||
fun Context.getFilePicker(currentDir: String): Intent {
|
||||
return Intent(this, CustomLayoutPickerActivity::class.java)
|
||||
.putExtra(FilePickerActivity.EXTRA_ALLOW_MULTIPLE, false)
|
||||
.putExtra(FilePickerActivity.EXTRA_ALLOW_CREATE_DIR, true)
|
||||
.putExtra(FilePickerActivity.EXTRA_MODE, FilePickerActivity.MODE_DIR)
|
||||
.putExtra(FilePickerActivity.EXTRA_START_PATH, currentDir)
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the give permission is granted.
|
||||
*
|
||||
@@ -250,12 +237,21 @@ fun Context.isServiceRunning(serviceClass: Class<*>): Boolean {
|
||||
/**
|
||||
* Opens a URL in a custom tab.
|
||||
*/
|
||||
fun Context.openInBrowser(url: String) {
|
||||
fun Context.openInBrowser(url: String, @ColorInt toolbarColor: Int? = null, block: CustomTabsIntent.() -> Unit = {}) {
|
||||
this.openInBrowser(url.toUri(), toolbarColor, block)
|
||||
}
|
||||
|
||||
fun Context.openInBrowser(uri: Uri, @ColorInt toolbarColor: Int? = null, block: CustomTabsIntent.() -> Unit = {}) {
|
||||
try {
|
||||
val intent = CustomTabsIntent.Builder()
|
||||
.setToolbarColor(getResourceColor(R.attr.colorPrimary))
|
||||
.setDefaultColorSchemeParams(
|
||||
CustomTabColorSchemeParams.Builder()
|
||||
.setToolbarColor(toolbarColor ?: getResourceColor(R.attr.colorPrimary))
|
||||
.build()
|
||||
)
|
||||
.build()
|
||||
intent.launchUrl(this, url.toUri())
|
||||
block(intent)
|
||||
intent.launchUrl(this, uri)
|
||||
} catch (e: Exception) {
|
||||
toast(e.message)
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ object WebViewUtil {
|
||||
|
||||
const val REQUESTED_WITH = "com.android.browser"
|
||||
|
||||
const val MINIMUM_WEBVIEW_VERSION = 84
|
||||
const val MINIMUM_WEBVIEW_VERSION = 86
|
||||
|
||||
fun supportsWebView(context: Context): Boolean {
|
||||
try {
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
package eu.kanade.tachiyomi.widget
|
||||
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.nononsenseapps.filepicker.AbstractFilePickerFragment
|
||||
import com.nononsenseapps.filepicker.FilePickerActivity
|
||||
import com.nononsenseapps.filepicker.FilePickerFragment
|
||||
import com.nononsenseapps.filepicker.LogicHandler
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.util.view.inflate
|
||||
import java.io.File
|
||||
|
||||
class CustomLayoutPickerActivity : FilePickerActivity() {
|
||||
|
||||
override fun getFragment(startPath: String?, mode: Int, allowMultiple: Boolean, allowCreateDir: Boolean):
|
||||
AbstractFilePickerFragment<File> {
|
||||
val fragment = CustomLayoutFilePickerFragment()
|
||||
fragment.setArgs(startPath, mode, allowMultiple, allowCreateDir)
|
||||
return fragment
|
||||
}
|
||||
}
|
||||
|
||||
class CustomLayoutFilePickerFragment : FilePickerFragment() {
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
return when (viewType) {
|
||||
LogicHandler.VIEWTYPE_DIR -> {
|
||||
val view = parent.inflate(R.layout.common_listitem_dir)
|
||||
DirViewHolder(view)
|
||||
}
|
||||
else -> super.onCreateViewHolder(parent, viewType)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.source.online.UrlImportableSource
|
||||
import eu.kanade.tachiyomi.source.online.all.EHentai
|
||||
@@ -14,6 +15,8 @@ import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
|
||||
import eu.kanade.tachiyomi.util.lang.awaitSingle
|
||||
import exh.source.getMainSource
|
||||
import exh.util.executeOnIO
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
class GalleryAdder {
|
||||
@@ -22,6 +25,11 @@ class GalleryAdder {
|
||||
|
||||
private val sourceManager: SourceManager by injectLazy()
|
||||
|
||||
private val filters: Pair<Set<String>, Set<Long>> = run {
|
||||
val preferences = Injekt.get<PreferencesHelper>()
|
||||
preferences.enabledLanguages().get() to preferences.disabledSources().get().map { it.toLong() }.toSet()
|
||||
}
|
||||
|
||||
private val logger = XLog.tag("GalleryAdder").enableStackTrace(2).build()
|
||||
|
||||
fun pickSource(url: String): List<UrlImportableSource> {
|
||||
@@ -30,7 +38,7 @@ class GalleryAdder {
|
||||
.map { it.getMainSource() }
|
||||
.filterIsInstance<UrlImportableSource>()
|
||||
.filter {
|
||||
try {
|
||||
it.lang in filters.first && it.id !in filters.second && try {
|
||||
it.matchesUri(uri)
|
||||
} catch (e: Exception) {
|
||||
false
|
||||
@@ -63,7 +71,7 @@ class GalleryAdder {
|
||||
.map { it.getMainSource() }
|
||||
.filterIsInstance<UrlImportableSource>()
|
||||
.find {
|
||||
try {
|
||||
it.lang in filters.first && it.id !in filters.second && try {
|
||||
it.matchesUri(uri)
|
||||
} catch (e: Exception) {
|
||||
false
|
||||
|
||||
@@ -35,18 +35,17 @@ object MetadataUtil {
|
||||
private const val GB_FACTOR = 1000 * MB_FACTOR
|
||||
private const val GIB_FACTOR = 1024 * MIB_FACTOR
|
||||
|
||||
fun parseHumanReadableByteCount(arg0: String): Double? {
|
||||
val spaceNdx = arg0.indexOf(" ")
|
||||
val ret = java.lang.Double.parseDouble(arg0.substring(0, spaceNdx))
|
||||
when (arg0.substring(spaceNdx + 1)) {
|
||||
"GB" -> return ret * GB_FACTOR
|
||||
"GiB" -> return ret * GIB_FACTOR
|
||||
"MB" -> return ret * MB_FACTOR
|
||||
"MiB" -> return ret * MIB_FACTOR
|
||||
"KB" -> return ret * KB_FACTOR
|
||||
"KiB" -> return ret * KIB_FACTOR
|
||||
fun parseHumanReadableByteCount(bytes: String): Double? {
|
||||
val ret = bytes.substringBefore(' ').toDouble()
|
||||
return when (bytes.substringAfter(' ')) {
|
||||
"GB" -> ret * GB_FACTOR
|
||||
"GiB" -> ret * GIB_FACTOR
|
||||
"MB" -> ret * MB_FACTOR
|
||||
"MiB" -> ret * MIB_FACTOR
|
||||
"KB" -> ret * KB_FACTOR
|
||||
"KiB" -> ret * KIB_FACTOR
|
||||
else -> null
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
val ONGOING_SUFFIX = arrayOf(
|
||||
|
||||
@@ -59,8 +59,8 @@ class EHentaiSearchMetadata : RaisedSearchMetadata() {
|
||||
titleObj?.let { manga.title = it }
|
||||
|
||||
// Set artist (if we can find one)
|
||||
tags.filter { it.namespace == EH_ARTIST_NAMESPACE }.let {
|
||||
if (it.isNotEmpty()) manga.artist = it.joinToString(transform = { it.name })
|
||||
tags.filter { it.namespace == EH_ARTIST_NAMESPACE }.let { tags ->
|
||||
if (tags.isNotEmpty()) manga.artist = tags.joinToString(transform = { it.name })
|
||||
}
|
||||
|
||||
// Copy tags -> genres
|
||||
@@ -77,35 +77,7 @@ class EHentaiSearchMetadata : RaisedSearchMetadata() {
|
||||
}
|
||||
}
|
||||
|
||||
// Build a nice looking description out of what we know
|
||||
/* val titleDesc = StringBuilder()
|
||||
title?.let { titleDesc += "Title: $it\n" }
|
||||
altTitle?.let { titleDesc += "Alternate Title: $it\n" }
|
||||
|
||||
val detailsDesc = StringBuilder()
|
||||
genre?.let { detailsDesc += "Genre: $it\n" }
|
||||
uploader?.let { detailsDesc += "Uploader: $it\n" }
|
||||
datePosted?.let { detailsDesc += "Posted: ${EX_DATE_FORMAT.format(Date(it))}\n" }
|
||||
visible?.let { detailsDesc += "Visible: $it\n" }
|
||||
language?.let {
|
||||
detailsDesc += "Language: $it"
|
||||
if (translated == true) detailsDesc += " TR"
|
||||
detailsDesc += "\n"
|
||||
}
|
||||
size?.let { detailsDesc += "File size: ${humanReadableByteCount(it, true)}\n" }
|
||||
length?.let { detailsDesc += "Length: $it pages\n" }
|
||||
favorites?.let { detailsDesc += "Favorited: $it times\n" }
|
||||
averageRating?.let {
|
||||
detailsDesc += "Rating: $it"
|
||||
ratingCount?.let { detailsDesc += " ($it)" }
|
||||
detailsDesc += "\n"
|
||||
}
|
||||
|
||||
val tagsDesc = tagsToDescription()*/
|
||||
|
||||
manga.description = "meta" /*listOf(titleDesc.toString(), detailsDesc.toString(), tagsDesc.toString())
|
||||
.filter(String::isNotBlank)
|
||||
.joinToString(separator = "\n")*/
|
||||
manga.description = "meta"
|
||||
}
|
||||
|
||||
override fun getExtraInfoPairs(context: Context): List<Pair<String, String>> {
|
||||
|
||||
@@ -29,14 +29,7 @@ class EightMusesSearchMetadata : RaisedSearchMetadata() {
|
||||
|
||||
manga.genre = tagsToGenreString()
|
||||
|
||||
/*val titleDesc = StringBuilder()
|
||||
title?.let { titleDesc += "Title: $it\n" }
|
||||
|
||||
val tagsDesc = tagsToDescription()*/
|
||||
|
||||
manga.description = "meta" /*listOf(titleDesc.toString(), tagsDesc.toString())
|
||||
.filter(String::isNotBlank)
|
||||
.joinToString(separator = "\n")*/
|
||||
manga.description = "meta"
|
||||
}
|
||||
|
||||
override fun getExtraInfoPairs(context: Context): List<Pair<String, String>> {
|
||||
|
||||
@@ -37,15 +37,7 @@ class HBrowseSearchMetadata : RaisedSearchMetadata() {
|
||||
|
||||
manga.genre = tagsToGenreString()
|
||||
|
||||
/*val titleDesc = StringBuilder()
|
||||
title?.let { titleDesc += "Title: $it\n" }
|
||||
length?.let { titleDesc += "Length: $it page(s)\n" }
|
||||
|
||||
val tagsDesc = tagsToDescription()*/
|
||||
|
||||
manga.description = "meta" /*listOf(titleDesc.toString(), tagsDesc.toString())
|
||||
.filter(String::isNotBlank)
|
||||
.joinToString(separator = "\n")*/
|
||||
manga.description = "meta"
|
||||
}
|
||||
|
||||
override fun getExtraInfoPairs(context: Context): List<Pair<String, String>> {
|
||||
|
||||
@@ -36,14 +36,7 @@ class HentaiCafeSearchMetadata : RaisedSearchMetadata() {
|
||||
|
||||
manga.genre = tagsToGenreString()
|
||||
|
||||
/* val detailsDesc = "Title: $title\n" +
|
||||
"Artist: $artist\n"
|
||||
|
||||
val tagsDesc = tagsToDescription()*/
|
||||
|
||||
manga.description = "meta" /*listOf(detailsDesc, tagsDesc.toString())
|
||||
.filter(String::isNotBlank)
|
||||
.joinToString(separator = "\n")*/
|
||||
manga.description = "meta"
|
||||
}
|
||||
|
||||
override fun getExtraInfoPairs(context: Context): List<Pair<String, String>> {
|
||||
|
||||
@@ -51,45 +51,7 @@ class HitomiSearchMetadata : RaisedSearchMetadata() {
|
||||
|
||||
manga.status = SManga.UNKNOWN
|
||||
|
||||
/*val titleDesc = StringBuilder()
|
||||
|
||||
title?.let {
|
||||
titleDesc += "Title: $it\n"
|
||||
}
|
||||
|
||||
val detailsDesc = StringBuilder()
|
||||
|
||||
detailsDesc += "Artist(s): ${manga.artist}\n"
|
||||
|
||||
group?.let {
|
||||
detailsDesc += "Group: $it\n"
|
||||
}
|
||||
|
||||
type?.let {
|
||||
detailsDesc += "Type: ${it.capitalize()}\n"
|
||||
}
|
||||
|
||||
(language ?: "unknown").let {
|
||||
detailsDesc += "Language: ${it.capitalize()}\n"
|
||||
}
|
||||
|
||||
if (series.isNotEmpty()) {
|
||||
detailsDesc += "Series: ${series.joinToString()}\n"
|
||||
}
|
||||
|
||||
if (characters.isNotEmpty()) {
|
||||
detailsDesc += "Characters: ${characters.joinToString()}\n"
|
||||
}
|
||||
|
||||
uploadDate?.let {
|
||||
detailsDesc += "Upload date: ${EX_DATE_FORMAT.format(Date(it))}\n"
|
||||
}
|
||||
|
||||
val tagsDesc = tagsToDescription()*/
|
||||
|
||||
manga.description = "meta" /*listOf(titleDesc.toString(), detailsDesc.toString(), tagsDesc.toString())
|
||||
.filter(String::isNotBlank)
|
||||
.joinToString(separator = "\n")*/
|
||||
manga.description = "meta"
|
||||
}
|
||||
|
||||
override fun getExtraInfoPairs(context: Context): List<Pair<String, String>> {
|
||||
|
||||
@@ -53,8 +53,8 @@ class NHentaiSearchMetadata : RaisedSearchMetadata() {
|
||||
}
|
||||
|
||||
// Set artist (if we can find one)
|
||||
tags.filter { it.namespace == NHENTAI_ARTIST_NAMESPACE }.let {
|
||||
if (it.isNotEmpty()) manga.artist = it.joinToString(transform = { it.name })
|
||||
tags.filter { it.namespace == NHENTAI_ARTIST_NAMESPACE }.let { tags ->
|
||||
if (tags.isNotEmpty()) manga.artist = tags.joinToString(transform = { it.name })
|
||||
}
|
||||
|
||||
// Copy tags -> genres
|
||||
|
||||
@@ -56,41 +56,7 @@ class PervEdenSearchMetadata : RaisedSearchMetadata() {
|
||||
// Copy tags -> genres
|
||||
manga.genre = tagsToGenreString()
|
||||
|
||||
/*val titleDesc = StringBuilder()
|
||||
|
||||
title?.let {
|
||||
titleDesc += "Title: $it\n"
|
||||
}
|
||||
if (altTitles.isNotEmpty()) {
|
||||
titleDesc += "Alternate Titles: \n" + altTitles
|
||||
.joinToString(separator = "\n", postfix = "\n") {
|
||||
"▪ $it"
|
||||
}
|
||||
}
|
||||
|
||||
val detailsDesc = StringBuilder()
|
||||
artist?.let {
|
||||
detailsDesc += "Artist: $it\n"
|
||||
}
|
||||
|
||||
type?.let {
|
||||
detailsDesc += "Type: $it\n"
|
||||
}
|
||||
|
||||
status?.let {
|
||||
detailsDesc += "Status: $it\n"
|
||||
}
|
||||
|
||||
rating?.let {
|
||||
detailsDesc += "Rating: %.2\n".format(it)
|
||||
}
|
||||
|
||||
|
||||
val tagsDesc = tagsToDescription()*/
|
||||
|
||||
manga.description = "meta" /*listOf(titleDesc.toString(), detailsDesc.toString(), tagsDesc.toString())
|
||||
.filter(String::isNotBlank)
|
||||
.joinToString(separator = "\n")*/
|
||||
manga.description = "meta"
|
||||
}
|
||||
|
||||
override fun getExtraInfoPairs(context: Context): List<Pair<String, String>> {
|
||||
|
||||
@@ -45,21 +45,7 @@ class PururinSearchMetadata : RaisedSearchMetadata() {
|
||||
|
||||
manga.genre = tagsToGenreString()
|
||||
|
||||
/*val titleDesc = StringBuilder()
|
||||
title?.let { titleDesc += "English Title: $it\n" }
|
||||
altTitle?.let { titleDesc += "Japanese Title: $it\n" }
|
||||
|
||||
val detailsDesc = StringBuilder()
|
||||
(uploaderDisp ?: uploader)?.let { detailsDesc += "Uploader: $it\n" }
|
||||
pages?.let { detailsDesc += "Length: $it pages\n" }
|
||||
fileSize?.let { detailsDesc += "Size: $it\n" }
|
||||
ratingCount?.let { detailsDesc += "Rating: $averageRating ($ratingCount)\n" }
|
||||
|
||||
val tagsDesc = tagsToDescription()*/
|
||||
|
||||
manga.description = "meta" /*listOf(titleDesc.toString(), detailsDesc.toString(), tagsDesc.toString())
|
||||
.filter(String::isNotBlank)
|
||||
.joinToString(separator = "\n")*/
|
||||
manga.description = "meta"
|
||||
}
|
||||
|
||||
override fun getExtraInfoPairs(context: Context): List<Pair<String, String>> {
|
||||
|
||||
@@ -52,32 +52,7 @@ class TsuminoSearchMetadata : RaisedSearchMetadata() {
|
||||
// Copy tags -> genres
|
||||
manga.genre = tagsToGenreString()
|
||||
|
||||
/*val titleDesc = "Title: $title\n"
|
||||
|
||||
val detailsDesc = StringBuilder()
|
||||
uploader?.let { detailsDesc += "Uploader: $it\n" }
|
||||
uploadDate?.let { detailsDesc += "Uploaded: ${EX_DATE_FORMAT.format(Date(it))}\n" }
|
||||
length?.let { detailsDesc += "Length: $it pages\n" }
|
||||
ratingString?.let { detailsDesc += "Rating: $it\n" }
|
||||
category?.let {
|
||||
detailsDesc += "Category: $it\n"
|
||||
}
|
||||
collection?.let { detailsDesc += "Collection: $it\n" }
|
||||
group?.let { detailsDesc += "Group: $it\n" }
|
||||
val parodiesString = parody.joinToString()
|
||||
if (parodiesString.isNotEmpty()) {
|
||||
detailsDesc += "Parody: $parodiesString\n"
|
||||
}
|
||||
val charactersString = character.joinToString()
|
||||
if (charactersString.isNotEmpty()) {
|
||||
detailsDesc += "Character: $charactersString\n"
|
||||
}
|
||||
|
||||
val tagsDesc = tagsToDescription()*/
|
||||
|
||||
manga.description = "meta" /*listOf(titleDesc, detailsDesc.toString(), tagsDesc.toString())
|
||||
.filter(String::isNotBlank)
|
||||
.joinToString(separator = "\n")*/
|
||||
manga.description = "meta"
|
||||
}
|
||||
|
||||
override fun getExtraInfoPairs(context: Context): List<Pair<String, String>> {
|
||||
|
||||
@@ -22,6 +22,7 @@ import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonArray
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.buildJsonObject
|
||||
import kotlinx.serialization.json.contentOrNull
|
||||
import kotlinx.serialization.json.jsonArray
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
@@ -103,10 +104,21 @@ class Anilist : API("https://graphql.anilist.co/") {
|
||||
}
|
||||
|
||||
private fun getTitle(obj: JsonObject): String {
|
||||
return obj["title"]!!.jsonObject.let {
|
||||
it["romaji"]?.jsonPrimitive?.content
|
||||
?: it["english"]?.jsonPrimitive?.content
|
||||
?: it["native"]!!.jsonPrimitive.content
|
||||
val titleObj = obj["title"]!!.jsonObject
|
||||
|
||||
val english = titleObj["english"]?.jsonPrimitive?.contentOrNull
|
||||
val romaji = titleObj["romaji"]?.jsonPrimitive?.contentOrNull
|
||||
val native = titleObj["native"]?.jsonPrimitive?.contentOrNull
|
||||
val synonym = obj["synonyms"]!!.jsonArray.getOrNull(0)?.jsonPrimitive?.contentOrNull
|
||||
|
||||
val isJP = obj["countryOfOrigin"]!!.jsonPrimitive.content == "JP"
|
||||
|
||||
return when {
|
||||
!english.isNullOrBlank() -> english
|
||||
isJP && !romaji.isNullOrBlank() -> romaji
|
||||
!synonym.isNullOrBlank() -> synonym
|
||||
!isJP && !romaji.isNullOrBlank() -> romaji
|
||||
else -> native ?: "NO NAME FOUND"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,12 +138,14 @@ class Anilist : API("https://graphql.anilist.co/") {
|
||||
|edges {
|
||||
|node {
|
||||
|mediaRecommendation {
|
||||
|countryOfOrigin
|
||||
|siteUrl
|
||||
|title {
|
||||
|romaji
|
||||
|english
|
||||
|native
|
||||
|}
|
||||
|synonyms
|
||||
|coverImage {
|
||||
|large
|
||||
|}
|
||||
@@ -203,7 +217,7 @@ open class RecommendsPager(
|
||||
recs
|
||||
} catch (e: Exception) {
|
||||
Timber.tag("RECOMMENDATIONS").e("%s > Error: %s", key, e.message)
|
||||
listOf()
|
||||
listOf<SMangaImpl>()
|
||||
}
|
||||
}
|
||||
.firstOrNull { it.isNotEmpty() }
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
package exh.ui.base
|
||||
|
||||
import androidx.annotation.CallSuper
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.mapNotNull
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import nucleus.presenter.Presenter
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
open class CoroutinePresenter<V> : Presenter<V>() {
|
||||
val scope = CoroutineScope(Job() + Dispatchers.Main)
|
||||
|
||||
@Suppress("DeprecatedCallableAddReplaceWith")
|
||||
@Deprecated("Use launchInView")
|
||||
override fun getView(): V? {
|
||||
return super.getView()
|
||||
}
|
||||
|
||||
fun launchInView(block: (CoroutineScope, V) -> Unit) = scope.launch(Dispatchers.Main) {
|
||||
view?.let { block.invoke(this, it) }
|
||||
}
|
||||
|
||||
fun <F> Flow<F>.onEachView(block: (V, F) -> Unit) = onEach {
|
||||
view?.let { view -> block(view, it) }
|
||||
}
|
||||
|
||||
fun <F, P> Flow<F>.mapView(block: (V, F) -> P): Flow<P> = mapNotNull {
|
||||
view?.let { view -> block(view, it) }
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
override fun destroy() {
|
||||
super.destroy()
|
||||
scope.cancel()
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@ import com.afollestad.materialdialogs.list.listItemsSingleChoice
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.databinding.EhActivityInterceptBinding
|
||||
import eu.kanade.tachiyomi.source.online.UrlImportableSource
|
||||
import eu.kanade.tachiyomi.ui.base.activity.BaseActivity
|
||||
import eu.kanade.tachiyomi.ui.base.activity.BaseViewBindingActivity
|
||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||
import exh.GalleryAddEvent
|
||||
@@ -23,7 +23,7 @@ import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class InterceptActivity : BaseActivity<EhActivityInterceptBinding>() {
|
||||
class InterceptActivity : BaseViewBindingActivity<EhActivityInterceptBinding>() {
|
||||
private var statusJob: Job? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
|
||||
@@ -11,12 +11,9 @@ import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.databinding.MetadataViewControllerBinding
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.source.online.MetadataSource
|
||||
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||
import exh.metadata.metadata.base.FlatMetadata
|
||||
import exh.metadata.metadata.base.RaisedSearchMetadata
|
||||
import exh.source.getMainSource
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
@@ -74,13 +71,6 @@ class MetadataViewController : NucleusController<MetadataViewControllerBinding,
|
||||
binding.recycler.setHasFixedSize(true)
|
||||
}
|
||||
|
||||
fun onNextMetaInfo(flatMetadata: FlatMetadata) {
|
||||
val mainSource = presenter.source.getMainSource()
|
||||
if (mainSource is MetadataSource<*, *>) {
|
||||
presenter.meta = flatMetadata.raise(mainSource.metaClass)
|
||||
}
|
||||
}
|
||||
|
||||
fun onNextMangaInfo(meta: RaisedSearchMetadata?) {
|
||||
val context = view?.context ?: return
|
||||
data = meta?.getExtraInfoPairs(context).orEmpty()
|
||||
|
||||
@@ -1,17 +1,23 @@
|
||||
package exh.ui.metadata
|
||||
|
||||
import android.os.Bundle
|
||||
import com.elvishew.xlog.XLog
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||
import eu.kanade.tachiyomi.source.online.MetadataSource
|
||||
import eu.kanade.tachiyomi.util.lang.asFlow
|
||||
import exh.metadata.metadata.base.FlatMetadata
|
||||
import exh.metadata.metadata.base.RaisedSearchMetadata
|
||||
import exh.metadata.metadata.base.getFlatMetadataForManga
|
||||
import rx.Observable
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import exh.source.getMainSource
|
||||
import exh.ui.base.CoroutinePresenter
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.plus
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
@@ -20,29 +26,31 @@ class MetadataViewPresenter(
|
||||
val source: Source,
|
||||
val preferences: PreferencesHelper = Injekt.get(),
|
||||
private val db: DatabaseHelper = Injekt.get()
|
||||
) : BasePresenter<MetadataViewController>() {
|
||||
) : CoroutinePresenter<MetadataViewController>() {
|
||||
|
||||
var meta: RaisedSearchMetadata? = null
|
||||
val meta = MutableStateFlow<RaisedSearchMetadata?>(null)
|
||||
|
||||
override fun onCreate(savedState: Bundle?) {
|
||||
super.onCreate(savedState)
|
||||
|
||||
getMangaMetaObservable().subscribeLatestCache({ view, flatMetadata -> if (flatMetadata != null) view.onNextMetaInfo(flatMetadata) else XLog.tag("MetadataViewPresenter").disableStackTrace().d("Invalid metadata") })
|
||||
getMangaMetaObservable()
|
||||
.onEach {
|
||||
if (it == null) return@onEach
|
||||
val mainSource = source.getMainSource()
|
||||
if (mainSource is MetadataSource<*, *>) {
|
||||
meta.value = it.raise(mainSource.metaClass)
|
||||
}
|
||||
}
|
||||
.launchIn(scope + Dispatchers.IO)
|
||||
|
||||
getMangaObservable()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeLatestCache({ view, _ -> view.onNextMangaInfo(meta) })
|
||||
meta
|
||||
.onEachView { view, metadata ->
|
||||
view.onNextMangaInfo(metadata)
|
||||
}
|
||||
.launchIn(scope)
|
||||
}
|
||||
|
||||
private fun getMangaObservable(): Observable<Manga> {
|
||||
return db.getManga(manga.url, manga.source).asRxObservable()
|
||||
}
|
||||
|
||||
private fun getMangaMetaObservable(): Observable<FlatMetadata?> {
|
||||
val mangaId = manga.id
|
||||
return if (mangaId != null) {
|
||||
db.getFlatMetadataForManga(mangaId).asRxObservable()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
} else Observable.just(null)
|
||||
private fun getMangaMetaObservable(): Flow<FlatMetadata?> {
|
||||
return db.getFlatMetadataForManga(manga.id!!).asRxObservable().asFlow()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ package exh.widget.preference
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.core.view.isVisible
|
||||
import com.afollestad.materialdialogs.MaterialDialog
|
||||
@@ -40,7 +39,7 @@ class MangadexLoginDialog(bundle: Bundle? = null) : DialogController(bundle) {
|
||||
|
||||
val scope = CoroutineScope(Job() + Dispatchers.Main)
|
||||
|
||||
var binding: PrefSiteLoginTwoFactorAuthBinding? = null
|
||||
lateinit var binding: PrefSiteLoginTwoFactorAuthBinding
|
||||
|
||||
constructor(source: MangaDex) : this(
|
||||
bundleOf(
|
||||
@@ -51,31 +50,31 @@ class MangadexLoginDialog(bundle: Bundle? = null) : DialogController(bundle) {
|
||||
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
|
||||
binding = PrefSiteLoginTwoFactorAuthBinding.inflate(LayoutInflater.from(activity!!))
|
||||
val dialog = MaterialDialog(activity!!)
|
||||
.customView(view = binding!!.root, scrollable = false)
|
||||
.customView(view = binding.root, scrollable = false)
|
||||
|
||||
onViewCreated(dialog.view)
|
||||
onViewCreated()
|
||||
|
||||
return dialog
|
||||
}
|
||||
|
||||
fun onViewCreated(view: View) {
|
||||
binding!!.login.setMode(ActionProcessButton.Mode.ENDLESS)
|
||||
binding!!.login.setOnClickListener { checkLogin() }
|
||||
fun onViewCreated() {
|
||||
binding.login.setMode(ActionProcessButton.Mode.ENDLESS)
|
||||
binding.login.setOnClickListener { checkLogin() }
|
||||
|
||||
setCredentialsOnView()
|
||||
|
||||
binding!!.twoFactorCheck.setOnCheckedChangeListener { _, isChecked ->
|
||||
binding!!.twoFactorHolder.isVisible = isChecked
|
||||
binding.twoFactorCheck.setOnCheckedChangeListener { _, isChecked ->
|
||||
binding.twoFactorHolder.isVisible = isChecked
|
||||
}
|
||||
}
|
||||
|
||||
private fun setCredentialsOnView() {
|
||||
binding?.username?.setText(service.getUsername())
|
||||
binding?.password?.setText(service.getPassword())
|
||||
binding.username.setText(service.getUsername())
|
||||
binding.password.setText(service.getPassword())
|
||||
}
|
||||
|
||||
private fun checkLogin() {
|
||||
binding?.apply {
|
||||
with(binding) {
|
||||
if (username.text.isNullOrBlank() || password.text.isNullOrBlank() || (twoFactorCheck.isChecked && twoFactorEdit.text.isNullOrBlank())) {
|
||||
errorResult()
|
||||
root.context.toast(R.string.fields_cannot_be_blank)
|
||||
@@ -110,7 +109,7 @@ class MangadexLoginDialog(bundle: Bundle? = null) : DialogController(bundle) {
|
||||
}
|
||||
|
||||
private fun errorResult() {
|
||||
binding?.apply {
|
||||
with(binding) {
|
||||
dialog?.setCancelable(true)
|
||||
dialog?.setCanceledOnTouchOutside(true)
|
||||
login.progress = -1
|
||||
@@ -127,7 +126,6 @@ class MangadexLoginDialog(bundle: Bundle? = null) : DialogController(bundle) {
|
||||
|
||||
private fun onDialogClosed() {
|
||||
scope.cancel()
|
||||
binding = null
|
||||
if (activity != null) {
|
||||
(activity as? Listener)?.siteLoginDialogClosed(source!!)
|
||||
} else {
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M10,4H4c-1.1,0 -1.99,0.9 -1.99,2L2,18c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2V8c0,-1.1 -0.9,-2 -2,-2h-8l-2,-2z" />
|
||||
</vector>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user