mirror of
https://github.com/mihonapp/mihon.git
synced 2025-06-26 19:17:51 +02:00
Compare commits
47 Commits
Author | SHA1 | Date | |
---|---|---|---|
e863e8c64b | |||
f5b591430c | |||
8cfaf8eb51 | |||
675c0cefc3 | |||
1a52385b78 | |||
372e500590 | |||
cc1a317439 | |||
6d650518a1 | |||
7940117577 | |||
b0f87fdd21 | |||
dc92ffed87 | |||
4af578e310 | |||
e22825d818 | |||
e2da6259e7 | |||
d149017c60 | |||
afc400121b | |||
ef993515c6 | |||
edb1d21ddc | |||
ba8abd94a8 | |||
c6d4e4c15f | |||
09f0ac866f | |||
7ed25704d6 | |||
2196dac63e | |||
c8f70efded | |||
ea97488670 | |||
c2255b0a0f | |||
f754b081ce | |||
07771cb5e4 | |||
690d8e43ae | |||
82f14a7d59 | |||
b284384f0a | |||
1ae0d1b5d0 | |||
9de08c8166 | |||
a2d007f2a9 | |||
774f818bbb | |||
0ec7121b8f | |||
d7d46f4447 | |||
45fad147bf | |||
3664195c71 | |||
fce3cd00a1 | |||
33b3be0d0e | |||
cfd1b4a6c6 | |||
d45fefd6f0 | |||
f125ab01ee | |||
be001d090c | |||
971d8a7e40 | |||
a2cf210a52 |
2
.github/ISSUE_TEMPLATE.md
vendored
2
.github/ISSUE_TEMPLATE.md
vendored
@ -3,7 +3,7 @@
|
||||
I acknowledge that:
|
||||
|
||||
- I have updated:
|
||||
- To the latest version of the app (stable is v0.12.2)
|
||||
- To the latest version of the app (stable is v0.12.3)
|
||||
- All extensions
|
||||
- I have tried the troubleshooting guide: https://tachiyomi.org/help/guides/troubleshooting-problems/
|
||||
- If this is an issue with an extension, that I should be opening an issue in https://github.com/tachiyomiorg/tachiyomi-extensions
|
||||
|
4
.github/ISSUE_TEMPLATE/report_issue.yml
vendored
4
.github/ISSUE_TEMPLATE/report_issue.yml
vendored
@ -53,7 +53,7 @@ body:
|
||||
label: Tachiyomi version
|
||||
description: You can find your Tachiyomi version in **More → About**.
|
||||
placeholder: |
|
||||
Example: "0.12.2"
|
||||
Example: "0.12.3"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@ -98,7 +98,7 @@ body:
|
||||
required: true
|
||||
- label: I have tried the [troubleshooting guide](https://tachiyomi.org/help/guides/troubleshooting/).
|
||||
required: true
|
||||
- label: I have updated the app to version **[0.12.2](https://github.com/tachiyomiorg/tachiyomi/releases/latest)**.
|
||||
- label: I have updated the app to version **[0.12.3](https://github.com/tachiyomiorg/tachiyomi/releases/latest)**.
|
||||
required: true
|
||||
- label: I have updated all installed extensions.
|
||||
required: true
|
||||
|
2
.github/ISSUE_TEMPLATE/request_feature.yml
vendored
2
.github/ISSUE_TEMPLATE/request_feature.yml
vendored
@ -33,7 +33,7 @@ body:
|
||||
required: true
|
||||
- label: If this is an issue with an extension, I should be opening an issue in the [extensions repository](https://github.com/tachiyomiorg/tachiyomi-extensions/issues/new/choose).
|
||||
required: true
|
||||
- label: I have updated the app to version **[0.12.2](https://github.com/tachiyomiorg/tachiyomi/releases/latest)**.
|
||||
- label: I have updated the app to version **[0.12.3](https://github.com/tachiyomiorg/tachiyomi/releases/latest)**.
|
||||
required: true
|
||||
- label: I will fill out all of the requested information in this form.
|
||||
required: true
|
||||
|
2
.github/workflows/issue_closer.yml
vendored
2
.github/workflows/issue_closer.yml
vendored
@ -25,7 +25,7 @@ jobs:
|
||||
},
|
||||
{
|
||||
"type": "both",
|
||||
"regex": ".*(aniyomi|anime).*",
|
||||
"regex": "^(?!.*myanimelist.*).*(aniyomi|anime).*$",
|
||||
"ignoreCase": true,
|
||||
"message": "Tachiyomi does not support anime, and has no plans to support anime. In addition Tachiyomi is not affiliated with Aniyomi https://github.com/jmir1/aniyomi"
|
||||
}
|
||||
|
@ -29,8 +29,8 @@ android {
|
||||
minSdk = AndroidConfig.minSdk
|
||||
targetSdk = AndroidConfig.targetSdk
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
versionCode = 68
|
||||
versionName = "0.12.2"
|
||||
versionCode = 69
|
||||
versionName = "0.12.3"
|
||||
|
||||
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
|
||||
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
|
||||
@ -138,19 +138,20 @@ dependencies {
|
||||
implementation("org.tachiyomi:source-api:1.1")
|
||||
|
||||
// AndroidX libraries
|
||||
implementation("androidx.annotation:annotation:1.3.0-alpha01")
|
||||
implementation("androidx.annotation:annotation:1.3.0-beta01")
|
||||
implementation("androidx.appcompat:appcompat:1.4.0-alpha03")
|
||||
implementation("androidx.biometric:biometric-ktx:1.2.0-alpha03")
|
||||
implementation("androidx.browser:browser:1.3.0")
|
||||
implementation("androidx.constraintlayout:constraintlayout:2.1.0")
|
||||
implementation("androidx.browser:browser:1.4.0-beta01")
|
||||
implementation("androidx.constraintlayout:constraintlayout:2.1.1")
|
||||
implementation("androidx.coordinatorlayout:coordinatorlayout:1.1.0")
|
||||
implementation("androidx.core:core-ktx:1.7.0-alpha02")
|
||||
implementation("androidx.core:core-splashscreen:1.0.0-alpha01")
|
||||
implementation("androidx.recyclerview:recyclerview:1.2.1")
|
||||
implementation("androidx.core:core-ktx:1.7.0-beta02")
|
||||
implementation("androidx.core:core-splashscreen:1.0.0-alpha02")
|
||||
implementation("androidx.recyclerview:recyclerview:1.3.0-alpha01")
|
||||
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01")
|
||||
implementation("androidx.viewpager:viewpager:1.1.0-alpha01")
|
||||
|
||||
val lifecycleVersion = "2.4.0-alpha01"
|
||||
implementation("androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion")
|
||||
val lifecycleVersion = "2.4.0-beta01"
|
||||
implementation("androidx.lifecycle:lifecycle-common:$lifecycleVersion")
|
||||
implementation("androidx.lifecycle:lifecycle-process:$lifecycleVersion")
|
||||
implementation("androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion")
|
||||
|
||||
@ -174,7 +175,7 @@ dependencies {
|
||||
implementation("org.conscrypt:conscrypt-android:2.5.2")
|
||||
|
||||
// Data serialization (JSON, protobuf)
|
||||
val kotlinSerializationVersion = "1.3.0-RC"
|
||||
val kotlinSerializationVersion = "1.3.0"
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinSerializationVersion")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-protobuf:$kotlinSerializationVersion")
|
||||
|
||||
@ -225,13 +226,15 @@ dependencies {
|
||||
implementation("com.github.gpanther:java-nat-sort:natural-comparator-1.1")
|
||||
|
||||
// UI libraries
|
||||
implementation("com.google.android.material:material:1.5.0-alpha03")
|
||||
implementation("com.google.android.material:material:1.5.0-alpha04")
|
||||
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.nightlynexus.viewstatepageradapter:viewstatepageradapter:1.1.0")
|
||||
implementation("com.github.chrisbanes:PhotoView:2.3.0")
|
||||
implementation("com.github.tachiyomiorg:DirectionalViewPager:1.0.0")
|
||||
implementation("com.github.tachiyomiorg:DirectionalViewPager:1.0.0") {
|
||||
exclude(group = "androidx.viewpager", module = "viewpager")
|
||||
}
|
||||
implementation("dev.chrisbanes.insetter:insetter:0.6.0")
|
||||
|
||||
// Conductor
|
||||
@ -258,6 +261,11 @@ dependencies {
|
||||
// Licenses
|
||||
implementation("com.mikepenz:aboutlibraries-core:${BuildPluginsVersion.ABOUTLIB_PLUGIN}")
|
||||
|
||||
// Shizuku
|
||||
val shizukuVersion = "12.0.0"
|
||||
implementation("dev.rikka.shizuku:api:$shizukuVersion")
|
||||
implementation("dev.rikka.shizuku:provider:$shizukuVersion")
|
||||
|
||||
// Tests
|
||||
testImplementation("junit:junit:4.13.2")
|
||||
testImplementation("org.assertj:assertj-core:3.16.1")
|
||||
|
@ -18,6 +18,7 @@
|
||||
<!-- For managing extensions -->
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
|
||||
<uses-permission android:name="android.permission.UPDATE_PACKAGES_WITHOUT_USER_ACTION" />
|
||||
<!-- To view extension packages in API 30+ -->
|
||||
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
|
||||
|
||||
@ -188,6 +189,9 @@
|
||||
android:name=".data.backup.BackupRestoreService"
|
||||
android:exported="false" />
|
||||
|
||||
<service android:name=".extension.util.ExtensionInstallService"
|
||||
android:exported="false" />
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.provider"
|
||||
@ -198,6 +202,14 @@
|
||||
android:resource="@xml/provider_paths" />
|
||||
</provider>
|
||||
|
||||
<provider
|
||||
android:name="rikka.shizuku.ShizukuProvider"
|
||||
android:authorities="${applicationId}.shizuku"
|
||||
android:multiprocess="false"
|
||||
android:enabled="true"
|
||||
android:exported="true"
|
||||
android:permission="android.permission.INTERACT_ACROSS_USERS_FULL" />
|
||||
|
||||
<meta-data android:name="android.webkit.WebView.EnableSafeBrowsing"
|
||||
android:value="false" />
|
||||
<meta-data android:name="android.webkit.WebView.MetricsOptOut"
|
||||
|
@ -0,0 +1,100 @@
|
||||
package com.google.android.material.appbar
|
||||
|
||||
import android.animation.ValueAnimator
|
||||
import android.view.View
|
||||
import android.view.animation.DecelerateInterpolator
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import androidx.core.animation.doOnEnd
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.marginTop
|
||||
import eu.kanade.tachiyomi.util.system.animatorDurationScale
|
||||
import eu.kanade.tachiyomi.util.view.findChild
|
||||
import eu.kanade.tachiyomi.widget.ElevationAppBarLayout
|
||||
import kotlin.math.roundToLong
|
||||
|
||||
/**
|
||||
* Hide toolbar on scroll behavior for [AppBarLayout].
|
||||
*
|
||||
* Inside this package to access some package-private methods.
|
||||
*/
|
||||
class HideToolbarOnScrollBehavior : AppBarLayout.Behavior() {
|
||||
|
||||
@ViewCompat.NestedScrollType
|
||||
private var lastStartedType: Int = 0
|
||||
|
||||
private var offsetAnimator: ValueAnimator? = null
|
||||
|
||||
private var toolbarHeight: Int = 0
|
||||
|
||||
override fun onStartNestedScroll(
|
||||
parent: CoordinatorLayout,
|
||||
child: AppBarLayout,
|
||||
directTargetChild: View,
|
||||
target: View,
|
||||
nestedScrollAxes: Int,
|
||||
type: Int
|
||||
): Boolean {
|
||||
lastStartedType = type
|
||||
offsetAnimator?.cancel()
|
||||
return super.onStartNestedScroll(parent, child, directTargetChild, target, nestedScrollAxes, type)
|
||||
}
|
||||
|
||||
override fun onStopNestedScroll(
|
||||
parent: CoordinatorLayout,
|
||||
layout: AppBarLayout,
|
||||
target: View,
|
||||
type: Int
|
||||
) {
|
||||
super.onStopNestedScroll(parent, layout, target, type)
|
||||
if (toolbarHeight == 0) {
|
||||
toolbarHeight = layout.findChild<Toolbar>()?.height ?: 0
|
||||
}
|
||||
if (lastStartedType == ViewCompat.TYPE_TOUCH || type == ViewCompat.TYPE_NON_TOUCH) {
|
||||
animateToolbarVisibility(
|
||||
parent,
|
||||
layout,
|
||||
getTopBottomOffsetForScrollingSibling(layout) > -toolbarHeight / 2
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFlingFinished(parent: CoordinatorLayout, layout: AppBarLayout) {
|
||||
super.onFlingFinished(parent, layout)
|
||||
animateToolbarVisibility(
|
||||
parent,
|
||||
layout,
|
||||
getTopBottomOffsetForScrollingSibling(layout) > -toolbarHeight / 2
|
||||
)
|
||||
}
|
||||
|
||||
private fun getTopBottomOffsetForScrollingSibling(abl: AppBarLayout): Int {
|
||||
return topBottomOffsetForScrollingSibling - abl.marginTop
|
||||
}
|
||||
|
||||
private fun animateToolbarVisibility(
|
||||
coordinatorLayout: CoordinatorLayout,
|
||||
child: AppBarLayout,
|
||||
isVisible: Boolean
|
||||
) {
|
||||
val current = getTopBottomOffsetForScrollingSibling(child)
|
||||
val target = if (isVisible) 0 else -toolbarHeight
|
||||
if (current == target) return
|
||||
|
||||
offsetAnimator?.cancel()
|
||||
offsetAnimator = ValueAnimator().apply {
|
||||
interpolator = DecelerateInterpolator()
|
||||
duration = (150 * child.context.animatorDurationScale).roundToLong()
|
||||
addUpdateListener {
|
||||
setHeaderTopBottomOffset(coordinatorLayout, child, it.animatedValue as Int)
|
||||
}
|
||||
doOnEnd {
|
||||
if ((child as? ElevationAppBarLayout)?.isTransparentWhenNotLifted == true) {
|
||||
child.isLifted = !isVisible
|
||||
}
|
||||
}
|
||||
setIntValues(current, target)
|
||||
start()
|
||||
}
|
||||
}
|
||||
}
|
@ -12,9 +12,8 @@ import android.webkit.WebView
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleObserver
|
||||
import androidx.lifecycle.OnLifecycleEvent
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.ProcessLifecycleOwner
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import coil.ImageLoader
|
||||
@ -45,14 +44,14 @@ import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.security.Security
|
||||
|
||||
open class App : Application(), LifecycleObserver, ImageLoaderFactory {
|
||||
open class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory {
|
||||
|
||||
private val preferences: PreferencesHelper by injectLazy()
|
||||
|
||||
private val disableIncognitoReceiver = DisableIncognitoReceiver()
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
super<Application>.onCreate()
|
||||
if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree())
|
||||
|
||||
// TLS 1.3 support for Android < 10
|
||||
@ -131,9 +130,7 @@ open class App : Application(), LifecycleObserver, ImageLoaderFactory {
|
||||
}.build()
|
||||
}
|
||||
|
||||
@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
|
||||
@Suppress("unused")
|
||||
fun onAppBackgrounded() {
|
||||
override fun onStop(owner: LifecycleOwner) {
|
||||
if (!AuthenticatorUtil.isAuthenticating && preferences.lockAppAfter().get() >= 0) {
|
||||
SecureActivityDelegate.locked = true
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
package eu.kanade.tachiyomi.annotations
|
||||
|
||||
// TODO: remove this when no longer used in extensions
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
@Target(AnnotationTarget.CLASS)
|
||||
annotation class Nsfw
|
||||
|
@ -36,7 +36,8 @@ interface Manga : SManga {
|
||||
}
|
||||
|
||||
fun getGenres(): List<String>? {
|
||||
return genre?.split(", ")?.map { it.trim() }
|
||||
if (genre.isNullOrBlank()) return null
|
||||
return genre?.split(", ")?.map { it.trim() }?.filterNot { it.isBlank() }?.distinct()
|
||||
}
|
||||
|
||||
private fun setChapterFlags(flag: Int, mask: Int) {
|
||||
|
@ -15,11 +15,11 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.util.lang.plusAssign
|
||||
import eu.kanade.tachiyomi.util.lang.withUIContext
|
||||
import eu.kanade.tachiyomi.util.system.acquireWakeLock
|
||||
import eu.kanade.tachiyomi.util.system.isConnectedToWifi
|
||||
import eu.kanade.tachiyomi.util.system.isOnline
|
||||
import eu.kanade.tachiyomi.util.system.isServiceRunning
|
||||
import eu.kanade.tachiyomi.util.system.notification
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import eu.kanade.tachiyomi.util.system.wifiManager
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
@ -29,6 +29,7 @@ import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import ru.beryukhov.reactivenetwork.ReactiveNetwork
|
||||
import rx.subscriptions.CompositeSubscription
|
||||
import timber.log.Timber
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
/**
|
||||
@ -140,8 +141,9 @@ class DownloadService : Service() {
|
||||
onNetworkStateChanged()
|
||||
}
|
||||
}
|
||||
.catch {
|
||||
.catch { error ->
|
||||
withUIContext {
|
||||
Timber.e(error)
|
||||
toast(R.string.download_queue_error)
|
||||
stopSelf()
|
||||
}
|
||||
@ -154,7 +156,7 @@ class DownloadService : Service() {
|
||||
*/
|
||||
private fun onNetworkStateChanged() {
|
||||
if (isOnline()) {
|
||||
if (preferences.downloadOnlyOverWifi() && !wifiManager.isWifiEnabled) {
|
||||
if (preferences.downloadOnlyOverWifi() && !isConnectedToWifi()) {
|
||||
stopDownloads(R.string.download_notifier_text_only_wifi)
|
||||
} else {
|
||||
val started = downloadManager.startDownloads()
|
||||
|
@ -51,7 +51,7 @@ class LibraryUpdateNotifier(private val context: Context) {
|
||||
* Cached progress notification to avoid creating a lot.
|
||||
*/
|
||||
val progressNotificationBuilder by lazy {
|
||||
context.notificationBuilder(Notifications.CHANNEL_LIBRARY) {
|
||||
context.notificationBuilder(Notifications.CHANNEL_LIBRARY_PROGRESS) {
|
||||
setContentTitle(context.getString(R.string.app_name))
|
||||
setSmallIcon(R.drawable.ic_refresh_24dp)
|
||||
setLargeIcon(notificationBitmap)
|
||||
@ -101,7 +101,7 @@ class LibraryUpdateNotifier(private val context: Context) {
|
||||
|
||||
context.notificationManager.notify(
|
||||
Notifications.ID_LIBRARY_ERROR,
|
||||
context.notificationBuilder(Notifications.CHANNEL_LIBRARY) {
|
||||
context.notificationBuilder(Notifications.CHANNEL_LIBRARY_ERROR) {
|
||||
setContentTitle(context.resources.getQuantityString(R.plurals.notification_update_error, errors.size, errors.size))
|
||||
setStyle(
|
||||
NotificationCompat.BigTextStyle().bigText(
|
||||
|
@ -350,7 +350,7 @@ class LibraryUpdateService(
|
||||
}
|
||||
}
|
||||
|
||||
if (preferences.showLibraryUpdateErrors() && failedUpdates.isNotEmpty()) {
|
||||
if (failedUpdates.isNotEmpty()) {
|
||||
val errorFile = writeErrorFile(failedUpdates)
|
||||
notifier.showUpdateErrorNotification(
|
||||
failedUpdates.map { it.first.title },
|
||||
|
@ -24,8 +24,10 @@ object Notifications {
|
||||
/**
|
||||
* Notification channel and ids used by the library updater.
|
||||
*/
|
||||
const val CHANNEL_LIBRARY = "library_channel"
|
||||
private const val GROUP_LIBRARY = "group_library"
|
||||
const val CHANNEL_LIBRARY_PROGRESS = "library_progress_channel"
|
||||
const val ID_LIBRARY_PROGRESS = -101
|
||||
const val CHANNEL_LIBRARY_ERROR = "library_errors_channel"
|
||||
const val ID_LIBRARY_ERROR = -102
|
||||
|
||||
/**
|
||||
@ -51,6 +53,7 @@ object Notifications {
|
||||
*/
|
||||
const val CHANNEL_UPDATES_TO_EXTS = "updates_ext_channel"
|
||||
const val ID_UPDATES_TO_EXTS = -401
|
||||
const val ID_EXTENSION_INSTALLER = -402
|
||||
|
||||
/**
|
||||
* Notification channel and ids used by the backup/restore system.
|
||||
@ -77,7 +80,8 @@ object Notifications {
|
||||
|
||||
private val deprecatedChannels = listOf(
|
||||
"downloader_channel",
|
||||
"backup_restore_complete_channel"
|
||||
"backup_restore_complete_channel",
|
||||
"library_channel",
|
||||
)
|
||||
|
||||
/**
|
||||
@ -89,64 +93,75 @@ object Notifications {
|
||||
fun createChannels(context: Context) {
|
||||
val notificationService = NotificationManagerCompat.from(context)
|
||||
|
||||
val channelGroupList = listOf(
|
||||
buildNotificationChannelGroup(GROUP_BACKUP_RESTORE) {
|
||||
setName(context.getString(R.string.group_backup_restore))
|
||||
},
|
||||
buildNotificationChannelGroup(GROUP_DOWNLOADER) {
|
||||
setName(context.getString(R.string.group_downloader))
|
||||
}
|
||||
notificationService.createNotificationChannelGroupsCompat(
|
||||
listOf(
|
||||
buildNotificationChannelGroup(GROUP_BACKUP_RESTORE) {
|
||||
setName(context.getString(R.string.label_backup))
|
||||
},
|
||||
buildNotificationChannelGroup(GROUP_DOWNLOADER) {
|
||||
setName(context.getString(R.string.download_notifier_downloader_title))
|
||||
},
|
||||
buildNotificationChannelGroup(GROUP_LIBRARY) {
|
||||
setName(context.getString(R.string.label_library))
|
||||
},
|
||||
)
|
||||
)
|
||||
notificationService.createNotificationChannelGroupsCompat(channelGroupList)
|
||||
|
||||
val channelList = listOf(
|
||||
buildNotificationChannel(CHANNEL_COMMON, IMPORTANCE_LOW) {
|
||||
setName(context.getString(R.string.channel_common))
|
||||
},
|
||||
buildNotificationChannel(CHANNEL_LIBRARY, IMPORTANCE_LOW) {
|
||||
setName(context.getString(R.string.channel_library))
|
||||
setShowBadge(false)
|
||||
},
|
||||
buildNotificationChannel(CHANNEL_DOWNLOADER_PROGRESS, IMPORTANCE_LOW) {
|
||||
setName(context.getString(R.string.channel_progress))
|
||||
setGroup(GROUP_DOWNLOADER)
|
||||
setShowBadge(false)
|
||||
},
|
||||
buildNotificationChannel(CHANNEL_DOWNLOADER_COMPLETE, IMPORTANCE_LOW) {
|
||||
setName(context.getString(R.string.channel_complete))
|
||||
setGroup(GROUP_DOWNLOADER)
|
||||
setShowBadge(false)
|
||||
},
|
||||
buildNotificationChannel(CHANNEL_DOWNLOADER_ERROR, IMPORTANCE_LOW) {
|
||||
setName(context.getString(R.string.channel_errors))
|
||||
setGroup(GROUP_DOWNLOADER)
|
||||
setShowBadge(false)
|
||||
},
|
||||
buildNotificationChannel(CHANNEL_NEW_CHAPTERS, IMPORTANCE_DEFAULT) {
|
||||
setName(context.getString(R.string.channel_new_chapters))
|
||||
},
|
||||
buildNotificationChannel(CHANNEL_UPDATES_TO_EXTS, IMPORTANCE_DEFAULT) {
|
||||
setName(context.getString(R.string.channel_ext_updates))
|
||||
},
|
||||
buildNotificationChannel(CHANNEL_BACKUP_RESTORE_PROGRESS, IMPORTANCE_LOW) {
|
||||
setName(context.getString(R.string.channel_progress))
|
||||
setGroup(GROUP_BACKUP_RESTORE)
|
||||
setShowBadge(false)
|
||||
},
|
||||
buildNotificationChannel(CHANNEL_BACKUP_RESTORE_COMPLETE, IMPORTANCE_HIGH) {
|
||||
setName(context.getString(R.string.channel_complete))
|
||||
setGroup(GROUP_BACKUP_RESTORE)
|
||||
setShowBadge(false)
|
||||
setSound(null, null)
|
||||
},
|
||||
buildNotificationChannel(CHANNEL_CRASH_LOGS, IMPORTANCE_HIGH) {
|
||||
setName(context.getString(R.string.channel_crash_logs))
|
||||
},
|
||||
buildNotificationChannel(CHANNEL_INCOGNITO_MODE, IMPORTANCE_LOW) {
|
||||
setName(context.getString(R.string.pref_incognito_mode))
|
||||
},
|
||||
notificationService.createNotificationChannelsCompat(
|
||||
listOf(
|
||||
buildNotificationChannel(CHANNEL_COMMON, IMPORTANCE_LOW) {
|
||||
setName(context.getString(R.string.channel_common))
|
||||
},
|
||||
buildNotificationChannel(CHANNEL_LIBRARY_PROGRESS, IMPORTANCE_LOW) {
|
||||
setName(context.getString(R.string.channel_progress))
|
||||
setGroup(GROUP_LIBRARY)
|
||||
setShowBadge(false)
|
||||
},
|
||||
buildNotificationChannel(CHANNEL_LIBRARY_ERROR, IMPORTANCE_LOW) {
|
||||
setName(context.getString(R.string.channel_errors))
|
||||
setGroup(GROUP_LIBRARY)
|
||||
setShowBadge(false)
|
||||
},
|
||||
buildNotificationChannel(CHANNEL_NEW_CHAPTERS, IMPORTANCE_DEFAULT) {
|
||||
setName(context.getString(R.string.channel_new_chapters))
|
||||
},
|
||||
buildNotificationChannel(CHANNEL_DOWNLOADER_PROGRESS, IMPORTANCE_LOW) {
|
||||
setName(context.getString(R.string.channel_progress))
|
||||
setGroup(GROUP_DOWNLOADER)
|
||||
setShowBadge(false)
|
||||
},
|
||||
buildNotificationChannel(CHANNEL_DOWNLOADER_COMPLETE, IMPORTANCE_LOW) {
|
||||
setName(context.getString(R.string.channel_complete))
|
||||
setGroup(GROUP_DOWNLOADER)
|
||||
setShowBadge(false)
|
||||
},
|
||||
buildNotificationChannel(CHANNEL_DOWNLOADER_ERROR, IMPORTANCE_LOW) {
|
||||
setName(context.getString(R.string.channel_errors))
|
||||
setGroup(GROUP_DOWNLOADER)
|
||||
setShowBadge(false)
|
||||
},
|
||||
buildNotificationChannel(CHANNEL_BACKUP_RESTORE_PROGRESS, IMPORTANCE_LOW) {
|
||||
setName(context.getString(R.string.channel_progress))
|
||||
setGroup(GROUP_BACKUP_RESTORE)
|
||||
setShowBadge(false)
|
||||
},
|
||||
buildNotificationChannel(CHANNEL_BACKUP_RESTORE_COMPLETE, IMPORTANCE_HIGH) {
|
||||
setName(context.getString(R.string.channel_complete))
|
||||
setGroup(GROUP_BACKUP_RESTORE)
|
||||
setShowBadge(false)
|
||||
setSound(null, null)
|
||||
},
|
||||
buildNotificationChannel(CHANNEL_CRASH_LOGS, IMPORTANCE_HIGH) {
|
||||
setName(context.getString(R.string.channel_crash_logs))
|
||||
},
|
||||
buildNotificationChannel(CHANNEL_INCOGNITO_MODE, IMPORTANCE_LOW) {
|
||||
setName(context.getString(R.string.pref_incognito_mode))
|
||||
},
|
||||
buildNotificationChannel(CHANNEL_UPDATES_TO_EXTS, IMPORTANCE_DEFAULT) {
|
||||
setName(context.getString(R.string.channel_ext_updates))
|
||||
},
|
||||
)
|
||||
)
|
||||
notificationService.createNotificationChannelsCompat(channelList)
|
||||
|
||||
// Delete old notification channels
|
||||
deprecatedChannels.forEach(notificationService::deleteNotificationChannel)
|
||||
|
@ -151,11 +151,12 @@ object PreferenceKeys {
|
||||
const val librarySortingMode = "library_sorting_mode"
|
||||
const val librarySortingDirection = "library_sorting_ascending"
|
||||
|
||||
const val migrationSortingMode = "pref_migration_sorting"
|
||||
const val migrationSortingDirection = "pref_migration_direction"
|
||||
|
||||
const val automaticExtUpdates = "automatic_ext_updates"
|
||||
|
||||
const val showNsfwSource = "show_nsfw_source"
|
||||
const val showNsfwExtension = "show_nsfw_extension"
|
||||
const val labelNsfwExtension = "label_nsfw_extension"
|
||||
|
||||
const val startScreen = "start_screen"
|
||||
|
||||
@ -173,8 +174,6 @@ object PreferenceKeys {
|
||||
|
||||
const val autoUpdateTrackers = "auto_update_trackers"
|
||||
|
||||
const val showLibraryUpdateErrors = "show_library_update_errors"
|
||||
|
||||
const val downloadNew = "download_new"
|
||||
|
||||
const val downloadNewCategories = "download_new_categories"
|
||||
@ -226,6 +225,8 @@ object PreferenceKeys {
|
||||
|
||||
const val tabletUiMode = "tablet_ui_mode"
|
||||
|
||||
const val extensionInstaller = "extension_installer"
|
||||
|
||||
fun trackUsername(syncId: Int) = "pref_mangasync_username_$syncId"
|
||||
|
||||
fun trackPassword(syncId: Int) = "pref_mangasync_password_$syncId"
|
||||
|
@ -57,4 +57,10 @@ object PreferenceValues {
|
||||
LANDSCAPE,
|
||||
NEVER,
|
||||
}
|
||||
|
||||
enum class ExtensionInstaller {
|
||||
LEGACY,
|
||||
PACKAGEINSTALLER,
|
||||
SHIZUKU
|
||||
}
|
||||
}
|
||||
|
@ -12,11 +12,13 @@ import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.preference.PreferenceValues.ThemeMode.system
|
||||
import eu.kanade.tachiyomi.data.track.TrackService
|
||||
import eu.kanade.tachiyomi.data.track.anilist.Anilist
|
||||
import eu.kanade.tachiyomi.ui.browse.migration.sources.MigrationSourcesController
|
||||
import eu.kanade.tachiyomi.ui.library.setting.DisplayModeSetting
|
||||
import eu.kanade.tachiyomi.ui.library.setting.SortDirectionSetting
|
||||
import eu.kanade.tachiyomi.ui.library.setting.SortModeSetting
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
|
||||
import eu.kanade.tachiyomi.util.system.MiuiUtil
|
||||
import eu.kanade.tachiyomi.util.system.isTablet
|
||||
import eu.kanade.tachiyomi.widget.ExtendedNavigationView
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
@ -86,8 +88,6 @@ class PreferencesHelper(val context: Context) {
|
||||
|
||||
fun autoUpdateTrackers() = prefs.getBoolean(Keys.autoUpdateTrackers, false)
|
||||
|
||||
fun showLibraryUpdateErrors() = prefs.getBoolean(Keys.showLibraryUpdateErrors, true)
|
||||
|
||||
fun themeMode() = flowPrefs.getEnum(Keys.themeMode, system)
|
||||
|
||||
fun appTheme() = flowPrefs.getEnum(Keys.appTheme, Values.AppTheme.DEFAULT)
|
||||
@ -190,7 +190,7 @@ class PreferencesHelper(val context: Context) {
|
||||
|
||||
fun sourceDisplayMode() = flowPrefs.getEnum(Keys.sourceDisplayMode, DisplayModeSetting.COMPACT_GRID)
|
||||
|
||||
fun enabledLanguages() = flowPrefs.getStringSet(Keys.enabledLanguages, setOf("en", Locale.getDefault().language))
|
||||
fun enabledLanguages() = flowPrefs.getStringSet(Keys.enabledLanguages, setOf("all", "en", Locale.getDefault().language))
|
||||
|
||||
fun trackUsername(sync: TrackService) = prefs.getString(Keys.trackUsername(sync.id), "")
|
||||
|
||||
@ -268,11 +268,12 @@ class PreferencesHelper(val context: Context) {
|
||||
fun librarySortingMode() = flowPrefs.getEnum(Keys.librarySortingMode, SortModeSetting.ALPHABETICAL)
|
||||
fun librarySortingAscending() = flowPrefs.getEnum(Keys.librarySortingDirection, SortDirectionSetting.ASCENDING)
|
||||
|
||||
fun migrationSortingMode() = flowPrefs.getEnum(Keys.migrationSortingMode, MigrationSourcesController.SortSetting.ALPHABETICAL)
|
||||
fun migrationSortingDirection() = flowPrefs.getEnum(Keys.migrationSortingDirection, MigrationSourcesController.DirectionSetting.ASCENDING)
|
||||
|
||||
fun automaticExtUpdates() = flowPrefs.getBoolean(Keys.automaticExtUpdates, true)
|
||||
|
||||
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)
|
||||
|
||||
@ -325,6 +326,11 @@ class PreferencesHelper(val context: Context) {
|
||||
if (context.applicationContext.isTablet()) Values.TabletUiMode.ALWAYS else Values.TabletUiMode.NEVER
|
||||
)
|
||||
|
||||
fun extensionInstaller() = flowPrefs.getEnum(
|
||||
Keys.extensionInstaller,
|
||||
if (MiuiUtil.isMiui()) Values.ExtensionInstaller.LEGACY else Values.ExtensionInstaller.PACKAGEINSTALLER
|
||||
)
|
||||
|
||||
fun setChapterSettingsDefault(manga: Manga) {
|
||||
prefs.edit {
|
||||
putInt(Keys.defaultChapterFilterByRead, manga.readFilter)
|
||||
|
@ -47,7 +47,7 @@ class KomgaApi(private val client: OkHttpClient) {
|
||||
track.apply {
|
||||
cover_url = "$url/thumbnail"
|
||||
tracking_url = url
|
||||
total_chapters = progress.booksCount
|
||||
total_chapters = progress.maxNumberSort.toInt()
|
||||
status = when (progress.booksCount) {
|
||||
progress.booksUnreadCount -> Komga.UNREAD
|
||||
progress.booksReadCount -> Komga.COMPLETED
|
||||
|
@ -91,7 +91,8 @@ data class ReadProgressDto(
|
||||
booksReadCount,
|
||||
booksUnreadCount,
|
||||
booksInProgressCount,
|
||||
lastReadContinuousIndex.toFloat()
|
||||
lastReadContinuousIndex.toFloat(),
|
||||
booksCount.toFloat(),
|
||||
)
|
||||
}
|
||||
|
||||
@ -102,4 +103,5 @@ data class ReadProgressV2Dto(
|
||||
val booksUnreadCount: Int,
|
||||
val booksInProgressCount: Int,
|
||||
val lastReadContinuousNumberSort: Float,
|
||||
val maxNumberSort: Float,
|
||||
)
|
||||
|
@ -227,14 +227,26 @@ class ExtensionManager(
|
||||
return installExtension(availableExt)
|
||||
}
|
||||
|
||||
fun cancelInstallUpdateExtension(extension: Extension) {
|
||||
installer.cancelInstall(extension.pkgName)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the result of the installation of an extension.
|
||||
* Sets to "installing" status of an extension installation.
|
||||
*
|
||||
* @param downloadId The id of the download.
|
||||
* @param result Whether the extension was installed or not.
|
||||
*/
|
||||
fun setInstalling(downloadId: Long) {
|
||||
installer.updateInstallStep(downloadId, InstallStep.Installing)
|
||||
}
|
||||
|
||||
fun setInstallationResult(downloadId: Long, result: Boolean) {
|
||||
installer.setInstallationResult(downloadId, result)
|
||||
val step = if (result) InstallStep.Installed else InstallStep.Error
|
||||
installer.updateInstallStep(downloadId, step)
|
||||
}
|
||||
|
||||
fun updateInstallStep(downloadId: Long, step: InstallStep) {
|
||||
installer.updateInstallStep(downloadId, step)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -10,11 +10,7 @@ import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import eu.kanade.tachiyomi.network.await
|
||||
import eu.kanade.tachiyomi.network.parseAs
|
||||
import eu.kanade.tachiyomi.util.lang.withIOContext
|
||||
import kotlinx.serialization.json.JsonArray
|
||||
import kotlinx.serialization.json.int
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import kotlinx.serialization.json.long
|
||||
import kotlinx.serialization.Serializable
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.util.Date
|
||||
|
||||
@ -28,8 +24,8 @@ internal class ExtensionGithubApi {
|
||||
networkService.client
|
||||
.newCall(GET("${REPO_URL_PREFIX}index.min.json"))
|
||||
.await()
|
||||
.parseAs<JsonArray>()
|
||||
.let { parseResponse(it) }
|
||||
.parseAs<List<ExtensionJsonObject>>()
|
||||
.toExtensions()
|
||||
}
|
||||
}
|
||||
|
||||
@ -56,24 +52,23 @@ internal class ExtensionGithubApi {
|
||||
return extensionsWithUpdate
|
||||
}
|
||||
|
||||
private fun parseResponse(json: JsonArray): List<Extension.Available> {
|
||||
return json
|
||||
.filter { element ->
|
||||
val versionName = element.jsonObject["version"]!!.jsonPrimitive.content
|
||||
val libVersion = versionName.substringBeforeLast('.').toDouble()
|
||||
private fun List<ExtensionJsonObject>.toExtensions(): List<Extension.Available> {
|
||||
return this
|
||||
.filter {
|
||||
val libVersion = it.version.substringBeforeLast('.').toDouble()
|
||||
libVersion >= ExtensionLoader.LIB_VERSION_MIN && libVersion <= ExtensionLoader.LIB_VERSION_MAX
|
||||
}
|
||||
.map { element ->
|
||||
val name = element.jsonObject["name"]!!.jsonPrimitive.content.substringAfter("Tachiyomi: ")
|
||||
val pkgName = element.jsonObject["pkg"]!!.jsonPrimitive.content
|
||||
val apkName = element.jsonObject["apk"]!!.jsonPrimitive.content
|
||||
val versionName = element.jsonObject["version"]!!.jsonPrimitive.content
|
||||
val versionCode = element.jsonObject["code"]!!.jsonPrimitive.long
|
||||
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")}"
|
||||
|
||||
Extension.Available(name, pkgName, versionName, versionCode, lang, nsfw, apkName, icon)
|
||||
.map {
|
||||
Extension.Available(
|
||||
name = it.name.substringAfter("Tachiyomi: "),
|
||||
pkgName = it.pkg,
|
||||
versionName = it.version,
|
||||
versionCode = it.code,
|
||||
lang = it.lang,
|
||||
isNsfw = it.nsfw == 1,
|
||||
apkName = it.apk,
|
||||
iconUrl = "${REPO_URL_PREFIX}icon/${it.apk.replace(".apk", ".png")}"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -83,3 +78,14 @@ internal class ExtensionGithubApi {
|
||||
}
|
||||
|
||||
private const val REPO_URL_PREFIX = "https://raw.githubusercontent.com/tachiyomiorg/tachiyomi-extensions/repo/"
|
||||
|
||||
@Serializable
|
||||
private data class ExtensionJsonObject(
|
||||
val name: String,
|
||||
val pkg: String,
|
||||
val apk: String,
|
||||
val version: String,
|
||||
val code: Long,
|
||||
val lang: String,
|
||||
val nsfw: Int,
|
||||
)
|
||||
|
@ -0,0 +1,170 @@
|
||||
package eu.kanade.tachiyomi.extension.installer
|
||||
|
||||
import android.app.Service
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.net.Uri
|
||||
import androidx.annotation.CallSuper
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||
import eu.kanade.tachiyomi.extension.ExtensionManager
|
||||
import eu.kanade.tachiyomi.extension.model.InstallStep
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.util.Collections
|
||||
import java.util.concurrent.atomic.AtomicReference
|
||||
|
||||
/**
|
||||
* Base implementation class for extension installer. To be used inside a foreground [Service].
|
||||
*/
|
||||
abstract class Installer(private val service: Service) {
|
||||
|
||||
private val extensionManager: ExtensionManager by injectLazy()
|
||||
|
||||
private var waitingInstall = AtomicReference<Entry>(null)
|
||||
private val queue = Collections.synchronizedList(mutableListOf<Entry>())
|
||||
|
||||
private val cancelReceiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
val downloadId = intent.getLongExtra(EXTRA_DOWNLOAD_ID, -1).takeIf { it >= 0 } ?: return
|
||||
cancelQueue(downloadId)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Installer readiness. If false, queue check will not run.
|
||||
*
|
||||
* @see checkQueue
|
||||
*/
|
||||
abstract var ready: Boolean
|
||||
|
||||
/**
|
||||
* Add an item to install queue.
|
||||
*
|
||||
* @param downloadId Download ID as known by [ExtensionManager]
|
||||
* @param uri Uri of APK to install
|
||||
*/
|
||||
fun addToQueue(downloadId: Long, uri: Uri) {
|
||||
queue.add(Entry(downloadId, uri))
|
||||
checkQueue()
|
||||
}
|
||||
|
||||
/**
|
||||
* Proceeds to install the APK of this entry inside this method. Call [continueQueue]
|
||||
* when the install process for this entry is finished to continue the queue.
|
||||
*
|
||||
* @param entry The [Entry] of item to process
|
||||
* @see continueQueue
|
||||
*/
|
||||
@CallSuper
|
||||
open fun processEntry(entry: Entry) {
|
||||
extensionManager.setInstalling(entry.downloadId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Called before queue continues. Override this to handle when the removed entry is
|
||||
* currently being processed.
|
||||
*
|
||||
* @return true if this entry can be removed from queue.
|
||||
*/
|
||||
open fun cancelEntry(entry: Entry): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells the queue to continue processing the next entry and updates the install step
|
||||
* of the completed entry ([waitingInstall]) to [ExtensionManager].
|
||||
*
|
||||
* @param resultStep new install step for the processed entry.
|
||||
* @see waitingInstall
|
||||
*/
|
||||
fun continueQueue(resultStep: InstallStep) {
|
||||
val completedEntry = waitingInstall.getAndSet(null)
|
||||
if (completedEntry != null) {
|
||||
extensionManager.updateInstallStep(completedEntry.downloadId, resultStep)
|
||||
checkQueue()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the queue. The provided service will be stopped if the queue is empty.
|
||||
* Will not be run when not ready.
|
||||
*
|
||||
* @see ready
|
||||
*/
|
||||
fun checkQueue() {
|
||||
if (!ready) {
|
||||
return
|
||||
}
|
||||
if (queue.isEmpty()) {
|
||||
service.stopSelf()
|
||||
return
|
||||
}
|
||||
val nextEntry = queue.first()
|
||||
if (waitingInstall.compareAndSet(null, nextEntry)) {
|
||||
queue.removeFirst()
|
||||
processEntry(nextEntry)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Call this method when the provided service is destroyed.
|
||||
*/
|
||||
@CallSuper
|
||||
open fun onDestroy() {
|
||||
LocalBroadcastManager.getInstance(service).unregisterReceiver(cancelReceiver)
|
||||
queue.forEach { extensionManager.updateInstallStep(it.downloadId, InstallStep.Error) }
|
||||
queue.clear()
|
||||
waitingInstall.set(null)
|
||||
}
|
||||
|
||||
protected fun getActiveEntry(): Entry? = waitingInstall.get()
|
||||
|
||||
/**
|
||||
* Cancels queue for the provided download ID if exists.
|
||||
*
|
||||
* @param downloadId Download ID as known by [ExtensionManager]
|
||||
*/
|
||||
private fun cancelQueue(downloadId: Long) {
|
||||
val waitingInstall = this.waitingInstall.get()
|
||||
val toCancel = queue.find { it.downloadId == downloadId } ?: waitingInstall ?: return
|
||||
if (cancelEntry(toCancel)) {
|
||||
queue.remove(toCancel)
|
||||
if (waitingInstall == toCancel) {
|
||||
// Currently processing removed entry, continue queue
|
||||
this.waitingInstall.set(null)
|
||||
checkQueue()
|
||||
}
|
||||
extensionManager.updateInstallStep(downloadId, InstallStep.Idle)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Install item to queue.
|
||||
*
|
||||
* @param downloadId Download ID as known by [ExtensionManager]
|
||||
* @param uri Uri of APK to install
|
||||
*/
|
||||
data class Entry(val downloadId: Long, val uri: Uri)
|
||||
|
||||
init {
|
||||
val filter = IntentFilter(ACTION_CANCEL_QUEUE)
|
||||
LocalBroadcastManager.getInstance(service).registerReceiver(cancelReceiver, filter)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val ACTION_CANCEL_QUEUE = "Installer.action.CANCEL_QUEUE"
|
||||
private const val EXTRA_DOWNLOAD_ID = "Installer.extra.DOWNLOAD_ID"
|
||||
|
||||
/**
|
||||
* Attempts to cancel the installation entry for the provided download ID.
|
||||
*
|
||||
* @param downloadId Download ID as known by [ExtensionManager]
|
||||
*/
|
||||
fun cancelInstallQueue(context: Context, downloadId: Long) {
|
||||
val intent = Intent(ACTION_CANCEL_QUEUE)
|
||||
intent.putExtra(EXTRA_DOWNLOAD_ID, downloadId)
|
||||
LocalBroadcastManager.getInstance(context).sendBroadcast(intent)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,105 @@
|
||||
package eu.kanade.tachiyomi.extension.installer
|
||||
|
||||
import android.app.PendingIntent
|
||||
import android.app.Service
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.content.pm.PackageInstaller
|
||||
import android.os.Build
|
||||
import eu.kanade.tachiyomi.extension.model.InstallStep
|
||||
import eu.kanade.tachiyomi.util.lang.use
|
||||
import eu.kanade.tachiyomi.util.system.getUriSize
|
||||
import timber.log.Timber
|
||||
|
||||
class PackageInstallerInstaller(private val service: Service) : Installer(service) {
|
||||
|
||||
private val packageInstaller = service.packageManager.packageInstaller
|
||||
|
||||
private val packageActionReceiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
when (intent.getIntExtra(PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_FAILURE)) {
|
||||
PackageInstaller.STATUS_PENDING_USER_ACTION -> {
|
||||
val userAction = intent.getParcelableExtra<Intent>(Intent.EXTRA_INTENT)
|
||||
if (userAction == null) {
|
||||
Timber.e("Fatal error for $intent")
|
||||
continueQueue(InstallStep.Error)
|
||||
return
|
||||
}
|
||||
userAction.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
service.startActivity(userAction)
|
||||
}
|
||||
PackageInstaller.STATUS_FAILURE_ABORTED -> {
|
||||
continueQueue(InstallStep.Idle)
|
||||
}
|
||||
PackageInstaller.STATUS_SUCCESS -> continueQueue(InstallStep.Installed)
|
||||
else -> continueQueue(InstallStep.Error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var activeSession: Pair<Entry, Int>? = null
|
||||
|
||||
// Always ready
|
||||
override var ready = true
|
||||
|
||||
override fun processEntry(entry: Entry) {
|
||||
super.processEntry(entry)
|
||||
activeSession = null
|
||||
try {
|
||||
val installParams = PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
installParams.setRequireUserAction(PackageInstaller.SessionParams.USER_ACTION_NOT_REQUIRED)
|
||||
}
|
||||
activeSession = entry to packageInstaller.createSession(installParams)
|
||||
val fileSize = service.getUriSize(entry.uri) ?: throw IllegalStateException()
|
||||
installParams.setSize(fileSize)
|
||||
|
||||
val inputStream = service.contentResolver.openInputStream(entry.uri) ?: throw IllegalStateException()
|
||||
val session = packageInstaller.openSession(activeSession!!.second)
|
||||
val outputStream = session.openWrite(entry.downloadId.toString(), 0, fileSize)
|
||||
session.use {
|
||||
arrayOf(inputStream, outputStream).use {
|
||||
inputStream.copyTo(outputStream)
|
||||
session.fsync(outputStream)
|
||||
}
|
||||
|
||||
val intentSender = PendingIntent.getBroadcast(
|
||||
service,
|
||||
activeSession!!.second,
|
||||
Intent(INSTALL_ACTION),
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) PendingIntent.FLAG_MUTABLE else 0
|
||||
).intentSender
|
||||
session.commit(intentSender)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "Failed to install extension ${entry.downloadId} ${entry.uri}")
|
||||
activeSession?.let { (_, sessionId) ->
|
||||
packageInstaller.abandonSession(sessionId)
|
||||
}
|
||||
continueQueue(InstallStep.Error)
|
||||
}
|
||||
}
|
||||
|
||||
override fun cancelEntry(entry: Entry): Boolean {
|
||||
activeSession?.let { (activeEntry, sessionId) ->
|
||||
if (activeEntry == entry) {
|
||||
packageInstaller.abandonSession(sessionId)
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
service.unregisterReceiver(packageActionReceiver)
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
init {
|
||||
service.registerReceiver(packageActionReceiver, IntentFilter(INSTALL_ACTION))
|
||||
}
|
||||
}
|
||||
|
||||
private const val INSTALL_ACTION = "PackageInstallerInstaller.INSTALL_ACTION"
|
@ -0,0 +1,127 @@
|
||||
package eu.kanade.tachiyomi.extension.installer
|
||||
|
||||
import android.app.Service
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.extension.model.InstallStep
|
||||
import eu.kanade.tachiyomi.util.system.getUriSize
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.launch
|
||||
import rikka.shizuku.Shizuku
|
||||
import timber.log.Timber
|
||||
import java.io.BufferedReader
|
||||
import java.io.InputStream
|
||||
|
||||
class ShizukuInstaller(private val service: Service) : Installer(service) {
|
||||
|
||||
private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||
|
||||
private val shizukuDeadListener = Shizuku.OnBinderDeadListener {
|
||||
Timber.e("Shizuku was killed prematurely")
|
||||
service.stopSelf()
|
||||
}
|
||||
|
||||
private val shizukuPermissionListener = object : Shizuku.OnRequestPermissionResultListener {
|
||||
override fun onRequestPermissionResult(requestCode: Int, grantResult: Int) {
|
||||
if (requestCode == SHIZUKU_PERMISSION_REQUEST_CODE) {
|
||||
if (grantResult == PackageManager.PERMISSION_GRANTED) {
|
||||
ready = true
|
||||
checkQueue()
|
||||
} else {
|
||||
service.stopSelf()
|
||||
}
|
||||
Shizuku.removeRequestPermissionResultListener(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override var ready = false
|
||||
|
||||
@Suppress("BlockingMethodInNonBlockingContext")
|
||||
override fun processEntry(entry: Entry) {
|
||||
super.processEntry(entry)
|
||||
ioScope.launch {
|
||||
var sessionId: String? = null
|
||||
try {
|
||||
val size = service.getUriSize(entry.uri) ?: throw IllegalStateException()
|
||||
service.contentResolver.openInputStream(entry.uri)!!.use {
|
||||
val createCommand = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
"pm install-create --user current -i ${service.packageName} -S $size"
|
||||
} else {
|
||||
"pm install-create -i ${service.packageName} -S $size"
|
||||
}
|
||||
val createResult = exec(createCommand)
|
||||
sessionId = SESSION_ID_REGEX.find(createResult.out)?.value
|
||||
?: throw RuntimeException("Failed to create install session")
|
||||
|
||||
val writeResult = exec("pm install-write -S $size $sessionId base -", it)
|
||||
if (writeResult.resultCode != 0) {
|
||||
throw RuntimeException("Failed to write APK to session $sessionId")
|
||||
}
|
||||
|
||||
val commitResult = exec("pm install-commit $sessionId")
|
||||
if (commitResult.resultCode != 0) {
|
||||
throw RuntimeException("Failed to commit install session $sessionId")
|
||||
}
|
||||
|
||||
continueQueue(InstallStep.Installed)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "Failed to install extension ${entry.downloadId} ${entry.uri}")
|
||||
if (sessionId != null) {
|
||||
exec("pm install-abandon $sessionId")
|
||||
}
|
||||
continueQueue(InstallStep.Error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Don't cancel if entry is already started installing
|
||||
override fun cancelEntry(entry: Entry): Boolean = getActiveEntry() != entry
|
||||
|
||||
override fun onDestroy() {
|
||||
Shizuku.removeBinderDeadListener(shizukuDeadListener)
|
||||
Shizuku.removeRequestPermissionResultListener(shizukuPermissionListener)
|
||||
ioScope.cancel()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
private fun exec(command: String, stdin: InputStream? = null): ShellResult {
|
||||
@Suppress("DEPRECATION")
|
||||
val process = Shizuku.newProcess(arrayOf("sh", "-c", command), null, null)
|
||||
if (stdin != null) {
|
||||
process.outputStream.use { stdin.copyTo(it) }
|
||||
}
|
||||
val output = process.inputStream.bufferedReader().use(BufferedReader::readText)
|
||||
val resultCode = process.waitFor()
|
||||
return ShellResult(resultCode, output)
|
||||
}
|
||||
|
||||
private data class ShellResult(val resultCode: Int, val out: String)
|
||||
|
||||
init {
|
||||
Shizuku.addBinderDeadListener(shizukuDeadListener)
|
||||
ready = if (Shizuku.pingBinder()) {
|
||||
if (Shizuku.checkSelfPermission() == PackageManager.PERMISSION_GRANTED) {
|
||||
true
|
||||
} else {
|
||||
Shizuku.addRequestPermissionResultListener(shizukuPermissionListener)
|
||||
Shizuku.requestPermission(SHIZUKU_PERMISSION_REQUEST_CODE)
|
||||
false
|
||||
}
|
||||
} else {
|
||||
Timber.e("Shizuku is not ready to use.")
|
||||
service.toast(R.string.ext_installer_shizuku_stopped)
|
||||
service.stopSelf()
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private const val SHIZUKU_PERMISSION_REQUEST_CODE = 14045
|
||||
private val SESSION_ID_REGEX = Regex("(?<=\\[).+?(?=])")
|
@ -1,9 +1,9 @@
|
||||
package eu.kanade.tachiyomi.extension.model
|
||||
|
||||
enum class InstallStep {
|
||||
Pending, Downloading, Installing, Installed, Error;
|
||||
Idle, Pending, Downloading, Installing, Installed, Error;
|
||||
|
||||
fun isCompleted(): Boolean {
|
||||
return this == Installed || this == Error
|
||||
return this == Installed || this == Error || this == Idle
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import eu.kanade.tachiyomi.extension.ExtensionManager
|
||||
import eu.kanade.tachiyomi.extension.model.InstallStep
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
@ -40,10 +41,13 @@ class ExtensionInstallActivity : Activity() {
|
||||
|
||||
private fun checkInstallationResult(resultCode: Int) {
|
||||
val downloadId = intent.extras!!.getLong(ExtensionInstaller.EXTRA_DOWNLOAD_ID)
|
||||
val success = resultCode == RESULT_OK
|
||||
|
||||
val extensionManager = Injekt.get<ExtensionManager>()
|
||||
extensionManager.setInstallationResult(downloadId, success)
|
||||
val newStep = when (resultCode) {
|
||||
RESULT_OK -> InstallStep.Installed
|
||||
RESULT_CANCELED -> InstallStep.Idle
|
||||
else -> InstallStep.Error
|
||||
}
|
||||
extensionManager.updateInstallStep(downloadId, newStep)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,82 @@
|
||||
package eu.kanade.tachiyomi.extension.util
|
||||
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.IBinder
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||
import eu.kanade.tachiyomi.data.preference.PreferenceValues
|
||||
import eu.kanade.tachiyomi.extension.installer.Installer
|
||||
import eu.kanade.tachiyomi.extension.installer.PackageInstallerInstaller
|
||||
import eu.kanade.tachiyomi.extension.installer.ShizukuInstaller
|
||||
import eu.kanade.tachiyomi.extension.util.ExtensionInstaller.Companion.EXTRA_DOWNLOAD_ID
|
||||
import eu.kanade.tachiyomi.util.system.notificationBuilder
|
||||
import timber.log.Timber
|
||||
|
||||
class ExtensionInstallService : Service() {
|
||||
|
||||
private var installer: Installer? = null
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
val notification = notificationBuilder(Notifications.CHANNEL_DOWNLOADER_PROGRESS) {
|
||||
setSmallIcon(R.drawable.ic_tachi)
|
||||
setAutoCancel(false)
|
||||
setOngoing(true)
|
||||
setShowWhen(false)
|
||||
setContentTitle(getString(R.string.ext_install_service_notif))
|
||||
setProgress(100, 100, true)
|
||||
}.build()
|
||||
startForeground(Notifications.ID_EXTENSION_INSTALLER, notification)
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
val uri = intent?.data
|
||||
val id = intent?.getLongExtra(EXTRA_DOWNLOAD_ID, -1)?.takeIf { it != -1L }
|
||||
val installerUsed = intent?.getSerializableExtra(EXTRA_INSTALLER) as? PreferenceValues.ExtensionInstaller
|
||||
if (uri == null || id == null || installerUsed == null) {
|
||||
stopSelf()
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
|
||||
if (installer == null) {
|
||||
installer = when (installerUsed) {
|
||||
PreferenceValues.ExtensionInstaller.PACKAGEINSTALLER -> PackageInstallerInstaller(this)
|
||||
PreferenceValues.ExtensionInstaller.SHIZUKU -> ShizukuInstaller(this)
|
||||
else -> {
|
||||
Timber.e("Not implemented for installer $installerUsed")
|
||||
stopSelf()
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
}
|
||||
}
|
||||
installer!!.addToQueue(id, uri)
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
installer?.onDestroy()
|
||||
installer = null
|
||||
}
|
||||
|
||||
override fun onBind(i: Intent?): IBinder? = null
|
||||
|
||||
companion object {
|
||||
private const val EXTRA_INSTALLER = "EXTRA_INSTALLER"
|
||||
|
||||
fun getIntent(
|
||||
context: Context,
|
||||
downloadId: Long,
|
||||
uri: Uri,
|
||||
installer: PreferenceValues.ExtensionInstaller
|
||||
): Intent {
|
||||
return Intent(context, ExtensionInstallService::class.java)
|
||||
.setDataAndType(uri, ExtensionInstaller.APK_MIME)
|
||||
.putExtra(EXTRA_DOWNLOAD_ID, downloadId)
|
||||
.putExtra(EXTRA_INSTALLER, installer)
|
||||
}
|
||||
}
|
||||
}
|
@ -7,15 +7,21 @@ import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.net.Uri
|
||||
import android.os.Environment
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.core.net.toUri
|
||||
import com.jakewharton.rxrelay.PublishRelay
|
||||
import eu.kanade.tachiyomi.data.preference.PreferenceValues
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.extension.installer.Installer
|
||||
import eu.kanade.tachiyomi.extension.model.Extension
|
||||
import eu.kanade.tachiyomi.extension.model.InstallStep
|
||||
import eu.kanade.tachiyomi.util.storage.getUriCompat
|
||||
import rx.Observable
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import timber.log.Timber
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.File
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
@ -47,6 +53,8 @@ internal class ExtensionInstaller(private val context: Context) {
|
||||
*/
|
||||
private val downloadsRelay = PublishRelay.create<Pair<Long, InstallStep>>()
|
||||
|
||||
private val installerPref = Injekt.get<PreferencesHelper>().extensionInstaller()
|
||||
|
||||
/**
|
||||
* Adds the given extension to the downloads queue and returns an observable containing its
|
||||
* step in the installation process.
|
||||
@ -79,8 +87,6 @@ internal class ExtensionInstaller(private val context: Context) {
|
||||
.map { it.second }
|
||||
// Poll download status
|
||||
.mergeWith(pollStatus(id))
|
||||
// Force an error if the download takes more than 3 minutes
|
||||
.mergeWith(Observable.timer(3, TimeUnit.MINUTES).map { InstallStep.Error })
|
||||
// Stop when the application is installed or errors
|
||||
.takeUntil { it.isCompleted() }
|
||||
// Always notify on main thread
|
||||
@ -126,12 +132,29 @@ internal class ExtensionInstaller(private val context: Context) {
|
||||
* @param uri The uri of the extension to install.
|
||||
*/
|
||||
fun installApk(downloadId: Long, uri: Uri) {
|
||||
val intent = Intent(context, ExtensionInstallActivity::class.java)
|
||||
.setDataAndType(uri, APK_MIME)
|
||||
.putExtra(EXTRA_DOWNLOAD_ID, downloadId)
|
||||
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
when (val installer = installerPref.get()) {
|
||||
PreferenceValues.ExtensionInstaller.LEGACY -> {
|
||||
val intent = Intent(context, ExtensionInstallActivity::class.java)
|
||||
.setDataAndType(uri, APK_MIME)
|
||||
.putExtra(EXTRA_DOWNLOAD_ID, downloadId)
|
||||
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
|
||||
context.startActivity(intent)
|
||||
context.startActivity(intent)
|
||||
}
|
||||
else -> {
|
||||
val intent = ExtensionInstallService.getIntent(context, downloadId, uri, installer)
|
||||
ContextCompat.startForegroundService(context, intent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels extension install and remove from download manager and installer.
|
||||
*/
|
||||
fun cancelInstall(pkgName: String) {
|
||||
val downloadId = activeDownloads.remove(pkgName) ?: return
|
||||
downloadManager.remove(downloadId)
|
||||
Installer.cancelInstallQueue(context, downloadId)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -147,13 +170,12 @@ internal class ExtensionInstaller(private val context: Context) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the result of the installation of an extension.
|
||||
* Sets the step of the installation of an extension.
|
||||
*
|
||||
* @param downloadId The id of the download.
|
||||
* @param result Whether the extension was installed or not.
|
||||
* @param step New install step.
|
||||
*/
|
||||
fun setInstallationResult(downloadId: Long, result: Boolean) {
|
||||
val step = if (result) InstallStep.Installed else InstallStep.Error
|
||||
fun updateInstallStep(downloadId: Long, step: InstallStep) {
|
||||
downloadsRelay.call(downloadId to step)
|
||||
}
|
||||
|
||||
@ -216,9 +238,7 @@ internal class ExtensionInstaller(private val context: Context) {
|
||||
val uri = downloadManager.getUriForDownloadedFile(id)
|
||||
|
||||
// Set next installation step
|
||||
if (uri != null) {
|
||||
downloadsRelay.call(id to InstallStep.Installing)
|
||||
} else {
|
||||
if (uri == null) {
|
||||
Timber.e("Couldn't locate downloaded APK")
|
||||
downloadsRelay.call(id to InstallStep.Error)
|
||||
return
|
||||
|
@ -6,7 +6,6 @@ import android.content.pm.PackageInfo
|
||||
import android.content.pm.PackageManager
|
||||
import androidx.core.content.pm.PackageInfoCompat
|
||||
import dalvik.system.PathClassLoader
|
||||
import eu.kanade.tachiyomi.annotations.Nsfw
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.extension.model.Extension
|
||||
import eu.kanade.tachiyomi.extension.model.LoadResult
|
||||
@ -154,13 +153,7 @@ internal object ExtensionLoader {
|
||||
try {
|
||||
when (val obj = Class.forName(it, false, classLoader).newInstance()) {
|
||||
is Source -> listOf(obj)
|
||||
is SourceFactory -> {
|
||||
if (isSourceNsfw(obj)) {
|
||||
emptyList()
|
||||
} else {
|
||||
obj.createSources()
|
||||
}
|
||||
}
|
||||
is SourceFactory -> obj.createSources()
|
||||
else -> throw Exception("Unknown source class type! ${obj.javaClass}")
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
@ -168,7 +161,6 @@ internal object ExtensionLoader {
|
||||
return LoadResult.Error(e)
|
||||
}
|
||||
}
|
||||
.filter { !isSourceNsfw(it) }
|
||||
|
||||
val langs = sources.filterIsInstance<CatalogueSource>()
|
||||
.map { it.lang }
|
||||
@ -215,22 +207,4 @@ internal object ExtensionLoader {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a Source or SourceFactory is annotated with @Nsfw.
|
||||
*/
|
||||
private fun isSourceNsfw(clazz: Any): Boolean {
|
||||
if (loadNsfwSource) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (clazz !is Source && clazz !is SourceFactory) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Annotations are proxied, hence this janky way of checking for them
|
||||
return clazz.javaClass.annotations
|
||||
.flatMap { it.javaClass.interfaces.map { it.simpleName } }
|
||||
.firstOrNull { it == Nsfw::class.java.simpleName } != null
|
||||
}
|
||||
}
|
||||
|
@ -271,18 +271,13 @@ class LocalSource(private val context: Context) : CatalogueSource {
|
||||
throw Exception(context.getString(R.string.chapter_not_found))
|
||||
}
|
||||
|
||||
private fun getFormat(file: File): Format {
|
||||
val extension = file.extension
|
||||
return if (file.isDirectory) {
|
||||
Format.Directory(file)
|
||||
} else if (extension.equals("zip", true) || extension.equals("cbz", true)) {
|
||||
Format.Zip(file)
|
||||
} else if (extension.equals("rar", true) || extension.equals("cbr", true)) {
|
||||
Format.Rar(file)
|
||||
} else if (extension.equals("epub", true)) {
|
||||
Format.Epub(file)
|
||||
} else {
|
||||
throw Exception(context.getString(R.string.local_invalid_format))
|
||||
private fun getFormat(file: File) = with(file) {
|
||||
when {
|
||||
isDirectory -> Format.Directory(this)
|
||||
extension.equals("zip", true) || extension.equals("cbz", true) -> Format.Zip(this)
|
||||
extension.equals("rar", true) || extension.equals("cbr", true) -> Format.Rar(this)
|
||||
extension.equals("epub", true) -> Format.Epub(this)
|
||||
else -> throw Exception(context.getString(R.string.local_invalid_format))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -11,7 +11,6 @@ import rx.Observable
|
||||
open class SourceManager(private val context: Context) {
|
||||
|
||||
private val sourcesMap = mutableMapOf<Long, Source>()
|
||||
|
||||
private val stubSourcesMap = mutableMapOf<Long, StubSource>()
|
||||
|
||||
init {
|
||||
|
@ -22,5 +22,6 @@ class ExtensionAdapter(controller: ExtensionController) :
|
||||
|
||||
interface OnButtonClickListener {
|
||||
fun onButtonClick(position: Int)
|
||||
fun onCancelButtonClick(position: Int)
|
||||
}
|
||||
}
|
||||
|
@ -119,6 +119,11 @@ open class ExtensionController :
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCancelButtonClick(position: Int) {
|
||||
val extension = (adapter?.getItem(position) as? ExtensionItem)?.extension ?: return
|
||||
presenter.cancelInstallUpdateExtension(extension)
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
inflater.inflate(R.menu.browse_extensions, menu)
|
||||
|
||||
|
@ -1,31 +1,28 @@
|
||||
package eu.kanade.tachiyomi.ui.browse.extension
|
||||
|
||||
import android.view.View
|
||||
import androidx.core.view.isVisible
|
||||
import coil.clear
|
||||
import coil.load
|
||||
import eu.davidea.viewholders.FlexibleViewHolder
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.databinding.ExtensionCardItemBinding
|
||||
import eu.kanade.tachiyomi.extension.model.Extension
|
||||
import eu.kanade.tachiyomi.extension.model.InstallStep
|
||||
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) :
|
||||
FlexibleViewHolder(view, adapter) {
|
||||
|
||||
private val binding = ExtensionCardItemBinding.bind(view)
|
||||
|
||||
private val shouldLabelNsfw by lazy {
|
||||
Injekt.get<PreferencesHelper>().labelNsfwExtension()
|
||||
}
|
||||
|
||||
init {
|
||||
binding.extButton.setOnClickListener {
|
||||
adapter.buttonClickListener.onButtonClick(bindingAdapterPosition)
|
||||
}
|
||||
binding.cancelButton.setOnClickListener {
|
||||
adapter.buttonClickListener.onCancelButtonClick(bindingAdapterPosition)
|
||||
}
|
||||
}
|
||||
|
||||
fun bind(item: ExtensionItem) {
|
||||
@ -38,7 +35,7 @@ class ExtensionHolder(view: View, val adapter: ExtensionAdapter) :
|
||||
extension is Extension.Untrusted -> itemView.context.getString(R.string.ext_untrusted)
|
||||
extension is Extension.Installed && extension.isUnofficial -> itemView.context.getString(R.string.ext_unofficial)
|
||||
extension is Extension.Installed && extension.isObsolete -> itemView.context.getString(R.string.ext_obsolete)
|
||||
extension.isNsfw && shouldLabelNsfw -> itemView.context.getString(R.string.ext_nsfw_short)
|
||||
extension.isNsfw -> itemView.context.getString(R.string.ext_nsfw_short)
|
||||
else -> ""
|
||||
}.uppercase()
|
||||
|
||||
@ -48,44 +45,40 @@ class ExtensionHolder(view: View, val adapter: ExtensionAdapter) :
|
||||
} else {
|
||||
extension.getApplicationIcon(itemView.context)?.let { binding.image.setImageDrawable(it) }
|
||||
}
|
||||
bindButton(item)
|
||||
bindButtons(item)
|
||||
}
|
||||
|
||||
@Suppress("ResourceType")
|
||||
fun bindButton(item: ExtensionItem) = with(binding.extButton) {
|
||||
isEnabled = true
|
||||
isClickable = true
|
||||
|
||||
fun bindButtons(item: ExtensionItem) = with(binding.extButton) {
|
||||
val extension = item.extension
|
||||
|
||||
val installStep = item.installStep
|
||||
if (installStep != null) {
|
||||
setText(
|
||||
when (installStep) {
|
||||
InstallStep.Pending -> R.string.ext_pending
|
||||
InstallStep.Downloading -> R.string.ext_downloading
|
||||
InstallStep.Installing -> R.string.ext_installing
|
||||
InstallStep.Installed -> R.string.ext_installed
|
||||
InstallStep.Error -> R.string.action_retry
|
||||
}
|
||||
)
|
||||
if (installStep != InstallStep.Error) {
|
||||
isEnabled = false
|
||||
isClickable = false
|
||||
}
|
||||
} else if (extension is Extension.Installed) {
|
||||
when {
|
||||
extension.hasUpdate -> {
|
||||
setText(R.string.ext_update)
|
||||
}
|
||||
else -> {
|
||||
setText(R.string.action_settings)
|
||||
setText(
|
||||
when (installStep) {
|
||||
InstallStep.Pending -> R.string.ext_pending
|
||||
InstallStep.Downloading -> R.string.ext_downloading
|
||||
InstallStep.Installing -> R.string.ext_installing
|
||||
InstallStep.Installed -> R.string.ext_installed
|
||||
InstallStep.Error -> R.string.action_retry
|
||||
InstallStep.Idle -> {
|
||||
when (extension) {
|
||||
is Extension.Installed -> {
|
||||
if (extension.hasUpdate) {
|
||||
R.string.ext_update
|
||||
} else {
|
||||
R.string.action_settings
|
||||
}
|
||||
}
|
||||
is Extension.Untrusted -> R.string.ext_trust
|
||||
is Extension.Available -> R.string.ext_install
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (extension is Extension.Untrusted) {
|
||||
setText(R.string.ext_trust)
|
||||
} else {
|
||||
setText(R.string.ext_install)
|
||||
}
|
||||
)
|
||||
|
||||
val isIdle = installStep == InstallStep.Idle || installStep == InstallStep.Error
|
||||
binding.cancelButton.isVisible = !isIdle
|
||||
isEnabled = isIdle
|
||||
isClickable = isIdle
|
||||
}
|
||||
}
|
||||
|
@ -19,7 +19,7 @@ import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
data class ExtensionItem(
|
||||
val extension: Extension,
|
||||
val header: ExtensionGroupItem? = null,
|
||||
val installStep: InstallStep? = null
|
||||
val installStep: InstallStep = InstallStep.Idle
|
||||
) :
|
||||
AbstractSectionableItem<ExtensionHolder, ExtensionGroupItem>(header) {
|
||||
|
||||
@ -49,7 +49,7 @@ data class ExtensionItem(
|
||||
if (payloads == null || payloads.isEmpty()) {
|
||||
holder.bind(this)
|
||||
} else {
|
||||
holder.bindButton(this)
|
||||
holder.bindButtons(this)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -55,14 +55,14 @@ open class ExtensionPresenter(
|
||||
private fun toItems(tuple: ExtensionTuple): List<ExtensionItem> {
|
||||
val context = Injekt.get<Application>()
|
||||
val activeLangs = preferences.enabledLanguages().get()
|
||||
val showNsfwExtensions = preferences.showNsfwExtension().get()
|
||||
val showNsfwSources = preferences.showNsfwSource().get()
|
||||
|
||||
val (installed, untrusted, available) = tuple
|
||||
|
||||
val items = mutableListOf<ExtensionItem>()
|
||||
|
||||
val updatesSorted = installed.filter { it.hasUpdate && (showNsfwExtensions || !it.isNsfw) }.sortedBy { it.name }
|
||||
val installedSorted = installed.filter { !it.hasUpdate && (showNsfwExtensions || !it.isNsfw) }.sortedWith(compareBy({ !it.isObsolete }, { it.name }))
|
||||
val updatesSorted = installed.filter { it.hasUpdate && (showNsfwSources || !it.isNsfw) }.sortedBy { it.name }
|
||||
val installedSorted = installed.filter { !it.hasUpdate && (showNsfwSources || !it.isNsfw) }.sortedWith(compareBy({ !it.isObsolete }, { it.name }))
|
||||
val untrustedSorted = untrusted.sortedBy { it.name }
|
||||
val availableSorted = available
|
||||
// Filter out already installed extensions and disabled languages
|
||||
@ -70,21 +70,21 @@ open class ExtensionPresenter(
|
||||
installed.none { it.pkgName == avail.pkgName } &&
|
||||
untrusted.none { it.pkgName == avail.pkgName } &&
|
||||
(avail.lang in activeLangs || avail.lang == "all") &&
|
||||
(showNsfwExtensions || !avail.isNsfw)
|
||||
(showNsfwSources || !avail.isNsfw)
|
||||
}
|
||||
.sortedBy { it.name }
|
||||
|
||||
if (updatesSorted.isNotEmpty()) {
|
||||
val header = ExtensionGroupItem(context.getString(R.string.ext_updates_pending), updatesSorted.size, true)
|
||||
items += updatesSorted.map { extension ->
|
||||
ExtensionItem(extension, header, currentDownloads[extension.pkgName])
|
||||
ExtensionItem(extension, header, currentDownloads[extension.pkgName] ?: InstallStep.Idle)
|
||||
}
|
||||
}
|
||||
if (installedSorted.isNotEmpty() || untrustedSorted.isNotEmpty()) {
|
||||
val header = ExtensionGroupItem(context.getString(R.string.ext_installed), installedSorted.size + untrustedSorted.size)
|
||||
|
||||
items += installedSorted.map { extension ->
|
||||
ExtensionItem(extension, header, currentDownloads[extension.pkgName])
|
||||
ExtensionItem(extension, header, currentDownloads[extension.pkgName] ?: InstallStep.Idle)
|
||||
}
|
||||
|
||||
items += untrustedSorted.map { extension ->
|
||||
@ -100,7 +100,7 @@ open class ExtensionPresenter(
|
||||
.forEach {
|
||||
val header = ExtensionGroupItem(it.key, it.value.size)
|
||||
items += it.value.map { extension ->
|
||||
ExtensionItem(extension, header, currentDownloads[extension.pkgName])
|
||||
ExtensionItem(extension, header, currentDownloads[extension.pkgName] ?: InstallStep.Idle)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -133,6 +133,10 @@ open class ExtensionPresenter(
|
||||
extensionManager.updateExtension(extension).subscribeToInstallUpdate(extension)
|
||||
}
|
||||
|
||||
fun cancelInstallUpdateExtension(extension: Extension) {
|
||||
extensionManager.cancelInstallUpdateExtension(extension)
|
||||
}
|
||||
|
||||
private fun Observable<InstallStep>.subscribeToInstallUpdate(extension: Extension) {
|
||||
this.doOnNext { currentDownloads[extension.pkgName] = it }
|
||||
.doOnUnsubscribe { currentDownloads.remove(extension.pkgName) }
|
||||
|
@ -2,10 +2,7 @@ package eu.kanade.tachiyomi.ui.browse.extension.details
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.provider.Settings
|
||||
import android.util.TypedValue
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
@ -14,7 +11,6 @@ import android.view.MenuItem
|
||||
import android.view.View
|
||||
import androidx.appcompat.view.ContextThemeWrapper
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceGroupAdapter
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.preference.PreferenceScreen
|
||||
@ -68,7 +64,7 @@ class ExtensionDetailsController(bundle: Bundle? = null) :
|
||||
}
|
||||
|
||||
override fun createPresenter(): ExtensionDetailsPresenter {
|
||||
return ExtensionDetailsPresenter(args.getString(PKGNAME_KEY)!!)
|
||||
return ExtensionDetailsPresenter(this, args.getString(PKGNAME_KEY)!!)
|
||||
}
|
||||
|
||||
override fun getTitle(): String? {
|
||||
@ -106,72 +102,87 @@ class ExtensionDetailsController(bundle: Bundle? = null) :
|
||||
val isMultiLangSingleSource = isMultiSource && extension.sources.map { it.name }.distinct().size == 1
|
||||
|
||||
with(screen) {
|
||||
extension.sources
|
||||
.groupBy { (it as CatalogueSource).lang }
|
||||
.toSortedMap(compareBy { LocaleHelper.getSourceDisplayName(it, context) })
|
||||
.forEach {
|
||||
val preferenceBlock = {
|
||||
it.value
|
||||
.sortedWith(compareBy({ !it.isEnabled() }, { it.name.lowercase() }))
|
||||
.forEach { source ->
|
||||
val sourcePrefs = mutableListOf<Preference>()
|
||||
|
||||
val block: (@DSL SwitchPreferenceCompat).() -> Unit = {
|
||||
key = source.getPreferenceKey()
|
||||
title = when {
|
||||
isMultiSource && !isMultiLangSingleSource -> source.toString()
|
||||
else -> LocaleHelper.getSourceDisplayName(it.key, context)
|
||||
}
|
||||
isPersistent = false
|
||||
isChecked = source.isEnabled()
|
||||
|
||||
onChange { newValue ->
|
||||
val checked = newValue as Boolean
|
||||
toggleSource(source, checked)
|
||||
true
|
||||
}
|
||||
|
||||
// React to enable/disable all changes
|
||||
preferences.disabledSources().asFlow()
|
||||
.onEach {
|
||||
val enabled = source.isEnabled()
|
||||
isChecked = enabled
|
||||
sourcePrefs.forEach { pref -> pref.isVisible = enabled }
|
||||
}
|
||||
.launchIn(viewScope)
|
||||
}
|
||||
|
||||
// Source enable/disable
|
||||
if (source is ConfigurableSource) {
|
||||
switchSettingsPreference {
|
||||
block()
|
||||
onSettingsClick = View.OnClickListener {
|
||||
router.pushController(
|
||||
SourcePreferencesController(source.id).withFadeTransaction()
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
switchPreference(block)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isMultiSource && !isMultiLangSingleSource) {
|
||||
preferenceCategory {
|
||||
title = LocaleHelper.getSourceDisplayName(it.key, context)
|
||||
|
||||
preferenceBlock()
|
||||
}
|
||||
} else {
|
||||
preferenceBlock()
|
||||
}
|
||||
}
|
||||
if (isMultiSource && isMultiLangSingleSource.not()) {
|
||||
multiLanguagePreference(context, extension.sources)
|
||||
} else {
|
||||
singleLanguagePreference(context, extension.sources)
|
||||
}
|
||||
}
|
||||
|
||||
return PreferenceGroupAdapter(screen)
|
||||
}
|
||||
|
||||
private fun PreferenceScreen.singleLanguagePreference(context: Context, sources: List<Source>) {
|
||||
sources
|
||||
.map { source -> LocaleHelper.getSourceDisplayName(source.lang, context) to source }
|
||||
.sortedWith(compareBy({ (_, source) -> !source.isEnabled() }, { (lang, _) -> lang.lowercase() }))
|
||||
.forEach { (lang, source) ->
|
||||
val preferenceBlock = {
|
||||
sourceSwitchPreference(source, LocaleHelper.getSourceDisplayName(lang, context))
|
||||
}
|
||||
|
||||
preferenceBlock()
|
||||
}
|
||||
}
|
||||
|
||||
private fun PreferenceScreen.multiLanguagePreference(context: Context, sources: List<Source>) {
|
||||
sources
|
||||
.groupBy { (it as CatalogueSource).lang }
|
||||
.toSortedMap(compareBy { LocaleHelper.getSourceDisplayName(it, context) })
|
||||
.forEach { entry ->
|
||||
val preferenceBlock = {
|
||||
entry.value
|
||||
.sortedWith(compareBy({ source -> !source.isEnabled() }, { source -> source.name.lowercase() }))
|
||||
.forEach { source ->
|
||||
sourceSwitchPreference(source, source.toString())
|
||||
}
|
||||
}
|
||||
|
||||
preferenceCategory {
|
||||
title = LocaleHelper.getSourceDisplayName(entry.key, context)
|
||||
|
||||
preferenceBlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun PreferenceScreen.sourceSwitchPreference(source: Source, name: String) {
|
||||
val block: (@DSL SwitchPreferenceCompat).() -> Unit = {
|
||||
key = source.getPreferenceKey()
|
||||
title = name
|
||||
isPersistent = false
|
||||
isChecked = source.isEnabled()
|
||||
|
||||
onChange { newValue ->
|
||||
val checked = newValue as Boolean
|
||||
toggleSource(source, checked)
|
||||
true
|
||||
}
|
||||
|
||||
// React to enable/disable all changes
|
||||
preferences.disabledSources().asFlow()
|
||||
.onEach {
|
||||
val enabled = source.isEnabled()
|
||||
isChecked = enabled
|
||||
}
|
||||
.launchIn(viewScope)
|
||||
}
|
||||
|
||||
// Source enable/disable
|
||||
if (source is ConfigurableSource) {
|
||||
switchSettingsPreference {
|
||||
block()
|
||||
onSettingsClick = View.OnClickListener {
|
||||
router.pushController(
|
||||
SourcePreferencesController(source.id).withFadeTransaction()
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
switchPreference(block)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyView(view: View) {
|
||||
preferenceScreen = null
|
||||
super.onDestroyView(view)
|
||||
@ -188,7 +199,6 @@ class ExtensionDetailsController(bundle: Bundle? = null) :
|
||||
R.id.action_history -> openCommitHistory()
|
||||
R.id.action_enable_all -> toggleAllSources(true)
|
||||
R.id.action_disable_all -> toggleAllSources(false)
|
||||
R.id.action_open_in_settings -> openInSettings()
|
||||
}
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
@ -219,13 +229,6 @@ class ExtensionDetailsController(bundle: Bundle? = null) :
|
||||
openInBrowser(url)
|
||||
}
|
||||
|
||||
private fun openInSettings() {
|
||||
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
|
||||
data = Uri.fromParts("package", presenter.pkgName, null)
|
||||
}
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
private fun Source.isEnabled(): Boolean {
|
||||
return id.toString() !in preferences.disabledSources().get()
|
||||
}
|
||||
|
@ -44,6 +44,9 @@ class ExtensionDetailsHeaderAdapter(private val presenter: ExtensionDetailsPrese
|
||||
binding.btnUninstall.clicks()
|
||||
.onEach { presenter.uninstallExtension() }
|
||||
.launchIn(presenter.presenterScope)
|
||||
binding.btnAppInfo.clicks()
|
||||
.onEach { presenter.openInSettings() }
|
||||
.launchIn(presenter.presenterScope)
|
||||
|
||||
if (extension.isObsolete) {
|
||||
binding.warningBanner.isVisible = true
|
||||
|
@ -1,17 +1,21 @@
|
||||
package eu.kanade.tachiyomi.ui.browse.extension.details
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.provider.Settings
|
||||
import eu.kanade.tachiyomi.extension.ExtensionManager
|
||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
class ExtensionDetailsPresenter(
|
||||
val pkgName: String,
|
||||
private val extensionManager: ExtensionManager = Injekt.get()
|
||||
private val controller: ExtensionDetailsController,
|
||||
private val pkgName: String,
|
||||
) : BasePresenter<ExtensionDetailsController>() {
|
||||
|
||||
private val extensionManager: ExtensionManager by injectLazy()
|
||||
|
||||
val extension = extensionManager.installedExtensions.find { it.pkgName == pkgName }
|
||||
|
||||
override fun onCreate(savedState: Bundle?) {
|
||||
@ -36,4 +40,11 @@ class ExtensionDetailsPresenter(
|
||||
val extension = extension ?: return
|
||||
extensionManager.uninstallExtension(extension.pkgName)
|
||||
}
|
||||
|
||||
fun openInSettings() {
|
||||
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
|
||||
data = Uri.fromParts("package", pkgName, null)
|
||||
}
|
||||
controller.startActivity(intent)
|
||||
}
|
||||
}
|
||||
|
@ -9,16 +9,20 @@ import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import dev.chrisbanes.insetter.applyInsetter
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.databinding.MigrationSourcesControllerBinding
|
||||
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
|
||||
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
||||
import eu.kanade.tachiyomi.ui.browse.migration.manga.MigrationMangaController
|
||||
import eu.kanade.tachiyomi.util.system.openInBrowser
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
class MigrationSourcesController :
|
||||
NucleusController<MigrationSourcesControllerBinding, MigrationSourcesPresenter>(),
|
||||
FlexibleAdapter.OnItemClickListener {
|
||||
|
||||
private val preferences: PreferencesHelper by injectLazy()
|
||||
|
||||
private var adapter: SourceAdapter? = null
|
||||
|
||||
init {
|
||||
@ -56,13 +60,39 @@ class MigrationSourcesController :
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
when (val itemId = item.itemId) {
|
||||
R.id.action_source_migration_help -> activity?.openInBrowser(HELP_URL)
|
||||
R.id.asc_alphabetical, R.id.desc_alphabetical -> {
|
||||
setSortingDirection(SortSetting.ALPHABETICAL, itemId == R.id.asc_alphabetical)
|
||||
}
|
||||
R.id.asc_count, R.id.desc_count -> {
|
||||
setSortingDirection(SortSetting.TOTAL, itemId == R.id.asc_count)
|
||||
}
|
||||
}
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
private fun setSortingDirection(sortSetting: SortSetting, isAscending: Boolean) {
|
||||
val direction = if (isAscending) {
|
||||
DirectionSetting.ASCENDING
|
||||
} else {
|
||||
DirectionSetting.DESCENDING
|
||||
}
|
||||
|
||||
preferences.migrationSortingDirection().set(direction)
|
||||
preferences.migrationSortingMode().set(sortSetting)
|
||||
|
||||
presenter.requestSortUpdate()
|
||||
}
|
||||
|
||||
fun setSources(sourcesWithManga: List<SourceItem>) {
|
||||
// Show empty view if needed
|
||||
if (sourcesWithManga.isNotEmpty()) {
|
||||
binding.emptyView.hide()
|
||||
} else {
|
||||
binding.emptyView.show(R.string.information_empty_library)
|
||||
}
|
||||
|
||||
adapter?.updateDataSet(sourcesWithManga)
|
||||
}
|
||||
|
||||
@ -72,6 +102,16 @@ class MigrationSourcesController :
|
||||
parentController!!.router.pushController(controller.withFadeTransaction())
|
||||
return false
|
||||
}
|
||||
|
||||
enum class DirectionSetting {
|
||||
ASCENDING,
|
||||
DESCENDING;
|
||||
}
|
||||
|
||||
enum class SortSetting {
|
||||
ALPHABETICAL,
|
||||
TOTAL;
|
||||
}
|
||||
}
|
||||
|
||||
private const val HELP_URL = "https://tachiyomi.org/help/guides/source-migration/"
|
||||
|
@ -1,25 +1,38 @@
|
||||
package eu.kanade.tachiyomi.ui.browse.migration.sources
|
||||
|
||||
import android.os.Bundle
|
||||
import com.jakewharton.rxrelay.BehaviorRelay
|
||||
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.LocalSource
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||
import eu.kanade.tachiyomi.util.lang.combineLatest
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import rx.schedulers.Schedulers
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.text.Collator
|
||||
import java.util.Collections
|
||||
import java.util.Locale
|
||||
|
||||
class MigrationSourcesPresenter(
|
||||
private val sourceManager: SourceManager = Injekt.get(),
|
||||
private val db: DatabaseHelper = Injekt.get()
|
||||
) : BasePresenter<MigrationSourcesController>() {
|
||||
|
||||
private val preferences: PreferencesHelper by injectLazy()
|
||||
|
||||
private val sortRelay = BehaviorRelay.create(Unit)
|
||||
|
||||
override fun onCreate(savedState: Bundle?) {
|
||||
super.onCreate(savedState)
|
||||
|
||||
db.getFavoriteMangas()
|
||||
.asRxObservable()
|
||||
.combineLatest(sortRelay.observeOn(Schedulers.io())) { sources, _ -> sources }
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.map { findSourcesWithManga(it) }
|
||||
.subscribeLatestCache(MigrationSourcesController::setSources)
|
||||
@ -34,7 +47,36 @@ class MigrationSourcesPresenter(
|
||||
val source = sourceManager.getOrStub(it.key)
|
||||
SourceItem(source, it.value.size, header)
|
||||
}
|
||||
.sortedBy { it.source.name.lowercase() }
|
||||
.sortedWith(sortFn())
|
||||
.toList()
|
||||
}
|
||||
|
||||
fun sortFn(): java.util.Comparator<SourceItem> {
|
||||
val sort by lazy {
|
||||
preferences.migrationSortingMode().get()
|
||||
}
|
||||
val direction by lazy {
|
||||
preferences.migrationSortingDirection().get()
|
||||
}
|
||||
|
||||
val locale = Locale.getDefault()
|
||||
val collator = Collator.getInstance(locale).apply {
|
||||
strength = Collator.PRIMARY
|
||||
}
|
||||
val sortFn: (SourceItem, SourceItem) -> Int = { a, b ->
|
||||
when (sort) {
|
||||
MigrationSourcesController.SortSetting.ALPHABETICAL -> collator.compare(a.source.name.lowercase(locale), b.source.name.lowercase(locale))
|
||||
MigrationSourcesController.SortSetting.TOTAL -> a.mangaCount.compareTo(b.mangaCount)
|
||||
}
|
||||
}
|
||||
|
||||
return when (direction) {
|
||||
MigrationSourcesController.DirectionSetting.ASCENDING -> Comparator(sortFn)
|
||||
MigrationSourcesController.DirectionSetting.DESCENDING -> Collections.reverseOrder(sortFn)
|
||||
}
|
||||
}
|
||||
|
||||
fun requestSortUpdate() {
|
||||
sortRelay.call(Unit)
|
||||
}
|
||||
}
|
||||
|
@ -195,7 +195,7 @@ class CategoryController :
|
||||
(activity as? MainActivity)?.binding?.rootCoordinator!!,
|
||||
R.string.snack_categories_deleted,
|
||||
R.string.action_undo,
|
||||
3000
|
||||
4000
|
||||
)
|
||||
|
||||
mode.finish()
|
||||
|
@ -6,6 +6,7 @@ import android.view.ViewGroup
|
||||
import eu.kanade.tachiyomi.data.database.models.Category
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.databinding.LibraryCategoryBinding
|
||||
import eu.kanade.tachiyomi.ui.library.setting.DisplayModeSetting
|
||||
import eu.kanade.tachiyomi.widget.RecyclerViewPagerAdapter
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
@ -45,15 +46,18 @@ class LibraryAdapter(
|
||||
|
||||
private var boundViews = arrayListOf<View>()
|
||||
|
||||
private val isPerCategory by lazy { preferences.categorisedDisplaySettings().get() }
|
||||
private val currentDisplayMode by lazy { preferences.libraryDisplayMode().get() }
|
||||
|
||||
/**
|
||||
* Creates a new view for this adapter.
|
||||
*
|
||||
* @return a new view.
|
||||
*/
|
||||
override fun createView(container: ViewGroup): View {
|
||||
override fun inflateView(container: ViewGroup, viewType: Int): View {
|
||||
val binding = LibraryCategoryBinding.inflate(LayoutInflater.from(container.context), container, false)
|
||||
val view: LibraryCategoryView = binding.root
|
||||
view.onCreate(controller, binding)
|
||||
view.onCreate(controller, binding, viewType)
|
||||
return view
|
||||
}
|
||||
|
||||
@ -120,4 +124,26 @@ class LibraryAdapter(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getViewType(position: Int): Int {
|
||||
val category = categories[position]
|
||||
return if (isPerCategory && category.id != 0) {
|
||||
if (DisplayModeSetting.fromFlag(category.displayMode) == DisplayModeSetting.LIST) {
|
||||
LIST_DISPLAY_MODE
|
||||
} else {
|
||||
GRID_DISPLAY_MODE
|
||||
}
|
||||
} else {
|
||||
if (currentDisplayMode == DisplayModeSetting.LIST) {
|
||||
LIST_DISPLAY_MODE
|
||||
} else {
|
||||
GRID_DISPLAY_MODE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val LIST_DISPLAY_MODE = 1
|
||||
const val GRID_DISPLAY_MODE = 2
|
||||
}
|
||||
}
|
||||
|
@ -5,14 +5,14 @@ import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import android.widget.FrameLayout
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import dev.chrisbanes.insetter.applyInsetter
|
||||
import dev.chrisbanes.insetter.Insetter
|
||||
import dev.chrisbanes.insetter.windowInsetTypesOf
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.SelectableAdapter
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Category
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateService
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.databinding.LibraryCategoryBinding
|
||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||
import eu.kanade.tachiyomi.util.lang.plusAssign
|
||||
@ -27,9 +27,7 @@ import kotlinx.coroutines.flow.onEach
|
||||
import reactivecircus.flowbinding.recyclerview.scrollStateChanges
|
||||
import reactivecircus.flowbinding.swiperefreshlayout.refreshes
|
||||
import rx.subscriptions.CompositeSubscription
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.util.ArrayDeque
|
||||
import eu.kanade.tachiyomi.ui.library.setting.DisplayModeSetting as DisplayMode
|
||||
|
||||
/**
|
||||
* Fragment containing the library manga for a certain category.
|
||||
@ -41,8 +39,6 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
|
||||
|
||||
private val scope = MainScope()
|
||||
|
||||
private val preferences: PreferencesHelper by injectLazy()
|
||||
|
||||
/**
|
||||
* The fragment containing this view.
|
||||
*/
|
||||
@ -71,12 +67,10 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
|
||||
|
||||
private var lastClickPositionStack = ArrayDeque(listOf(-1))
|
||||
|
||||
fun onCreate(controller: LibraryController, binding: LibraryCategoryBinding) {
|
||||
fun onCreate(controller: LibraryController, binding: LibraryCategoryBinding, viewType: Int) {
|
||||
this.controller = controller
|
||||
|
||||
recycler = if (preferences.libraryDisplayMode().get() == DisplayMode.LIST &&
|
||||
!preferences.categorisedDisplaySettings().get()
|
||||
) {
|
||||
recycler = if (viewType == LibraryAdapter.LIST_DISPLAY_MODE) {
|
||||
(binding.swipeRefresh.inflate(R.layout.library_list_recycler) as AutofitRecyclerView).apply {
|
||||
spanCount = 1
|
||||
}
|
||||
@ -86,11 +80,9 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
|
||||
}
|
||||
}
|
||||
|
||||
recycler.applyInsetter {
|
||||
type(navigationBars = true) {
|
||||
padding()
|
||||
}
|
||||
}
|
||||
Insetter.builder()
|
||||
.paddingBottom(windowInsetTypesOf(navigationBars = true))
|
||||
.applyToView(recycler)
|
||||
|
||||
adapter = LibraryCategoryAdapter(this)
|
||||
|
||||
@ -129,15 +121,6 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
|
||||
fun onBind(category: Category) {
|
||||
this.category = category
|
||||
|
||||
// If displayMode should be set from category adjust manga count per row
|
||||
if (preferences.categorisedDisplaySettings().get()) {
|
||||
recycler.spanCount = if (DisplayMode.fromFlag(category.displayMode) == DisplayMode.LIST || (preferences.libraryDisplayMode().get() == DisplayMode.LIST && category.id == 0)) {
|
||||
1
|
||||
} else {
|
||||
controller.mangaPerRow
|
||||
}
|
||||
}
|
||||
|
||||
adapter.mode = if (controller.selectedMangas.isNotEmpty()) {
|
||||
SelectableAdapter.Mode.MULTI
|
||||
} else {
|
||||
|
@ -42,9 +42,12 @@ import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import reactivecircus.flowbinding.android.view.clicks
|
||||
import reactivecircus.flowbinding.viewpager.pageSelections
|
||||
import rx.Observable
|
||||
import rx.Subscription
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class LibraryController(
|
||||
bundle: Bundle? = null,
|
||||
@ -199,10 +202,12 @@ class LibraryController(
|
||||
is LibrarySettingsSheet.Filter.FilterGroup -> onFilterChanged()
|
||||
is LibrarySettingsSheet.Sort.SortGroup -> onSortChanged()
|
||||
is LibrarySettingsSheet.Display.DisplayGroup -> {
|
||||
if (!preferences.categorisedDisplaySettings().get() || activeCategory == 0) {
|
||||
// Reattach adapter when flow preference change
|
||||
reattachAdapter()
|
||||
}
|
||||
val delay = if (preferences.categorisedDisplaySettings().get()) 125L else 0L
|
||||
|
||||
Observable.timer(delay, TimeUnit.MILLISECONDS, AndroidSchedulers.mainThread())
|
||||
.subscribe {
|
||||
reattachAdapter()
|
||||
}
|
||||
}
|
||||
is LibrarySettingsSheet.Display.BadgeGroup -> onBadgeSettingChanged()
|
||||
is LibrarySettingsSheet.Display.TabsGroup -> onTabsSettingsChanged()
|
||||
@ -299,11 +304,6 @@ class LibraryController(
|
||||
.map { (it.id ?: -1) to (mangaMap[it.id]?.size ?: 0) }
|
||||
.toMap()
|
||||
|
||||
if (preferences.categorisedDisplaySettings().get()) {
|
||||
// Reattach adapter so it doesn't get de-synced
|
||||
reattachAdapter()
|
||||
}
|
||||
|
||||
// Restore active category.
|
||||
binding.libraryPager.setCurrentItem(activeCat, false)
|
||||
|
||||
@ -383,7 +383,7 @@ class LibraryController(
|
||||
actionMode!!,
|
||||
R.menu.library_selection
|
||||
) { onActionItemClicked(it!!) }
|
||||
(activity as? MainActivity)?.showBottomNav(visible = false, expand = true)
|
||||
(activity as? MainActivity)?.showBottomNav(false)
|
||||
}
|
||||
}
|
||||
|
||||
@ -492,7 +492,7 @@ class LibraryController(
|
||||
selectionRelay.call(LibrarySelectionEvent.Cleared())
|
||||
|
||||
binding.actionToolbar.hide()
|
||||
(activity as? MainActivity)?.showBottomNav(visible = true, expand = true)
|
||||
(activity as? MainActivity)?.showBottomNav(true)
|
||||
|
||||
actionMode = null
|
||||
}
|
||||
|
@ -10,7 +10,6 @@ import android.view.Gravity
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.view.ActionMode
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import androidx.core.animation.doOnEnd
|
||||
import androidx.core.splashscreen.SplashScreen
|
||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||
@ -64,7 +63,6 @@ import eu.kanade.tachiyomi.util.system.dpToPx
|
||||
import eu.kanade.tachiyomi.util.system.isTablet
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import eu.kanade.tachiyomi.util.view.setNavigationBarTransparentCompat
|
||||
import eu.kanade.tachiyomi.widget.HideBottomNavigationOnScrollBehavior
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.drop
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
@ -86,8 +84,6 @@ class MainActivity : BaseViewBindingActivity<MainActivityBinding>() {
|
||||
}
|
||||
}
|
||||
|
||||
private var bottomNavAnimator: ViewHeightAnimator? = null
|
||||
|
||||
private var isConfirmingExit: Boolean = false
|
||||
private var isHandlingShortcut: Boolean = false
|
||||
|
||||
@ -138,15 +134,6 @@ class MainActivity : BaseViewBindingActivity<MainActivityBinding>() {
|
||||
}
|
||||
setSplashScreenExitAnimation(splashScreen)
|
||||
|
||||
if (binding.bottomNav != null) {
|
||||
bottomNavAnimator = ViewHeightAnimator(binding.bottomNav!!)
|
||||
|
||||
// Set behavior of bottom nav
|
||||
preferences.hideBottomBarOnScroll()
|
||||
.asImmediateFlow { setBottomNavBehaviorOnScroll() }
|
||||
.launchIn(lifecycleScope)
|
||||
}
|
||||
|
||||
if (binding.sideNav != null) {
|
||||
preferences.sideNavIconAlignment()
|
||||
.asImmediateFlow {
|
||||
@ -532,11 +519,11 @@ class MainActivity : BaseViewBindingActivity<MainActivityBinding>() {
|
||||
binding.appbar.setExpanded(true)
|
||||
|
||||
if ((from == null || from is RootController) && to !is RootController) {
|
||||
showNav(visible = false, expand = true)
|
||||
showNav(false)
|
||||
}
|
||||
if (to is RootController) {
|
||||
// Always show bottom nav again when returning to a RootController
|
||||
showNav(visible = true, expand = from !is RootController)
|
||||
showNav(true)
|
||||
}
|
||||
|
||||
if (from is TabbedController) {
|
||||
@ -587,27 +574,22 @@ class MainActivity : BaseViewBindingActivity<MainActivityBinding>() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun showNav(visible: Boolean, expand: Boolean = false) {
|
||||
showBottomNav(visible, expand)
|
||||
private fun showNav(visible: Boolean) {
|
||||
showBottomNav(visible)
|
||||
showSideNav(visible)
|
||||
}
|
||||
|
||||
// Also used from some controllers to swap bottom nav with action toolbar
|
||||
fun showBottomNav(visible: Boolean, expand: Boolean = false) {
|
||||
fun showBottomNav(visible: Boolean) {
|
||||
if (visible) {
|
||||
binding.bottomNav?.translationY = 0F
|
||||
if (expand) {
|
||||
bottomNavAnimator?.expand()
|
||||
}
|
||||
binding.bottomNav?.slideUp()
|
||||
} else {
|
||||
bottomNavAnimator?.collapse()
|
||||
binding.bottomNav?.slideDown()
|
||||
}
|
||||
}
|
||||
|
||||
private fun showSideNav(visible: Boolean) {
|
||||
binding.sideNav?.let {
|
||||
it.isVisible = visible
|
||||
}
|
||||
binding.sideNav?.isVisible = visible
|
||||
}
|
||||
|
||||
/**
|
||||
@ -622,18 +604,6 @@ class MainActivity : BaseViewBindingActivity<MainActivityBinding>() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun setBottomNavBehaviorOnScroll() {
|
||||
showNav(visible = true)
|
||||
|
||||
binding.bottomNav?.updateLayoutParams<CoordinatorLayout.LayoutParams> {
|
||||
behavior = when {
|
||||
preferences.hideBottomBarOnScroll().get() -> HideBottomNavigationOnScrollBehavior()
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
binding.bottomNav?.translationY = 0F
|
||||
}
|
||||
|
||||
private val nav: NavigationBarView
|
||||
get() = binding.bottomNav ?: binding.sideNav!!
|
||||
|
||||
|
@ -1,107 +0,0 @@
|
||||
package eu.kanade.tachiyomi.ui.main
|
||||
|
||||
import android.animation.ObjectAnimator
|
||||
import android.view.View
|
||||
import android.view.ViewTreeObserver
|
||||
import android.view.animation.DecelerateInterpolator
|
||||
import androidx.annotation.Keep
|
||||
|
||||
class ViewHeightAnimator(val view: View, val duration: Long = 250L) {
|
||||
|
||||
/**
|
||||
* The default height of the view. It's unknown until the view is layout.
|
||||
*/
|
||||
private var height = 0
|
||||
|
||||
/**
|
||||
* Whether the last state of the view is shown or hidden.
|
||||
*/
|
||||
private var isLastStateShown = true
|
||||
|
||||
/**
|
||||
* Animation used to expand and collapse the view.
|
||||
*/
|
||||
private val animation by lazy {
|
||||
ObjectAnimator.ofInt(this, "height", height).apply {
|
||||
duration = this@ViewHeightAnimator.duration
|
||||
interpolator = DecelerateInterpolator()
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
view.viewTreeObserver.addOnGlobalLayoutListener(
|
||||
object : ViewTreeObserver.OnGlobalLayoutListener {
|
||||
override fun onGlobalLayout() {
|
||||
if (view.height > 0) {
|
||||
view.viewTreeObserver.removeOnGlobalLayoutListener(this)
|
||||
|
||||
// Save the tabs default height.
|
||||
height = view.height
|
||||
|
||||
// Now that we know the height, set the initial height.
|
||||
if (isLastStateShown) {
|
||||
setHeight(height)
|
||||
} else {
|
||||
setHeight(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the height of the tab layout.
|
||||
*
|
||||
* @param newHeight The new height of the tab layout.
|
||||
*/
|
||||
@Keep
|
||||
fun setHeight(newHeight: Int) {
|
||||
view.layoutParams.height = newHeight
|
||||
view.requestLayout()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the height of the tab layout. This method is also called from the animator through
|
||||
* reflection.
|
||||
*/
|
||||
fun getHeight(): Int {
|
||||
return view.layoutParams.height
|
||||
}
|
||||
|
||||
/**
|
||||
* Expands the tab layout with an animation.
|
||||
*/
|
||||
fun expand() {
|
||||
if (isMeasured) {
|
||||
if (getHeight() != height) {
|
||||
animation.setIntValues(height)
|
||||
animation.start()
|
||||
} else {
|
||||
animation.cancel()
|
||||
}
|
||||
}
|
||||
isLastStateShown = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Collapse the tab layout with an animation.
|
||||
*/
|
||||
fun collapse() {
|
||||
if (isMeasured) {
|
||||
if (getHeight() != 0) {
|
||||
animation.setIntValues(0)
|
||||
animation.start()
|
||||
} else {
|
||||
animation.cancel()
|
||||
}
|
||||
}
|
||||
isLastStateShown = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the tab layout has a known height.
|
||||
*/
|
||||
private val isMeasured: Boolean
|
||||
get() = height > 0
|
||||
}
|
@ -53,6 +53,7 @@ import eu.kanade.tachiyomi.source.LocalSource
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
||||
import eu.kanade.tachiyomi.ui.base.controller.FabController
|
||||
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
|
||||
import eu.kanade.tachiyomi.ui.base.controller.getMainAppBarHeight
|
||||
@ -92,10 +93,8 @@ import eu.kanade.tachiyomi.util.view.getCoordinates
|
||||
import eu.kanade.tachiyomi.util.view.shrinkOnScroll
|
||||
import eu.kanade.tachiyomi.util.view.snack
|
||||
import eu.kanade.tachiyomi.widget.materialdialogs.QuadStateTextView
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import reactivecircus.flowbinding.recyclerview.scrollEvents
|
||||
import reactivecircus.flowbinding.recyclerview.scrollStateChanges
|
||||
import reactivecircus.flowbinding.swiperefreshlayout.refreshes
|
||||
import timber.log.Timber
|
||||
@ -179,7 +178,17 @@ class MangaController :
|
||||
|
||||
private var trackSheet: TrackSheet? = null
|
||||
|
||||
private var dialog: MangaFullCoverDialog? = null
|
||||
private var dialog: DialogController? = null
|
||||
|
||||
/**
|
||||
* For [recyclerViewUpdatesToolbarTitleAlpha]
|
||||
*/
|
||||
private var recyclerViewToolbarTitleAlphaUpdaterAdded = false
|
||||
private val recyclerViewToolbarTitleAlphaUpdater = object : RecyclerView.OnScrollListener() {
|
||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||
updateToolbarTitleAlpha()
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
setHasOptionsMenu(true)
|
||||
@ -191,15 +200,12 @@ class MangaController :
|
||||
|
||||
override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) {
|
||||
super.onChangeStarted(handler, type)
|
||||
|
||||
// Hide toolbar title on enter
|
||||
if (type.isEnter) {
|
||||
updateToolbarTitleAlpha()
|
||||
} else if (!type.isPush) {
|
||||
// Cancel listeners early
|
||||
viewScope.cancel()
|
||||
updateToolbarTitleAlpha(1F)
|
||||
// No need to update alpha for cover dialog
|
||||
if (dialog == null) {
|
||||
updateToolbarTitleAlpha(if (type.isEnter) 0F else 1F)
|
||||
}
|
||||
recyclerViewUpdatesToolbarTitleAlpha(type.isEnter)
|
||||
}
|
||||
|
||||
override fun onChangeEnded(handler: ControllerChangeHandler, type: ControllerChangeType) {
|
||||
@ -250,19 +256,15 @@ class MangaController :
|
||||
binding.fullRecycler?.let {
|
||||
it.adapter = ConcatAdapter(mangaInfoAdapter, chaptersHeaderAdapter, chaptersAdapter)
|
||||
|
||||
it.scrollEvents()
|
||||
.onEach { updateToolbarTitleAlpha() }
|
||||
.launchIn(viewScope)
|
||||
|
||||
// Skips directly to chapters list if navigated to from the library
|
||||
it.post {
|
||||
if (!fromSource && preferences.jumpToChapters()) {
|
||||
(it.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(1, 0)
|
||||
}
|
||||
|
||||
// Delayed in case we need to jump to chapters
|
||||
it.post {
|
||||
updateToolbarTitleAlpha()
|
||||
val mainActivityAppBar = (activity as? MainActivity)?.binding?.appbar
|
||||
(it.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(
|
||||
1,
|
||||
mainActivityAppBar?.height ?: 0
|
||||
)
|
||||
mainActivityAppBar?.isLifted = true
|
||||
}
|
||||
}
|
||||
|
||||
@ -279,11 +281,6 @@ class MangaController :
|
||||
scroller.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
topMargin = getMainAppBarHeight()
|
||||
}
|
||||
scroller.applyInsetter {
|
||||
type(navigationBars = true) {
|
||||
margin()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
binding.swipeRefresh.doOnLayout { swipeRefresh ->
|
||||
@ -299,22 +296,10 @@ class MangaController :
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tablet layout
|
||||
binding.infoRecycler?.let {
|
||||
it.adapter = mangaInfoAdapter
|
||||
|
||||
it.scrollEvents()
|
||||
.onEach { updateToolbarTitleAlpha() }
|
||||
.launchIn(viewScope)
|
||||
|
||||
// Delayed in case we need to jump to chapters
|
||||
it.post {
|
||||
updateToolbarTitleAlpha()
|
||||
}
|
||||
}
|
||||
binding.chaptersRecycler?.let {
|
||||
it.adapter = ConcatAdapter(chaptersHeaderAdapter, chaptersAdapter)
|
||||
}
|
||||
binding.infoRecycler?.adapter = mangaInfoAdapter
|
||||
binding.chaptersRecycler?.adapter = ConcatAdapter(chaptersHeaderAdapter, chaptersAdapter)
|
||||
|
||||
chaptersAdapter?.fastScroller = binding.fastScroller
|
||||
|
||||
@ -339,6 +324,20 @@ class MangaController :
|
||||
trackSheet = TrackSheet(this, manga!!, (activity as MainActivity).supportFragmentManager)
|
||||
|
||||
updateFilterIconState()
|
||||
recyclerViewUpdatesToolbarTitleAlpha(true)
|
||||
}
|
||||
|
||||
private fun recyclerViewUpdatesToolbarTitleAlpha(enable: Boolean) {
|
||||
val recycler = binding.fullRecycler ?: binding.infoRecycler ?: return
|
||||
if (enable) {
|
||||
if (!recyclerViewToolbarTitleAlphaUpdaterAdded) {
|
||||
recycler.addOnScrollListener(recyclerViewToolbarTitleAlphaUpdater)
|
||||
recyclerViewToolbarTitleAlphaUpdaterAdded = true
|
||||
}
|
||||
} else if (recyclerViewToolbarTitleAlphaUpdaterAdded) {
|
||||
recycler.removeOnScrollListener(recyclerViewToolbarTitleAlphaUpdater)
|
||||
recyclerViewToolbarTitleAlphaUpdaterAdded = false
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateToolbarTitleAlpha(@FloatRange(from = 0.0, to = 1.0) alpha: Float? = null) {
|
||||
@ -399,6 +398,7 @@ class MangaController :
|
||||
}
|
||||
|
||||
override fun onDestroyView(view: View) {
|
||||
recyclerViewUpdatesToolbarTitleAlpha(false)
|
||||
destroyActionModeIfNeeded()
|
||||
binding.actionToolbar.destroy()
|
||||
mangaInfoAdapter = null
|
||||
@ -579,8 +579,7 @@ class MangaController :
|
||||
}
|
||||
}.toTypedArray()
|
||||
|
||||
ChangeMangaCategoriesDialog(this, listOf(manga), categories, preselected)
|
||||
.showDialog(router)
|
||||
showChangeCategoryDialog(manga, categories, preselected)
|
||||
}
|
||||
}
|
||||
|
||||
@ -608,6 +607,9 @@ class MangaController :
|
||||
*/
|
||||
private fun toggleFavorite() {
|
||||
val isNowFavorite = presenter.toggleFavorite()
|
||||
if (isNowFavorite) {
|
||||
addSnackbar?.dismiss()
|
||||
}
|
||||
if (activity != null && !isNowFavorite && presenter.hasDownloads()) {
|
||||
(activity as? MainActivity)?.binding?.rootCoordinator?.snack(activity!!.getString(R.string.delete_downloads_for_manga)) {
|
||||
setAction(R.string.action_delete) {
|
||||
@ -615,7 +617,6 @@ class MangaController :
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mangaInfoAdapter?.notifyDataSetChanged()
|
||||
}
|
||||
|
||||
@ -631,8 +632,21 @@ class MangaController :
|
||||
QuadStateTextView.State.UNCHECKED.ordinal
|
||||
}
|
||||
}.toTypedArray()
|
||||
ChangeMangaCategoriesDialog(this, listOf(manga), categories, preselected)
|
||||
.showDialog(router)
|
||||
showChangeCategoryDialog(manga, categories, preselected)
|
||||
}
|
||||
|
||||
private fun showChangeCategoryDialog(manga: Manga, categories: List<Category>, preselected: Array<Int>) {
|
||||
if (dialog != null) return
|
||||
dialog = ChangeMangaCategoriesDialog(this, listOf(manga), categories, preselected)
|
||||
dialog?.addLifecycleListener(
|
||||
object : LifecycleListener() {
|
||||
override fun postDestroy(controller: Controller) {
|
||||
super.postDestroy(controller)
|
||||
dialog = null
|
||||
}
|
||||
}
|
||||
)
|
||||
dialog?.showDialog(router)
|
||||
}
|
||||
|
||||
override fun updateCategoriesForMangas(mangas: List<Manga>, addCategories: List<Category>, removeCategories: List<Category>) {
|
||||
@ -816,7 +830,7 @@ class MangaController :
|
||||
|
||||
fun onSetCoverSuccess() {
|
||||
mangaInfoAdapter?.notifyDataSetChanged()
|
||||
dialog?.setImage(manga)
|
||||
(dialog as? MangaFullCoverDialog)?.setImage(manga)
|
||||
activity?.toast(R.string.cover_updated)
|
||||
}
|
||||
|
||||
@ -1111,7 +1125,7 @@ class MangaController :
|
||||
val manga = presenter.manga
|
||||
presenter.downloadChapters(chapters)
|
||||
if (view != null && !manga.favorite) {
|
||||
addSnackbar = (activity as? MainActivity)?.binding?.rootCoordinator?.snack(view.context.getString(R.string.snack_add_to_library), Snackbar.LENGTH_INDEFINITE) {
|
||||
addSnackbar = (activity as? MainActivity)?.binding?.rootCoordinator?.snack(view.context.getString(R.string.snack_add_to_library)) {
|
||||
setAction(R.string.action_add) {
|
||||
if (!manga.favorite) {
|
||||
addToLibrary(manga)
|
||||
|
@ -365,10 +365,13 @@ class MangaInfoHeaderAdapter(
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateDescription(description: String?, isCurrentlyExpanded: Boolean): CharSequence? {
|
||||
private fun updateDescription(description: String?, isCurrentlyExpanded: Boolean): CharSequence {
|
||||
return when {
|
||||
description.isNullOrBlank() -> view.context.getString(R.string.unknown)
|
||||
isCurrentlyExpanded -> description.replace(Regex("[\\r\\n]{2,}", setOf(RegexOption.MULTILINE)), "\n")
|
||||
isCurrentlyExpanded ->
|
||||
description
|
||||
.replace(Regex(" +\$", setOf(RegexOption.MULTILINE)), "")
|
||||
.replace(Regex("[\\r\\n]{2,}", setOf(RegexOption.MULTILINE)), "\n")
|
||||
else -> description
|
||||
}
|
||||
}
|
||||
|
@ -9,6 +9,8 @@ import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.databinding.LicensesControllerBinding
|
||||
import eu.kanade.tachiyomi.ui.base.controller.BaseController
|
||||
import eu.kanade.tachiyomi.util.lang.launchUI
|
||||
import eu.kanade.tachiyomi.util.lang.withIOContext
|
||||
import eu.kanade.tachiyomi.util.system.openInBrowser
|
||||
|
||||
class LicensesController :
|
||||
@ -30,15 +32,25 @@ class LicensesController :
|
||||
padding()
|
||||
}
|
||||
}
|
||||
binding.progress.applyInsetter {
|
||||
type(navigationBars = true) {
|
||||
padding()
|
||||
}
|
||||
}
|
||||
|
||||
binding.recycler.layoutManager = LinearLayoutManager(view.context)
|
||||
adapter = LicensesAdapter(this)
|
||||
binding.recycler.adapter = adapter
|
||||
|
||||
val licenseItems = Libs(view.context).libraries
|
||||
.sortedBy { it.libraryName.lowercase() }
|
||||
.map { LicensesItem(it) }
|
||||
adapter?.updateDataSet(licenseItems)
|
||||
viewScope.launchUI {
|
||||
val licenseItems = withIOContext {
|
||||
Libs(view.context).libraries
|
||||
.sortedBy { it.libraryName.lowercase() }
|
||||
.map { LicensesItem(it) }
|
||||
}
|
||||
binding.progress.hide()
|
||||
adapter?.updateDataSet(licenseItems)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyView(view: View) {
|
||||
|
@ -27,7 +27,6 @@ import android.view.WindowManager
|
||||
import android.view.animation.Animation
|
||||
import android.view.animation.AnimationUtils
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.SeekBar
|
||||
import android.widget.Toast
|
||||
import androidx.core.graphics.ColorUtils
|
||||
import androidx.core.view.WindowCompat
|
||||
@ -39,6 +38,7 @@ import androidx.core.view.updateLayoutParams
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
|
||||
import com.google.android.material.shape.MaterialShapeDrawable
|
||||
import com.google.android.material.slider.Slider
|
||||
import dev.chrisbanes.insetter.applyInsetter
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
@ -74,7 +74,6 @@ import eu.kanade.tachiyomi.util.system.toast
|
||||
import eu.kanade.tachiyomi.util.view.popupMenu
|
||||
import eu.kanade.tachiyomi.util.view.setTooltip
|
||||
import eu.kanade.tachiyomi.widget.listener.SimpleAnimationListener
|
||||
import eu.kanade.tachiyomi.widget.listener.SimpleSeekBarListener
|
||||
import kotlinx.coroutines.flow.drop
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.merge
|
||||
@ -85,6 +84,7 @@ import timber.log.Timber
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.io.File
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.max
|
||||
|
||||
/**
|
||||
* Activity containing the reader of Tachiyomi. This activity is mostly a container of the
|
||||
@ -328,26 +328,22 @@ class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>()
|
||||
}
|
||||
|
||||
// Init listeners on bottom menu
|
||||
binding.pageSeekbar.setOnSeekBarChangeListener(
|
||||
object : SimpleSeekBarListener() {
|
||||
override fun onStartTrackingTouch(seekBar: SeekBar) {
|
||||
super.onStartTrackingTouch(seekBar)
|
||||
isScrollingThroughPages = true
|
||||
}
|
||||
|
||||
override fun onStopTrackingTouch(seekBar: SeekBar) {
|
||||
super.onStopTrackingTouch(seekBar)
|
||||
isScrollingThroughPages = false
|
||||
}
|
||||
|
||||
override fun onProgressChanged(seekBar: SeekBar, value: Int, fromUser: Boolean) {
|
||||
if (viewer != null && fromUser) {
|
||||
moveToPageIndex(value)
|
||||
binding.pageSeekbar.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY)
|
||||
}
|
||||
}
|
||||
binding.pageSlider.addOnSliderTouchListener(object : Slider.OnSliderTouchListener {
|
||||
override fun onStartTrackingTouch(slider: Slider) {
|
||||
isScrollingThroughPages = true
|
||||
}
|
||||
)
|
||||
|
||||
override fun onStopTrackingTouch(slider: Slider) {
|
||||
isScrollingThroughPages = false
|
||||
}
|
||||
})
|
||||
binding.pageSlider.addOnChangeListener { slider, value, fromUser ->
|
||||
if (viewer != null && fromUser) {
|
||||
isScrollingThroughPages = true
|
||||
moveToPageIndex(value.toInt())
|
||||
slider.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY)
|
||||
}
|
||||
}
|
||||
binding.leftChapter.setOnClickListener {
|
||||
if (viewer != null) {
|
||||
if (viewer is R2LPagerViewer) {
|
||||
@ -600,7 +596,7 @@ class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>()
|
||||
|
||||
binding.toolbar.title = manga.title
|
||||
|
||||
binding.pageSeekbar.isRTL = newViewer is R2LPagerViewer
|
||||
binding.pageSlider.isRTL = newViewer is R2LPagerViewer
|
||||
if (newViewer is R2LPagerViewer) {
|
||||
binding.leftChapter.setTooltip(R.string.action_next_chapter)
|
||||
binding.rightChapter.setTooltip(R.string.action_previous_chapter)
|
||||
@ -724,7 +720,7 @@ class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>()
|
||||
// Set bottom page number
|
||||
binding.pageNumber.text = "${page.number}/${pages.size}"
|
||||
|
||||
// Set seekbar page number
|
||||
// Set page numbers
|
||||
if (viewer !is R2LPagerViewer) {
|
||||
binding.leftPageText.text = "${page.number}"
|
||||
binding.rightPageText.text = "${pages.size}"
|
||||
@ -733,9 +729,10 @@ class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>()
|
||||
binding.leftPageText.text = "${pages.size}"
|
||||
}
|
||||
|
||||
// Set seekbar progress
|
||||
binding.pageSeekbar.max = pages.lastIndex
|
||||
binding.pageSeekbar.progress = page.index
|
||||
// Set slider progress
|
||||
binding.pageSlider.isEnabled = pages.size > 1
|
||||
binding.pageSlider.valueTo = max(pages.lastIndex.toFloat(), 1f)
|
||||
binding.pageSlider.value = page.index.toFloat()
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1,55 +0,0 @@
|
||||
package eu.kanade.tachiyomi.ui.reader
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.ColorStateList
|
||||
import android.graphics.Canvas
|
||||
import android.util.AttributeSet
|
||||
import android.view.MotionEvent
|
||||
import androidx.appcompat.widget.AppCompatSeekBar
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.util.system.getThemeColor
|
||||
|
||||
/**
|
||||
* Seekbar to show current chapter progress.
|
||||
*/
|
||||
class ReaderSeekBar @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null
|
||||
) : AppCompatSeekBar(context, attrs) {
|
||||
|
||||
/**
|
||||
* Whether the seekbar should draw from right to left.
|
||||
*/
|
||||
var isRTL = false
|
||||
|
||||
/**
|
||||
* Draws the seekbar, translating the canvas if using a right to left reader.
|
||||
*/
|
||||
override fun draw(canvas: Canvas) {
|
||||
if (isRTL) {
|
||||
val px = width / 2f
|
||||
val py = height / 2f
|
||||
|
||||
canvas.scale(-1f, 1f, px, py)
|
||||
}
|
||||
super.draw(canvas)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles touch events, translating coordinates if using a right to left reader.
|
||||
*/
|
||||
override fun onTouchEvent(event: MotionEvent): Boolean {
|
||||
if (isRTL) {
|
||||
event.setLocation(width - event.x, event.y)
|
||||
}
|
||||
return super.onTouchEvent(event)
|
||||
}
|
||||
|
||||
init {
|
||||
// Set color to onPrimary when ColoredBars theme is applied
|
||||
if (context.getThemeColor(R.attr.colorToolbar) == context.getThemeColor(R.attr.colorPrimary)) {
|
||||
thumbTintList = ColorStateList.valueOf(context.getThemeColor(R.attr.colorOnPrimary))
|
||||
progressTintList = thumbTintList
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
package eu.kanade.tachiyomi.ui.reader
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import com.google.android.material.slider.Slider
|
||||
|
||||
/**
|
||||
* Slider to show current chapter progress.
|
||||
*/
|
||||
class ReaderSlider @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null
|
||||
) : Slider(context, attrs) {
|
||||
|
||||
init {
|
||||
isTickVisible = false
|
||||
stepSize = 1f
|
||||
setLabelFormatter { value ->
|
||||
(value.toInt() + 1).toString()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the slider should draw from right to left.
|
||||
*/
|
||||
var isRTL: Boolean
|
||||
set(value) {
|
||||
layoutDirection = if (value) LAYOUT_DIRECTION_RTL else LAYOUT_DIRECTION_LTR
|
||||
}
|
||||
get() = layoutDirection == LAYOUT_DIRECTION_RTL
|
||||
}
|
@ -3,7 +3,6 @@ package eu.kanade.tachiyomi.ui.reader.setting
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.widget.SeekBar
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.core.graphics.alpha
|
||||
import androidx.core.graphics.blue
|
||||
@ -15,7 +14,6 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.databinding.ReaderColorFilterSettingsBinding
|
||||
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
|
||||
import eu.kanade.tachiyomi.util.preference.bindToPreference
|
||||
import eu.kanade.tachiyomi.widget.listener.SimpleSeekBarListener
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.sample
|
||||
@ -54,13 +52,13 @@ class ReaderColorFilterSettings @JvmOverloads constructor(context: Context, attr
|
||||
|
||||
// Set brightness value
|
||||
binding.txtBrightnessSeekbarValue.text = brightness.toString()
|
||||
binding.brightnessSeekbar.progress = brightness
|
||||
binding.sliderBrightness.value = brightness.toFloat()
|
||||
|
||||
// Initialize seekBar progress
|
||||
binding.seekbarColorFilterAlpha.progress = argb[0]
|
||||
binding.seekbarColorFilterRed.progress = argb[1]
|
||||
binding.seekbarColorFilterGreen.progress = argb[2]
|
||||
binding.seekbarColorFilterBlue.progress = argb[3]
|
||||
binding.sliderColorFilterAlpha.value = argb[0].toFloat()
|
||||
binding.sliderColorFilterRed.value = argb[1].toFloat()
|
||||
binding.sliderColorFilterGreen.value = argb[2].toFloat()
|
||||
binding.sliderColorFilterBlue.value = argb[3].toFloat()
|
||||
|
||||
// Set listeners
|
||||
binding.switchColorFilter.bindToPreference(preferences.colorFilter())
|
||||
@ -69,55 +67,32 @@ class ReaderColorFilterSettings @JvmOverloads constructor(context: Context, attr
|
||||
binding.grayscale.bindToPreference(preferences.grayscale())
|
||||
binding.invertedColors.bindToPreference(preferences.invertedColors())
|
||||
|
||||
binding.seekbarColorFilterAlpha.setOnSeekBarChangeListener(
|
||||
object : SimpleSeekBarListener() {
|
||||
override fun onProgressChanged(seekBar: SeekBar, value: Int, fromUser: Boolean) {
|
||||
if (fromUser) {
|
||||
setColorValue(value, ALPHA_MASK, 24)
|
||||
}
|
||||
}
|
||||
binding.sliderColorFilterAlpha.addOnChangeListener { _, value, fromUser ->
|
||||
if (fromUser) {
|
||||
setColorValue(value.toInt(), ALPHA_MASK, 24)
|
||||
}
|
||||
)
|
||||
}
|
||||
binding.sliderColorFilterRed.addOnChangeListener { _, value, fromUser ->
|
||||
if (fromUser) {
|
||||
setColorValue(value.toInt(), RED_MASK, 16)
|
||||
}
|
||||
}
|
||||
binding.sliderColorFilterGreen.addOnChangeListener { _, value, fromUser ->
|
||||
if (fromUser) {
|
||||
setColorValue(value.toInt(), GREEN_MASK, 8)
|
||||
}
|
||||
}
|
||||
binding.sliderColorFilterBlue.addOnChangeListener { _, value, fromUser ->
|
||||
if (fromUser) {
|
||||
setColorValue(value.toInt(), BLUE_MASK, 0)
|
||||
}
|
||||
}
|
||||
|
||||
binding.seekbarColorFilterRed.setOnSeekBarChangeListener(
|
||||
object : SimpleSeekBarListener() {
|
||||
override fun onProgressChanged(seekBar: SeekBar, value: Int, fromUser: Boolean) {
|
||||
if (fromUser) {
|
||||
setColorValue(value, RED_MASK, 16)
|
||||
}
|
||||
}
|
||||
binding.sliderBrightness.addOnChangeListener { _, value, fromUser ->
|
||||
if (fromUser) {
|
||||
preferences.customBrightnessValue().set(value.toInt())
|
||||
}
|
||||
)
|
||||
|
||||
binding.seekbarColorFilterGreen.setOnSeekBarChangeListener(
|
||||
object : SimpleSeekBarListener() {
|
||||
override fun onProgressChanged(seekBar: SeekBar, value: Int, fromUser: Boolean) {
|
||||
if (fromUser) {
|
||||
setColorValue(value, GREEN_MASK, 8)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
binding.seekbarColorFilterBlue.setOnSeekBarChangeListener(
|
||||
object : SimpleSeekBarListener() {
|
||||
override fun onProgressChanged(seekBar: SeekBar, value: Int, fromUser: Boolean) {
|
||||
if (fromUser) {
|
||||
setColorValue(value, BLUE_MASK, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
binding.brightnessSeekbar.setOnSeekBarChangeListener(
|
||||
object : SimpleSeekBarListener() {
|
||||
override fun onProgressChanged(seekBar: SeekBar, value: Int, fromUser: Boolean) {
|
||||
if (fromUser) {
|
||||
preferences.customBrightnessValue().set(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -125,10 +100,10 @@ class ReaderColorFilterSettings @JvmOverloads constructor(context: Context, attr
|
||||
* @param enabled determines if seekBar gets enabled
|
||||
*/
|
||||
private fun setColorFilterSeekBar(enabled: Boolean) {
|
||||
binding.seekbarColorFilterRed.isEnabled = enabled
|
||||
binding.seekbarColorFilterGreen.isEnabled = enabled
|
||||
binding.seekbarColorFilterBlue.isEnabled = enabled
|
||||
binding.seekbarColorFilterAlpha.isEnabled = enabled
|
||||
binding.sliderColorFilterRed.isEnabled = enabled
|
||||
binding.sliderColorFilterGreen.isEnabled = enabled
|
||||
binding.sliderColorFilterBlue.isEnabled = enabled
|
||||
binding.sliderColorFilterAlpha.isEnabled = enabled
|
||||
}
|
||||
|
||||
/**
|
||||
@ -136,14 +111,14 @@ class ReaderColorFilterSettings @JvmOverloads constructor(context: Context, attr
|
||||
* @param enabled value which determines if seekBar gets enabled
|
||||
*/
|
||||
private fun setCustomBrightnessSeekBar(enabled: Boolean) {
|
||||
binding.brightnessSeekbar.isEnabled = enabled
|
||||
binding.sliderBrightness.isEnabled = enabled
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the text value's of color filter
|
||||
* @param color integer containing color information
|
||||
*/
|
||||
fun setValues(color: Int): Array<Int> {
|
||||
private fun setValues(color: Int): Array<Int> {
|
||||
val alpha = color.alpha
|
||||
val red = color.red
|
||||
val green = color.green
|
||||
@ -214,21 +189,14 @@ class ReaderColorFilterSettings @JvmOverloads constructor(context: Context, attr
|
||||
* @param mask contains hex mask of chosen color
|
||||
* @param bitShift amounts of bits that gets shifted to receive value
|
||||
*/
|
||||
fun setColorValue(color: Int, mask: Long, bitShift: Int) {
|
||||
private fun setColorValue(color: Int, mask: Long, bitShift: Int) {
|
||||
val currentColor = preferences.colorFilterValue().get()
|
||||
val updatedColor = (color shl bitShift) or (currentColor and mask.inv().toInt())
|
||||
preferences.colorFilterValue().set(updatedColor)
|
||||
}
|
||||
}
|
||||
|
||||
/** Integer mask of alpha value **/
|
||||
private const val ALPHA_MASK: Long = 0xFF000000
|
||||
|
||||
/** Integer mask of red value **/
|
||||
private const val RED_MASK: Long = 0x00FF0000
|
||||
|
||||
/** Integer mask of green value **/
|
||||
private const val GREEN_MASK: Long = 0x0000FF00
|
||||
|
||||
/** Integer mask of blue value **/
|
||||
private const val BLUE_MASK: Long = 0x000000FF
|
||||
|
@ -13,6 +13,8 @@ class UpdatesAdapter(
|
||||
|
||||
var readColor = context.getResourceColor(R.attr.colorOnSurface, 0.38f)
|
||||
var unreadColor = context.getResourceColor(R.attr.colorOnSurface)
|
||||
val unreadColorSecondary = context.getResourceColor(android.R.attr.textColorSecondary)
|
||||
var bookmarkedColor = context.getResourceColor(R.attr.colorAccent)
|
||||
|
||||
val coverClickListener: OnCoverClickListener = controller
|
||||
|
||||
|
@ -180,7 +180,7 @@ class UpdatesController :
|
||||
actionMode!!,
|
||||
R.menu.updates_chapter_selection
|
||||
) { onActionItemClicked(it!!) }
|
||||
(activity as? MainActivity)?.showBottomNav(visible = false, expand = true)
|
||||
(activity as? MainActivity)?.showBottomNav(false)
|
||||
}
|
||||
|
||||
toggleSelection(position)
|
||||
@ -324,6 +324,11 @@ class UpdatesController :
|
||||
presenter.startDownloadingNow(chapter)
|
||||
}
|
||||
|
||||
private fun bookmarkChapters(chapters: List<UpdatesItem>, bookmarked: Boolean) {
|
||||
presenter.bookmarkChapters(chapters, bookmarked)
|
||||
destroyActionModeIfNeeded()
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when ActionMode created.
|
||||
* @param mode the ActionMode object
|
||||
@ -346,6 +351,8 @@ class UpdatesController :
|
||||
val chapters = getSelectedChapters()
|
||||
binding.actionToolbar.findItem(R.id.action_download)?.isVisible = chapters.any { !it.isDownloaded }
|
||||
binding.actionToolbar.findItem(R.id.action_delete)?.isVisible = chapters.any { it.isDownloaded }
|
||||
binding.actionToolbar.findItem(R.id.action_bookmark)?.isVisible = chapters.any { !it.bookmark }
|
||||
binding.actionToolbar.findItem(R.id.action_remove_bookmark)?.isVisible = chapters.all { it.bookmark }
|
||||
binding.actionToolbar.findItem(R.id.action_mark_as_read)?.isVisible = chapters.any { !it.chapter.read }
|
||||
binding.actionToolbar.findItem(R.id.action_mark_as_unread)?.isVisible = chapters.all { it.chapter.read }
|
||||
}
|
||||
@ -370,6 +377,8 @@ class UpdatesController :
|
||||
R.id.action_delete ->
|
||||
ConfirmDeleteChaptersDialog(this, getSelectedChapters())
|
||||
.showDialog(router)
|
||||
R.id.action_bookmark -> bookmarkChapters(getSelectedChapters(), true)
|
||||
R.id.action_remove_bookmark -> bookmarkChapters(getSelectedChapters(), false)
|
||||
R.id.action_mark_as_read -> markAsRead(getSelectedChapters())
|
||||
R.id.action_mark_as_unread -> markAsUnread(getSelectedChapters())
|
||||
else -> return false
|
||||
@ -386,7 +395,7 @@ class UpdatesController :
|
||||
adapter?.clearSelection()
|
||||
|
||||
binding.actionToolbar.hide()
|
||||
(activity as? MainActivity)?.showBottomNav(visible = true, expand = true)
|
||||
(activity as? MainActivity)?.showBottomNav(true)
|
||||
|
||||
actionMode = null
|
||||
}
|
||||
|
@ -39,15 +39,20 @@ class UpdatesHolder(private val view: View, private val adapter: UpdatesAdapter)
|
||||
// Set manga title
|
||||
binding.mangaTitle.text = item.manga.title
|
||||
|
||||
// Check if chapter is read and set correct color
|
||||
// Check if chapter is read and/or bookmarked and set correct color
|
||||
if (item.chapter.read) {
|
||||
binding.chapterTitle.setTextColor(adapter.readColor)
|
||||
binding.mangaTitle.setTextColor(adapter.readColor)
|
||||
} else {
|
||||
binding.chapterTitle.setTextColor(adapter.unreadColor)
|
||||
binding.mangaTitle.setTextColor(adapter.unreadColor)
|
||||
binding.chapterTitle.setTextColor(
|
||||
if (item.bookmark) adapter.bookmarkedColor else adapter.unreadColorSecondary
|
||||
)
|
||||
}
|
||||
|
||||
// Set bookmark status
|
||||
binding.bookmarkIcon.isVisible = item.bookmark
|
||||
|
||||
// Set chapter status
|
||||
binding.download.isVisible = item.manga.source != LocalSource.ID
|
||||
binding.download.setState(item.status, item.progress)
|
||||
|
@ -180,6 +180,22 @@ class UpdatesPresenter : BasePresenter<UpdatesController>() {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark selected chapters as bookmarked
|
||||
* @param items list of selected chapters
|
||||
* @param bookmarked bookmark status
|
||||
*/
|
||||
fun bookmarkChapters(items: List<UpdatesItem>, bookmarked: Boolean) {
|
||||
val chapters = items.map { it.chapter }
|
||||
chapters.forEach {
|
||||
it.bookmark = bookmarked
|
||||
}
|
||||
|
||||
Observable.fromCallable { db.updateChaptersProgress(chapters).executeAsBlocking() }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe()
|
||||
}
|
||||
|
||||
/**
|
||||
* Download selected chapters
|
||||
* @param items list of recent chapters seleted.
|
||||
|
@ -36,6 +36,8 @@ 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.MiuiUtil
|
||||
import eu.kanade.tachiyomi.util.system.isPackageInstalled
|
||||
import eu.kanade.tachiyomi.util.system.isTablet
|
||||
import eu.kanade.tachiyomi.util.system.powerManager
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
@ -187,6 +189,45 @@ class SettingsAdvancedController : SettingsController() {
|
||||
}
|
||||
}
|
||||
|
||||
preferenceCategory {
|
||||
titleRes = R.string.label_extensions
|
||||
|
||||
listPreference {
|
||||
key = Keys.extensionInstaller
|
||||
titleRes = R.string.ext_installer_pref
|
||||
summary = "%s"
|
||||
entriesRes = arrayOf(
|
||||
R.string.ext_installer_legacy,
|
||||
R.string.ext_installer_packageinstaller,
|
||||
R.string.ext_installer_shizuku,
|
||||
)
|
||||
entryValues = PreferenceValues.ExtensionInstaller.values().map { it.name }.toTypedArray()
|
||||
defaultValue = if (MiuiUtil.isMiui()) {
|
||||
PreferenceValues.ExtensionInstaller.LEGACY
|
||||
} else {
|
||||
PreferenceValues.ExtensionInstaller.PACKAGEINSTALLER
|
||||
}.name
|
||||
|
||||
onChange {
|
||||
if (it == PreferenceValues.ExtensionInstaller.SHIZUKU.name &&
|
||||
!context.isPackageInstalled("moe.shizuku.privileged.api")
|
||||
) {
|
||||
MaterialAlertDialogBuilder(context)
|
||||
.setTitle(R.string.ext_installer_shizuku)
|
||||
.setMessage(R.string.ext_installer_shizuku_unavailable_dialog)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
openInBrowser("https://shizuku.rikka.app/download")
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show()
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
preferenceCategory {
|
||||
titleRes = R.string.pref_category_display
|
||||
|
||||
|
@ -2,7 +2,6 @@ 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.util.preference.defaultValue
|
||||
import eu.kanade.tachiyomi.util.preference.infoPreference
|
||||
@ -11,7 +10,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 kotlinx.coroutines.flow.launchIn
|
||||
import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys
|
||||
|
||||
class SettingsBrowseController : SettingsController() {
|
||||
@ -54,18 +52,6 @@ class SettingsBrowseController : SettingsController() {
|
||||
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(viewScope)
|
||||
}
|
||||
|
||||
infoPreference(R.string.parental_controls_info)
|
||||
}
|
||||
|
@ -68,6 +68,9 @@ abstract class SettingsController : PreferenceController() {
|
||||
animatePreferenceHighlight(it.itemView)
|
||||
}
|
||||
}
|
||||
|
||||
// Explicitly clear it to avoid re-scrolling/animating on activity recreations
|
||||
preferenceKey = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -282,11 +282,6 @@ class SettingsLibraryController : SettingsController() {
|
||||
defaultValue = false
|
||||
}
|
||||
}
|
||||
switchPreference {
|
||||
key = Keys.showLibraryUpdateErrors
|
||||
titleRes = R.string.pref_library_update_error_notification
|
||||
defaultValue = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -8,6 +8,7 @@ import android.view.MenuItem
|
||||
import android.view.View
|
||||
import androidx.appcompat.widget.SearchView
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import dev.chrisbanes.insetter.applyInsetter
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.databinding.SettingsSearchControllerBinding
|
||||
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
|
||||
@ -25,7 +26,7 @@ class SettingsSearchController :
|
||||
/**
|
||||
* Adapter containing search results grouped by lang.
|
||||
*/
|
||||
protected var adapter: SettingsSearchAdapter? = null
|
||||
private var adapter: SettingsSearchAdapter? = null
|
||||
private lateinit var searchView: SearchView
|
||||
|
||||
init {
|
||||
@ -54,15 +55,18 @@ class SettingsSearchController :
|
||||
* @param inflater used to load the menu xml.
|
||||
*/
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
// Inflate menu.
|
||||
inflater.inflate(R.menu.settings_main, menu)
|
||||
|
||||
binding.recycler.applyInsetter {
|
||||
type(navigationBars = true) {
|
||||
padding()
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize search menu
|
||||
val searchItem = menu.findItem(R.id.action_search)
|
||||
searchView = searchItem.actionView as SearchView
|
||||
searchView.maxWidth = Int.MAX_VALUE
|
||||
|
||||
// Change hint to show "search settings."
|
||||
searchView.queryHint = applicationContext?.getString(R.string.action_search_settings)
|
||||
|
||||
searchItem.expandActionView()
|
||||
@ -102,8 +106,6 @@ class SettingsSearchController :
|
||||
super.onViewCreated(view)
|
||||
|
||||
adapter = SettingsSearchAdapter(this)
|
||||
|
||||
// Create recycler and set adapter.
|
||||
binding.recycler.layoutManager = LinearLayoutManager(view.context)
|
||||
binding.recycler.adapter = adapter
|
||||
|
||||
|
@ -0,0 +1,31 @@
|
||||
package eu.kanade.tachiyomi.util.lang
|
||||
|
||||
import java.io.Closeable
|
||||
|
||||
/**
|
||||
* Executes the given block function on this resources and then closes it down correctly whether an exception is
|
||||
* thrown or not.
|
||||
*
|
||||
* @param block a function to process with given Closeable resources.
|
||||
* @return the result of block function invoked on this resource.
|
||||
*/
|
||||
inline fun <T : Closeable?> Array<T>.use(block: () -> Unit) {
|
||||
var blockException: Throwable? = null
|
||||
try {
|
||||
return block()
|
||||
} catch (e: Throwable) {
|
||||
blockException = e
|
||||
throw e
|
||||
} finally {
|
||||
when (blockException) {
|
||||
null -> forEach { it?.close() }
|
||||
else -> forEach {
|
||||
try {
|
||||
it?.close()
|
||||
} catch (closeException: Throwable) {
|
||||
blockException.addSuppressed(closeException)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -94,6 +94,7 @@ fun initDialog(dialogPreference: DialogPreference) {
|
||||
inline fun <P : Preference> PreferenceGroup.add(p: P): P {
|
||||
return p.apply {
|
||||
this.isIconSpaceReserved = false
|
||||
this.isSingleLineTitle = false
|
||||
addPreference(this)
|
||||
}
|
||||
}
|
||||
@ -102,6 +103,7 @@ inline fun <P : Preference> PreferenceGroup.initThenAdd(p: P, block: P.() -> Uni
|
||||
return p.apply {
|
||||
block()
|
||||
this.isIconSpaceReserved = false
|
||||
this.isSingleLineTitle = false
|
||||
addPreference(this)
|
||||
}
|
||||
}
|
||||
@ -109,6 +111,7 @@ inline fun <P : Preference> PreferenceGroup.initThenAdd(p: P, block: P.() -> Uni
|
||||
inline fun <P : Preference> PreferenceGroup.addThenInit(p: P, block: P.() -> Unit): P {
|
||||
return p.apply {
|
||||
this.isIconSpaceReserved = false
|
||||
this.isSingleLineTitle = false
|
||||
addPreference(this)
|
||||
block()
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
package eu.kanade.tachiyomi.util.system
|
||||
|
||||
import android.content.Context
|
||||
import android.view.ViewPropertyAnimator
|
||||
import android.view.animation.Animation
|
||||
import androidx.constraintlayout.motion.widget.MotionScene.Transition
|
||||
|
||||
@ -14,3 +15,8 @@ fun Transition.applySystemAnimatorScale(context: Context) {
|
||||
// End layout of cover expanding animation tends to break when the transition is less than ~25ms
|
||||
this.duration = (this.duration * context.animatorDurationScale).toInt().coerceAtLeast(25)
|
||||
}
|
||||
|
||||
/** Scale the duration of this [ViewPropertyAnimator] by [Context.animatorDurationScale] */
|
||||
fun ViewPropertyAnimator.applySystemAnimatorScale(context: Context): ViewPropertyAnimator = apply {
|
||||
this.duration = (this.duration * context.animatorDurationScale).toLong()
|
||||
}
|
||||
|
@ -41,6 +41,7 @@ import androidx.core.graphics.green
|
||||
import androidx.core.graphics.red
|
||||
import androidx.core.net.toUri
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||
import com.hippo.unifile.UniFile
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.preference.PreferenceValues
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
@ -368,12 +369,51 @@ fun Context.createReaderThemeContext(): Context {
|
||||
}
|
||||
|
||||
fun Context.isOnline(): Boolean {
|
||||
val networkCapabilities = connectivityManager.activeNetwork ?: return false
|
||||
val actNw = connectivityManager.getNetworkCapabilities(networkCapabilities) ?: return false
|
||||
val activeNetwork = connectivityManager.activeNetwork ?: return false
|
||||
val networkCapabilities = connectivityManager.getNetworkCapabilities(activeNetwork) ?: return false
|
||||
val maxTransport = when {
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1 -> NetworkCapabilities.TRANSPORT_LOWPAN
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.O -> NetworkCapabilities.TRANSPORT_WIFI_AWARE
|
||||
else -> NetworkCapabilities.TRANSPORT_VPN
|
||||
}
|
||||
return (NetworkCapabilities.TRANSPORT_CELLULAR..maxTransport).any(actNw::hasTransport)
|
||||
return (NetworkCapabilities.TRANSPORT_CELLULAR..maxTransport).any(networkCapabilities::hasTransport)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if device is connected to Wifi.
|
||||
*/
|
||||
fun Context.isConnectedToWifi(): Boolean {
|
||||
if (!wifiManager.isWifiEnabled) return false
|
||||
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
val activeNetwork = connectivityManager.activeNetwork ?: return false
|
||||
val networkCapabilities = connectivityManager.getNetworkCapabilities(activeNetwork) ?: return false
|
||||
|
||||
networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) &&
|
||||
networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
wifiManager.connectionInfo.bssid != null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets document size of provided [Uri]
|
||||
*
|
||||
* @return document size of [uri] or null if size can't be obtained
|
||||
*/
|
||||
fun Context.getUriSize(uri: Uri): Long? {
|
||||
return UniFile.fromUri(this, uri).length().takeIf { it >= 0 }
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if [packageName] is installed.
|
||||
*/
|
||||
fun Context.isPackageInstalled(packageName: String): Boolean {
|
||||
return try {
|
||||
packageManager.getApplicationInfo(packageName, 0)
|
||||
true
|
||||
} catch (e: PackageManager.NameNotFoundException) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
@ -49,7 +49,7 @@ fun View.getCoordinates() = Point((left + right) / 2, (top + bottom) / 2)
|
||||
*/
|
||||
inline fun View.snack(
|
||||
message: String,
|
||||
length: Int = Snackbar.LENGTH_LONG,
|
||||
length: Int = 10_000,
|
||||
f: Snackbar.() -> Unit = {}
|
||||
): Snackbar {
|
||||
val snack = Snackbar.make(this, message, length)
|
||||
|
@ -5,10 +5,12 @@ import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.FloatRange
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import androidx.lifecycle.coroutineScope
|
||||
import androidx.lifecycle.findViewTreeLifecycleOwner
|
||||
import com.google.android.material.animation.AnimationUtils
|
||||
import com.google.android.material.appbar.AppBarLayout
|
||||
import com.google.android.material.appbar.HideToolbarOnScrollBehavior
|
||||
import com.google.android.material.appbar.MaterialToolbar
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.util.view.findChild
|
||||
@ -51,6 +53,8 @@ class ElevationAppBarLayout @JvmOverloads constructor(
|
||||
}
|
||||
}
|
||||
|
||||
override fun getBehavior(): CoordinatorLayout.Behavior<AppBarLayout> = HideToolbarOnScrollBehavior()
|
||||
|
||||
/**
|
||||
* Disabled. Lift on scroll is handled manually with [TachiyomiCoordinatorLayout]
|
||||
*/
|
||||
|
@ -1,11 +1,19 @@
|
||||
package eu.kanade.tachiyomi.widget
|
||||
|
||||
import android.animation.ValueAnimator
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.animation.DecelerateInterpolator
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import androidx.core.view.ViewCompat
|
||||
import com.google.android.material.appbar.AppBarLayout
|
||||
import com.google.android.material.bottomnavigation.BottomNavigationView
|
||||
import eu.kanade.tachiyomi.util.system.animatorDurationScale
|
||||
import eu.kanade.tachiyomi.util.view.findChild
|
||||
import kotlin.math.roundToLong
|
||||
|
||||
/**
|
||||
* Hide behavior similar to app bar for [BottomNavigationView]
|
||||
@ -15,6 +23,31 @@ class HideBottomNavigationOnScrollBehavior @JvmOverloads constructor(
|
||||
attrs: AttributeSet? = null
|
||||
) : CoordinatorLayout.Behavior<BottomNavigationView>(context, attrs) {
|
||||
|
||||
@ViewCompat.NestedScrollType
|
||||
private var lastStartedType: Int = 0
|
||||
|
||||
private var offsetAnimator: ValueAnimator? = null
|
||||
|
||||
private var dyRatio = 1F
|
||||
|
||||
override fun layoutDependsOn(parent: CoordinatorLayout, child: BottomNavigationView, dependency: View): Boolean {
|
||||
return dependency is AppBarLayout
|
||||
}
|
||||
|
||||
override fun onDependentViewChanged(
|
||||
parent: CoordinatorLayout,
|
||||
child: BottomNavigationView,
|
||||
dependency: View
|
||||
): Boolean {
|
||||
val toolbarSize = (dependency as ViewGroup).findChild<Toolbar>()?.height ?: 0
|
||||
dyRatio = if (toolbarSize > 0) {
|
||||
child.height.toFloat() / toolbarSize
|
||||
} else {
|
||||
1F
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onStartNestedScroll(
|
||||
coordinatorLayout: CoordinatorLayout,
|
||||
child: BottomNavigationView,
|
||||
@ -23,7 +56,12 @@ class HideBottomNavigationOnScrollBehavior @JvmOverloads constructor(
|
||||
axes: Int,
|
||||
type: Int
|
||||
): Boolean {
|
||||
return axes == ViewCompat.SCROLL_AXIS_VERTICAL
|
||||
if (axes != ViewCompat.SCROLL_AXIS_VERTICAL) {
|
||||
return false
|
||||
}
|
||||
lastStartedType = type
|
||||
offsetAnimator?.cancel()
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onNestedPreScroll(
|
||||
@ -36,6 +74,33 @@ class HideBottomNavigationOnScrollBehavior @JvmOverloads constructor(
|
||||
type: Int
|
||||
) {
|
||||
super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed, type)
|
||||
child.translationY = (child.translationY + dy).coerceIn(0F, child.height.toFloat())
|
||||
child.translationY = (child.translationY + (dy * dyRatio)).coerceIn(0F, child.height.toFloat())
|
||||
}
|
||||
|
||||
override fun onStopNestedScroll(
|
||||
coordinatorLayout: CoordinatorLayout,
|
||||
child: BottomNavigationView,
|
||||
target: View,
|
||||
type: Int
|
||||
) {
|
||||
if (lastStartedType == ViewCompat.TYPE_TOUCH || type == ViewCompat.TYPE_NON_TOUCH) {
|
||||
animateBottomNavigationVisibility(child, child.translationY < child.height / 2)
|
||||
}
|
||||
}
|
||||
|
||||
private fun animateBottomNavigationVisibility(child: BottomNavigationView, isVisible: Boolean) {
|
||||
offsetAnimator?.cancel()
|
||||
offsetAnimator = ValueAnimator().apply {
|
||||
interpolator = DecelerateInterpolator()
|
||||
duration = (150 * child.context.animatorDurationScale).roundToLong()
|
||||
addUpdateListener {
|
||||
child.translationY = it.animatedValue as Float
|
||||
}
|
||||
}
|
||||
offsetAnimator?.setFloatValues(
|
||||
child.translationY,
|
||||
if (isVisible) 0F else child.height.toFloat()
|
||||
)
|
||||
offsetAnimator?.start()
|
||||
}
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.MotionEvent
|
||||
import androidx.core.view.ViewCompat
|
||||
import dev.chrisbanes.insetter.applyInsetter
|
||||
import eu.davidea.fastscroller.FastScroller
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.util.system.dpToPxEnd
|
||||
@ -21,6 +22,12 @@ class MaterialFastScroll @JvmOverloads constructor(context: Context, attrs: Attr
|
||||
)
|
||||
autoHideEnabled = true
|
||||
ignoreTouchesOutsideHandle = true
|
||||
|
||||
applyInsetter {
|
||||
type(navigationBars = true) {
|
||||
margin()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Overridden to handle RTL
|
||||
|
@ -1,74 +0,0 @@
|
||||
package eu.kanade.tachiyomi.widget
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Parcelable
|
||||
import android.util.AttributeSet
|
||||
import android.widget.SeekBar
|
||||
import androidx.appcompat.widget.AppCompatSeekBar
|
||||
import eu.kanade.tachiyomi.R
|
||||
import kotlin.math.abs
|
||||
|
||||
class NegativeSeekBar @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
|
||||
AppCompatSeekBar(context, attrs) {
|
||||
|
||||
private var minValue: Int = 0
|
||||
private var maxValue: Int = 0
|
||||
private var listener: OnSeekBarChangeListener? = null
|
||||
|
||||
init {
|
||||
val styledAttributes = context.obtainStyledAttributes(
|
||||
attrs,
|
||||
R.styleable.NegativeSeekBar,
|
||||
0,
|
||||
0
|
||||
)
|
||||
|
||||
try {
|
||||
setMinSeek(styledAttributes.getInt(R.styleable.NegativeSeekBar_min_seek, 0))
|
||||
setMaxSeek(styledAttributes.getInt(R.styleable.NegativeSeekBar_max_seek, 0))
|
||||
} finally {
|
||||
styledAttributes.recycle()
|
||||
}
|
||||
|
||||
super.setOnSeekBarChangeListener(
|
||||
object : OnSeekBarChangeListener {
|
||||
override fun onProgressChanged(seekBar: SeekBar?, value: Int, fromUser: Boolean) {
|
||||
listener?.onProgressChanged(seekBar, minValue + value, fromUser)
|
||||
}
|
||||
|
||||
override fun onStartTrackingTouch(p0: SeekBar?) {
|
||||
listener?.onStartTrackingTouch(p0)
|
||||
}
|
||||
|
||||
override fun onStopTrackingTouch(p0: SeekBar?) {
|
||||
listener?.onStopTrackingTouch(p0)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override fun setProgress(progress: Int) {
|
||||
super.setProgress(abs(minValue) + progress)
|
||||
}
|
||||
|
||||
fun setMinSeek(minValue: Int) {
|
||||
this.minValue = minValue
|
||||
max = (this.maxValue - this.minValue)
|
||||
}
|
||||
|
||||
fun setMaxSeek(maxValue: Int) {
|
||||
this.maxValue = maxValue
|
||||
max = (this.maxValue - this.minValue)
|
||||
}
|
||||
|
||||
override fun setOnSeekBarChangeListener(listener: OnSeekBarChangeListener?) {
|
||||
this.listener = listener
|
||||
}
|
||||
|
||||
override fun onRestoreInstanceState(state: Parcelable?) {
|
||||
// We can't restore the progress from the saved state because it gets shifted.
|
||||
val origProgress = progress
|
||||
super.onRestoreInstanceState(state)
|
||||
super.setProgress(origProgress)
|
||||
}
|
||||
}
|
@ -8,7 +8,7 @@ import java.util.Stack
|
||||
|
||||
abstract class RecyclerViewPagerAdapter : ViewStatePagerAdapter() {
|
||||
|
||||
private val pool = Stack<View>()
|
||||
private val pool = HashMap<Int, Stack<View>>()
|
||||
|
||||
var recycle = true
|
||||
set(value) {
|
||||
@ -16,17 +16,20 @@ abstract class RecyclerViewPagerAdapter : ViewStatePagerAdapter() {
|
||||
field = value
|
||||
}
|
||||
|
||||
protected abstract fun createView(container: ViewGroup): View
|
||||
protected abstract fun getViewType(position: Int): Int
|
||||
|
||||
protected abstract fun inflateView(container: ViewGroup, viewType: Int): View
|
||||
|
||||
protected abstract fun bindView(view: View, position: Int)
|
||||
|
||||
protected open fun recycleView(view: View, position: Int) {}
|
||||
|
||||
override fun createView(container: ViewGroup, position: Int): View {
|
||||
val view = if (pool.isNotEmpty()) {
|
||||
pool.pop().setViewPagerPositionParam(position)
|
||||
val viewType = getViewType(position)
|
||||
val view = if (pool[viewType] != null && pool[viewType]!!.isNotEmpty()) {
|
||||
pool[viewType]!!.pop().setViewPagerPositionParam(position)
|
||||
} else {
|
||||
createView(container)
|
||||
inflateView(container, viewType)
|
||||
}
|
||||
bindView(view, position)
|
||||
return view
|
||||
@ -34,7 +37,9 @@ abstract class RecyclerViewPagerAdapter : ViewStatePagerAdapter() {
|
||||
|
||||
override fun destroyView(container: ViewGroup, position: Int, view: View) {
|
||||
recycleView(view, position)
|
||||
if (recycle) pool.push(view)
|
||||
val viewType = getViewType(position)
|
||||
if (pool[viewType] == null) pool[viewType] = Stack<View>()
|
||||
if (recycle) pool[viewType]!!.push(view)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -0,0 +1,174 @@
|
||||
package eu.kanade.tachiyomi.widget
|
||||
|
||||
import android.animation.Animator
|
||||
import android.animation.AnimatorListenerAdapter
|
||||
import android.animation.TimeInterpolator
|
||||
import android.content.Context
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import android.util.AttributeSet
|
||||
import android.view.ViewPropertyAnimator
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import androidx.core.view.doOnLayout
|
||||
import androidx.core.view.doOnNextLayout
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.customview.view.AbsSavedState
|
||||
import androidx.interpolator.view.animation.FastOutLinearInInterpolator
|
||||
import androidx.interpolator.view.animation.LinearOutSlowInInterpolator
|
||||
import androidx.lifecycle.findViewTreeLifecycleOwner
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.google.android.material.bottomnavigation.BottomNavigationView
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.preference.asImmediateFlow
|
||||
import eu.kanade.tachiyomi.util.system.applySystemAnimatorScale
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
class TachiyomiBottomNavigationView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = R.attr.bottomNavigationStyle,
|
||||
defStyleRes: Int = R.style.Widget_Design_BottomNavigationView
|
||||
) : BottomNavigationView(context, attrs, defStyleAttr, defStyleRes) {
|
||||
|
||||
private var currentAnimator: ViewPropertyAnimator? = null
|
||||
|
||||
private var currentState = STATE_UP
|
||||
|
||||
init {
|
||||
// Hide on scroll
|
||||
doOnLayout {
|
||||
findViewTreeLifecycleOwner()?.lifecycleScope?.let { scope ->
|
||||
Injekt.get<PreferencesHelper>().hideBottomBarOnScroll()
|
||||
.asImmediateFlow {
|
||||
updateLayoutParams<CoordinatorLayout.LayoutParams> {
|
||||
behavior = if (it) {
|
||||
HideBottomNavigationOnScrollBehavior()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
.launchIn(scope)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(): Parcelable {
|
||||
val superState = super.onSaveInstanceState()
|
||||
return SavedState(superState).also {
|
||||
it.currentState = currentState
|
||||
}
|
||||
}
|
||||
|
||||
override fun onRestoreInstanceState(state: Parcelable?) {
|
||||
if (state is SavedState) {
|
||||
super.onRestoreInstanceState(state.superState)
|
||||
doOnNextLayout {
|
||||
if (state.currentState == STATE_UP) {
|
||||
slideUp(animate = false)
|
||||
} else if (state.currentState == STATE_DOWN) {
|
||||
slideDown(animate = false)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
super.onRestoreInstanceState(state)
|
||||
}
|
||||
}
|
||||
|
||||
override fun setTranslationY(translationY: Float) {
|
||||
// Disallow translation change when state down
|
||||
if (currentState == STATE_DOWN) return
|
||||
super.setTranslationY(translationY)
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows this view up.
|
||||
*
|
||||
* @param animate True if slide up should be animated
|
||||
*/
|
||||
fun slideUp(animate: Boolean = true) {
|
||||
currentAnimator?.cancel()
|
||||
clearAnimation()
|
||||
|
||||
currentState = STATE_UP
|
||||
animateTranslation(
|
||||
0F,
|
||||
if (animate) SLIDE_UP_ANIMATION_DURATION else 0,
|
||||
LinearOutSlowInInterpolator()
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Hides this view down. [setTranslationY] won't work until [slideUp] is called.
|
||||
*
|
||||
* @param animate True if slide down should be animated
|
||||
*/
|
||||
fun slideDown(animate: Boolean = true) {
|
||||
currentAnimator?.cancel()
|
||||
clearAnimation()
|
||||
|
||||
currentState = STATE_DOWN
|
||||
animateTranslation(
|
||||
height.toFloat(),
|
||||
if (animate) SLIDE_DOWN_ANIMATION_DURATION else 0,
|
||||
FastOutLinearInInterpolator()
|
||||
)
|
||||
}
|
||||
|
||||
private fun animateTranslation(targetY: Float, duration: Long, interpolator: TimeInterpolator) {
|
||||
currentAnimator = animate()
|
||||
.translationY(targetY)
|
||||
.setInterpolator(interpolator)
|
||||
.setDuration(duration)
|
||||
.applySystemAnimatorScale(context)
|
||||
.setListener(object : AnimatorListenerAdapter() {
|
||||
override fun onAnimationEnd(animation: Animator?) {
|
||||
currentAnimator = null
|
||||
postInvalidate()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
internal class SavedState : AbsSavedState {
|
||||
var currentState = STATE_UP
|
||||
|
||||
constructor(superState: Parcelable) : super(superState)
|
||||
|
||||
constructor(source: Parcel, loader: ClassLoader?) : super(source, loader) {
|
||||
currentState = source.readByte().toInt()
|
||||
}
|
||||
|
||||
override fun writeToParcel(out: Parcel, flags: Int) {
|
||||
super.writeToParcel(out, flags)
|
||||
out.writeByte(currentState.toByte())
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmField
|
||||
val CREATOR: Parcelable.ClassLoaderCreator<SavedState> = object : Parcelable.ClassLoaderCreator<SavedState> {
|
||||
override fun createFromParcel(source: Parcel, loader: ClassLoader): SavedState {
|
||||
return SavedState(source, loader)
|
||||
}
|
||||
|
||||
override fun createFromParcel(source: Parcel): SavedState {
|
||||
return SavedState(source, null)
|
||||
}
|
||||
|
||||
override fun newArray(size: Int): Array<SavedState> {
|
||||
return newArray(size)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val STATE_DOWN = 1
|
||||
private const val STATE_UP = 2
|
||||
|
||||
private const val SLIDE_UP_ANIMATION_DURATION = 225L
|
||||
private const val SLIDE_DOWN_ANIMATION_DURATION = 175L
|
||||
}
|
||||
}
|
@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.widget
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import androidx.appcompat.widget.SearchView
|
||||
import androidx.core.view.inputmethod.EditorInfoCompat
|
||||
import eu.kanade.tachiyomi.R
|
||||
@ -39,6 +40,18 @@ class TachiyomiSearchView @JvmOverloads constructor(
|
||||
}.launchIn(scope!!)
|
||||
}
|
||||
|
||||
override fun setOnQueryTextListener(listener: OnQueryTextListener?) {
|
||||
super.setOnQueryTextListener(listener)
|
||||
val searchAutoComplete: SearchAutoComplete = findViewById(R.id.search_src_text)
|
||||
searchAutoComplete.setOnEditorActionListener { _, actionID, _ ->
|
||||
if (actionID == EditorInfo.IME_ACTION_SEARCH || actionID == EditorInfo.IME_NULL) {
|
||||
clearFocus()
|
||||
listener?.onQueryTextSubmit(query.toString())
|
||||
true
|
||||
} else false
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow()
|
||||
scope?.cancel()
|
||||
|
@ -1,14 +0,0 @@
|
||||
package eu.kanade.tachiyomi.widget.listener
|
||||
|
||||
import android.widget.SeekBar
|
||||
|
||||
open class SimpleSeekBarListener : SeekBar.OnSeekBarChangeListener {
|
||||
override fun onProgressChanged(seekBar: SeekBar, value: Int, fromUser: Boolean) {
|
||||
}
|
||||
|
||||
override fun onStartTrackingTouch(seekBar: SeekBar) {
|
||||
}
|
||||
|
||||
override fun onStopTrackingTouch(seekBar: SeekBar) {
|
||||
}
|
||||
}
|
Binary file not shown.
@ -64,29 +64,31 @@
|
||||
|
||||
<TextView
|
||||
android:id="@+id/manga_full_title"
|
||||
style="@style/TextAppearance.Medium.Title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:gravity="center"
|
||||
android:text="@string/manga_info_full_title_label"
|
||||
android:textAppearance="?attr/textAppearanceHeadline6"
|
||||
android:textIsSelectable="false" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/manga_author"
|
||||
style="@style/TextAppearance.Regular.Body1.Secondary.Bold"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="?attr/textAppearanceSubtitle2"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:textAlignment="center"
|
||||
android:textIsSelectable="false"
|
||||
tools:text="Author" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/manga_artist"
|
||||
style="@style/TextAppearance.Regular.Body1.Secondary.Bold"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:textAppearance="?attr/textAppearanceSubtitle2"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:textIsSelectable="false"
|
||||
tools:text="Artist" />
|
||||
|
||||
@ -106,31 +108,34 @@
|
||||
|
||||
<TextView
|
||||
android:id="@+id/manga_status"
|
||||
style="@style/TextAppearance.Regular.Body1.Secondary"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:textAppearance="?attr/textAppearanceBody2"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:textIsSelectable="false"
|
||||
tools:text="Status" />
|
||||
|
||||
<TextView
|
||||
style="@style/TextAppearance.Regular.Body1.Secondary"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:text="•"
|
||||
android:textAppearance="?attr/textAppearanceBody2"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:textIsSelectable="false"
|
||||
tools:ignore="HardcodedText" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/manga_source"
|
||||
style="@style/TextAppearance.Regular.Body1.Secondary"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:textAppearance="?attr/textAppearanceBody2"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:textIsSelectable="false"
|
||||
tools:text="Source" />
|
||||
|
||||
@ -194,7 +199,6 @@
|
||||
|
||||
<TextView
|
||||
android:id="@+id/manga_summary_text"
|
||||
style="@style/TextAppearance.Regular.Body1.Secondary"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
@ -203,6 +207,8 @@
|
||||
android:ellipsize="end"
|
||||
android:focusable="true"
|
||||
android:maxLines="3"
|
||||
android:textAppearance="?attr/textAppearanceBody2"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:textIsSelectable="false"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
|
@ -33,7 +33,7 @@
|
||||
android:layout_weight="1"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:textAppearance="@style/TextAppearance.Regular.SubHeading"
|
||||
android:textAppearance="?attr/textAppearanceBody2"
|
||||
tools:text="Category Title" />
|
||||
|
||||
</LinearLayout>
|
||||
|
@ -3,8 +3,10 @@
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="?android:attr/listPreferredItemHeight"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/list_item_selector_background"
|
||||
android:minHeight="?android:attr/listPreferredItemHeight"
|
||||
android:paddingVertical="10dp"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="5dp">
|
||||
|
||||
@ -12,41 +14,44 @@
|
||||
android:id="@+id/bookmark_icon"
|
||||
android:layout_width="16dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginTop="12dp"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:contentDescription="@string/action_filter_bookmarked"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="@id/chapter_title"
|
||||
app:layout_constraintEnd_toStartOf="@id/chapter_title"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@+id/chapter_title"
|
||||
app:srcCompat="@drawable/ic_bookmark_24dp"
|
||||
app:tint="?attr/colorAccent"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/chapter_title"
|
||||
style="@style/TextAppearance.Regular.Body1"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:textAppearance="?attr/textAppearanceBody2"
|
||||
app:layout_constraintBottom_toTopOf="@+id/chapter_description"
|
||||
app:layout_constraintEnd_toStartOf="@+id/download"
|
||||
app:layout_constraintStart_toEndOf="@+id/bookmark_icon"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_chainStyle="packed"
|
||||
tools:text="Title" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/chapter_description"
|
||||
style="@style/TextAppearance.Regular.Caption"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:layout_marginTop="6dp"
|
||||
android:ellipsize="end"
|
||||
android:singleLine="true"
|
||||
android:textAppearance="?attr/textAppearanceBody2"
|
||||
android:textSize="12sp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/download"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/chapter_title"
|
||||
tools:text="22/02/2016 • Scanlator • Page: 45" />
|
||||
|
||||
<eu.kanade.tachiyomi.ui.manga.chapter.ChapterDownloadView
|
||||
|
@ -10,7 +10,7 @@
|
||||
android:id="@+id/description"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="@style/TextAppearance.Regular.Body1" />
|
||||
android:textAppearance="?attr/textAppearanceBody2" />
|
||||
|
||||
<CheckBox
|
||||
android:id="@+id/checkbox_option"
|
||||
|
@ -9,20 +9,22 @@
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_face"
|
||||
style="@style/TextAppearance.Medium.Body2.Hint"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:textAppearance="?attr/textAppearanceBody2"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:textSize="48sp"
|
||||
tools:text="-_-" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_label"
|
||||
style="@style/TextAppearance.Medium.Body2.Hint"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="16dp"
|
||||
android:gravity="center"
|
||||
android:textAppearance="?attr/textAppearanceBody2"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
tools:text="Label" />
|
||||
|
||||
<LinearLayout
|
||||
|
@ -37,7 +37,7 @@
|
||||
android:layout_toEndOf="@id/reorder"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:textAppearance="@style/TextAppearance.Regular.Body1"
|
||||
android:textAppearance="?attr/textAppearanceBody2"
|
||||
app:layout_constraintEnd_toStartOf="@+id/download_progress_text"
|
||||
app:layout_constraintStart_toEndOf="@+id/reorder"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
@ -52,7 +52,8 @@
|
||||
android:layout_toEndOf="@id/reorder"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:textAppearance="@style/TextAppearance.Regular.Caption"
|
||||
android:textAppearance="?attr/textAppearanceBody2"
|
||||
android:textSize="12sp"
|
||||
app:layout_constraintEnd_toStartOf="@+id/manga_source"
|
||||
app:layout_constraintStart_toStartOf="@+id/manga_full_title"
|
||||
app:layout_constraintTop_toBottomOf="@+id/manga_full_title"
|
||||
@ -75,7 +76,9 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_toEndOf="@id/manga_full_title"
|
||||
android:maxLines="1"
|
||||
android:textAppearance="@style/TextAppearance.Regular.Caption.Hint"
|
||||
android:textAppearance="?attr/textAppearanceBody2"
|
||||
android:textColor="?android:textColorSecondary"
|
||||
android:textSize="12sp"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/manga_full_title"
|
||||
app:layout_constraintEnd_toStartOf="@+id/menu"
|
||||
app:layout_constraintTop_toTopOf="@+id/manga_full_title"
|
||||
@ -87,7 +90,9 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_toEndOf="@id/chapter_title"
|
||||
android:maxLines="1"
|
||||
android:textAppearance="@style/TextAppearance.Regular.Caption.Hint"
|
||||
android:textAppearance="?attr/textAppearanceBody2"
|
||||
android:textColor="?android:textColorSecondary"
|
||||
android:textSize="12sp"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/chapter_title"
|
||||
app:layout_constraintEnd_toStartOf="@+id/menu"
|
||||
app:layout_constraintTop_toTopOf="@+id/chapter_title"
|
||||
|
@ -4,6 +4,7 @@
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="64dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:background="@drawable/list_item_selector_background">
|
||||
|
||||
<ImageView
|
||||
@ -21,14 +22,12 @@
|
||||
|
||||
<TextView
|
||||
android:id="@+id/ext_title"
|
||||
style="@style/TextAppearance.Regular"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:textAppearance="@style/TextAppearance.Regular.SubHeading"
|
||||
android:textSize="14sp"
|
||||
android:textAppearance="?attr/textAppearanceBody2"
|
||||
app:layout_constraintBottom_toTopOf="@id/lang"
|
||||
app:layout_constraintEnd_toStartOf="@id/ext_button"
|
||||
app:layout_constraintStart_toEndOf="@id/image"
|
||||
@ -38,11 +37,10 @@
|
||||
|
||||
<TextView
|
||||
android:id="@+id/lang"
|
||||
style="@style/TextAppearance.Regular.Body1.Secondary"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:maxLines="1"
|
||||
android:textSize="12sp"
|
||||
android:textAppearance="?attr/textAppearanceCaption"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/image"
|
||||
app:layout_constraintTop_toBottomOf="@+id/ext_title"
|
||||
@ -51,25 +49,23 @@
|
||||
|
||||
<TextView
|
||||
android:id="@+id/version"
|
||||
style="@style/TextAppearance.Regular.Body1.Secondary"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="4dp"
|
||||
android:maxLines="1"
|
||||
android:textSize="12sp"
|
||||
android:textAppearance="?attr/textAppearanceCaption"
|
||||
app:layout_constraintStart_toEndOf="@id/lang"
|
||||
app:layout_constraintTop_toBottomOf="@+id/ext_title"
|
||||
tools:text="Version" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/warning"
|
||||
style="@style/TextAppearance.Regular.Body1.Secondary"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="4dp"
|
||||
android:maxLines="1"
|
||||
android:textAppearance="?attr/textAppearanceCaption"
|
||||
android:textColor="?attr/colorError"
|
||||
android:textSize="12sp"
|
||||
app:layout_constraintStart_toEndOf="@id/version"
|
||||
app:layout_constraintTop_toBottomOf="@+id/ext_title"
|
||||
tools:text="Warning" />
|
||||
@ -79,10 +75,24 @@
|
||||
style="?attr/borderlessButtonStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="16dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/cancel_button"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="Details" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/cancel_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@android:string/cancel"
|
||||
android:padding="12dp"
|
||||
android:src="@drawable/ic_close_24dp"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:tint="?android:attr/textColorPrimary"
|
||||
tools:visibility="visible" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
@ -36,11 +36,11 @@
|
||||
|
||||
<TextView
|
||||
android:id="@+id/title"
|
||||
style="@style/TextAppearance.Regular.SubHeading"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:elevation="3dp"
|
||||
android:textAppearance="?attr/textAppearanceSubtitle1"
|
||||
app:layout_constraintStart_toEndOf="@id/icon"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="Tachiyomi: Extension" />
|
||||
@ -53,6 +53,7 @@
|
||||
android:layout_weight="1"
|
||||
android:elevation="3dp"
|
||||
android:gravity="center"
|
||||
android:textAppearance="?attr/textAppearanceCaption"
|
||||
app:layout_constraintStart_toStartOf="@id/title"
|
||||
app:layout_constraintTop_toBottomOf="@id/title"
|
||||
tools:text="Version: 1.0.0" />
|
||||
@ -65,6 +66,7 @@
|
||||
android:layout_weight="1"
|
||||
android:elevation="3dp"
|
||||
android:gravity="center"
|
||||
android:textAppearance="?attr/textAppearanceCaption"
|
||||
app:layout_constraintStart_toStartOf="@id/title"
|
||||
app:layout_constraintTop_toBottomOf="@id/version"
|
||||
tools:text="Language: English" />
|
||||
@ -78,6 +80,7 @@
|
||||
android:elevation="3dp"
|
||||
android:gravity="center"
|
||||
android:text="@string/ext_nsfw_warning"
|
||||
android:textAppearance="?attr/textAppearanceCaption"
|
||||
android:textColor="?attr/colorError"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintStart_toStartOf="@id/title"
|
||||
@ -91,6 +94,7 @@
|
||||
android:elevation="3dp"
|
||||
android:ellipsize="middle"
|
||||
android:singleLine="true"
|
||||
android:textAppearance="?attr/textAppearanceCaption"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="@id/title"
|
||||
app:layout_constraintTop_toBottomOf="@id/nsfw"
|
||||
@ -98,11 +102,24 @@
|
||||
|
||||
<Button
|
||||
android:id="@+id/btn_uninstall"
|
||||
style="@style/Widget.Tachiyomi.Button.OutlinedButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:text="@string/ext_uninstall"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/btn_app_info"
|
||||
app:layout_constraintTop_toBottomOf="@id/pkgname" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btn_app_info"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginStart="4dp"
|
||||
android:text="@string/ext_app_info"
|
||||
app:layout_constraintStart_toEndOf="@+id/btn_uninstall"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/pkgname" />
|
||||
|
||||
|
@ -14,10 +14,10 @@
|
||||
|
||||
<TextView
|
||||
android:id="@+id/title"
|
||||
style="@style/TextAppearance.Regular.SubHeading"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:textAppearance="?attr/textAppearanceSubtitle2"
|
||||
app:layout_constraintBottom_toTopOf="@+id/subtitle"
|
||||
app:layout_constraintEnd_toStartOf="@+id/title_more_icon"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
@ -27,11 +27,11 @@
|
||||
|
||||
<TextView
|
||||
android:id="@+id/subtitle"
|
||||
style="@style/TextAppearance.Regular.Body1.Secondary"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:maxLines="1"
|
||||
android:textAppearance="?attr/textAppearanceBody2"
|
||||
android:textSize="12sp"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
|
@ -46,7 +46,6 @@
|
||||
|
||||
<TextView
|
||||
android:id="@+id/favorite_text"
|
||||
style="@style/TextAppearance.Regular.Caption"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/colorSecondary"
|
||||
@ -56,6 +55,7 @@
|
||||
android:paddingEnd="3dp"
|
||||
android:paddingBottom="1dp"
|
||||
android:text="@string/in_library"
|
||||
android:textAppearance="?attr/textAppearanceCaption"
|
||||
android:textColor="?attr/colorOnSecondary"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
@ -66,14 +66,13 @@
|
||||
|
||||
<TextView
|
||||
android:id="@+id/title"
|
||||
style="@style/TextAppearance.Regular.Body1"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:fontFamily="@font/ptsans_narrow_bold"
|
||||
android:lineSpacingExtra="-4dp"
|
||||
android:maxLines="2"
|
||||
android:padding="4dp"
|
||||
android:textAppearance="?attr/textAppearanceSubtitle2"
|
||||
android:textSize="12sp"
|
||||
app:layout_constraintEnd_toEndOf="@+id/card"
|
||||
app:layout_constraintStart_toStartOf="@+id/card"
|
||||
app:layout_constraintTop_toBottomOf="@+id/card"
|
||||
|
@ -42,14 +42,16 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="2"
|
||||
android:textAppearance="@style/TextAppearance.Medium"
|
||||
android:textAppearance="?attr/textAppearanceSubtitle2"
|
||||
tools:text="Title" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/manga_subtitle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:layout_marginTop="2dp"
|
||||
android:textAppearance="?attr/textAppearanceBody2"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
tools:text="Subtitle" />
|
||||
|
||||
</LinearLayout>
|
||||
|
@ -1,7 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.recyclerview.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/recycler"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
tools:listitem="@layout/licenses_item" />
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<com.google.android.material.progressindicator.CircularProgressIndicator
|
||||
android:id="@+id/progress"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerInParent="true"
|
||||
android:indeterminate="true" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recycler"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:clipToPadding="false"
|
||||
tools:listitem="@layout/licenses_item" />
|
||||
|
||||
</RelativeLayout>
|
||||
|
@ -10,21 +10,25 @@
|
||||
|
||||
<TextView
|
||||
android:id="@+id/name"
|
||||
style="@style/TextAppearance.Regular.Body1"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="?attr/textAppearanceSubtitle2"
|
||||
tools:text="Library name" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/artifact_id"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="?attr/textAppearanceBody2"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
tools:text="artifact:id:1.0" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/license"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="?attr/textAppearanceBody2"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
tools:text="Apache Version 2.0" />
|
||||
|
||||
</LinearLayout>
|
||||
|
@ -75,7 +75,7 @@
|
||||
android:id="@+id/fab_layout"
|
||||
layout="@layout/main_activity_fab" />
|
||||
|
||||
<com.google.android.material.bottomnavigation.BottomNavigationView
|
||||
<eu.kanade.tachiyomi.widget.TachiyomiBottomNavigationView
|
||||
android:id="@+id/bottom_nav"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
|
@ -13,12 +13,11 @@
|
||||
|
||||
<TextView
|
||||
android:id="@+id/chapters_label"
|
||||
style="@style/TextAppearance.Regular.SubHeading"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/chapters"
|
||||
android:textAppearance="?attr/textAppearanceSubtitle1"
|
||||
android:textIsSelectable="false"
|
||||
android:textSize="16sp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/btn_chapters_filter"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
|
@ -70,27 +70,29 @@
|
||||
|
||||
<TextView
|
||||
android:id="@+id/manga_full_title"
|
||||
style="@style/TextAppearance.Medium.Title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/manga_info_full_title_label"
|
||||
android:textAppearance="?attr/textAppearanceHeadline6"
|
||||
android:textIsSelectable="false" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/manga_author"
|
||||
style="@style/TextAppearance.Regular.Body1.Secondary.Bold"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:textAppearance="?attr/textAppearanceSubtitle2"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:textIsSelectable="false"
|
||||
tools:text="Author" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/manga_artist"
|
||||
style="@style/TextAppearance.Regular.Body1.Secondary.Bold"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="2dp"
|
||||
android:textAppearance="?attr/textAppearanceSubtitle2"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:textIsSelectable="false"
|
||||
tools:text="Artist" />
|
||||
|
||||
@ -111,31 +113,34 @@
|
||||
|
||||
<TextView
|
||||
android:id="@+id/manga_status"
|
||||
style="@style/TextAppearance.Regular.Body1.Secondary"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:textAppearance="?attr/textAppearanceBody2"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:textIsSelectable="false"
|
||||
tools:text="Status" />
|
||||
|
||||
<TextView
|
||||
style="@style/TextAppearance.Regular.Body1.Secondary"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:text="•"
|
||||
android:textAppearance="?attr/textAppearanceBody2"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:textIsSelectable="false"
|
||||
tools:ignore="HardcodedText" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/manga_source"
|
||||
style="@style/TextAppearance.Regular.Body1.Secondary"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:textAppearance="?attr/textAppearanceBody2"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:textIsSelectable="false"
|
||||
tools:text="Source" />
|
||||
|
||||
@ -205,7 +210,6 @@
|
||||
|
||||
<TextView
|
||||
android:id="@+id/manga_summary_text"
|
||||
style="@style/TextAppearance.Regular.Body1.Secondary"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
@ -214,6 +218,8 @@
|
||||
android:ellipsize="end"
|
||||
android:focusable="true"
|
||||
android:maxLines="3"
|
||||
android:textAppearance="?attr/textAppearanceBody2"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:textIsSelectable="false"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
|
@ -21,4 +21,11 @@
|
||||
app:fastScrollerBubbleEnabled="false"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<eu.kanade.tachiyomi.widget.EmptyView
|
||||
android:id="@+id/empty_view"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:visibility="gone" />
|
||||
|
||||
</FrameLayout>
|
||||
|
@ -2,6 +2,7 @@
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/listPreferredItemHeightSmall"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:focusable="true"
|
||||
android:paddingStart="?attr/listPreferredItemPaddingStart"
|
||||
@ -17,6 +18,7 @@
|
||||
android:gravity="center_vertical|start"
|
||||
android:maxLines="1"
|
||||
android:paddingHorizontal="16dp"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Body1" />
|
||||
android:textAppearance="?attr/textAppearanceBody2"
|
||||
tools:text="Title" />
|
||||
|
||||
</LinearLayout>
|
||||
|
@ -2,6 +2,7 @@
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/listPreferredItemHeightSmall"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:focusable="true"
|
||||
android:paddingStart="?attr/listPreferredItemPaddingStart"
|
||||
@ -15,6 +16,7 @@
|
||||
android:drawablePadding="16dp"
|
||||
android:gravity="center_vertical|start"
|
||||
android:maxLines="1"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Body1" />
|
||||
android:textAppearance="?attr/textAppearanceBody2"
|
||||
tools:text="Title" />
|
||||
|
||||
</LinearLayout>
|
||||
|
@ -17,7 +17,7 @@
|
||||
android:layout_weight="1"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:textAppearance="@style/TextAppearance.Medium.SubHeading"
|
||||
android:textAppearance="@style/TextAppearance.Tachiyomi.SectionHeader"
|
||||
tools:text="Header" />
|
||||
|
||||
<ImageView
|
||||
|
@ -17,6 +17,6 @@
|
||||
android:gravity="center_vertical|start"
|
||||
android:maxLines="1"
|
||||
android:paddingHorizontal="16dp"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Body1" />
|
||||
android:textAppearance="?attr/textAppearanceBody2" />
|
||||
|
||||
</LinearLayout>
|
||||
|
@ -17,6 +17,8 @@
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:paddingEnd="8dp"
|
||||
android:textAppearance="?attr/textAppearanceBody2"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
tools:text="Filter:" />
|
||||
|
||||
<Spinner
|
||||
|
@ -23,7 +23,7 @@
|
||||
android:imeOptions="actionDone"
|
||||
android:inputType="text"
|
||||
android:maxLines="1"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Body1" />
|
||||
android:textAppearance="?attr/textAppearanceBody2" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user