Merge remote-tracking branch 'upstream/master'

This commit is contained in:
jmir1 2021-07-25 14:37:23 +02:00
commit d51fb8de47
26 changed files with 173 additions and 86 deletions

View file

@ -40,8 +40,6 @@ android {
// Please disable ACRA or use your own instance in forked versions of the project
buildConfigField("String", "ACRA_URI", "\"https://acra.jmir.xyz/report\"")
multiDexEnabled = true
ndk {
//abiFilters += SUPPORTED_ABIS
setOf("armeabi-v7a", "arm64-v8a", "x86")
@ -150,7 +148,6 @@ dependencies {
implementation("androidx.coordinatorlayout:coordinatorlayout:1.1.0")
implementation("androidx.core:core-ktx:1.7.0-alpha01")
implementation("androidx.core:core-splashscreen:1.0.0-alpha01")
implementation("androidx.multidex:multidex:2.0.1")
implementation("androidx.preference:preference-ktx:1.1.1")
implementation("androidx.recyclerview:recyclerview:1.2.1")
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01")
@ -271,7 +268,6 @@ dependencies {
val robolectricVersion = "3.1.4"
testImplementation("org.robolectric:robolectric:$robolectricVersion")
testImplementation("org.robolectric:shadows-multidex:$robolectricVersion")
testImplementation("org.robolectric:shadows-play-services:$robolectricVersion")
// For detecting memory leaks; see https://square.github.io/leakcanary/

View file

@ -11,6 +11,7 @@ import android.os.Build
import android.os.StrictMode
import android.os.StrictMode.VmPolicy
import android.webkit.WebView
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.getSystemService
import androidx.lifecycle.Lifecycle
@ -18,7 +19,6 @@ import androidx.lifecycle.LifecycleObserver
import androidx.lifecycle.OnLifecycleEvent
import androidx.lifecycle.ProcessLifecycleOwner
import androidx.lifecycle.lifecycleScope
import androidx.multidex.MultiDex
import coil.ImageLoader
import coil.ImageLoaderFactory
import coil.decode.GifDecoder
@ -28,7 +28,9 @@ import eu.kanade.tachiyomi.data.coil.ByteBufferFetcher
import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher
import eu.kanade.tachiyomi.data.coil.TachiyomiImageDecoder
import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.data.preference.PreferenceValues
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.asImmediateFlow
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate
import eu.kanade.tachiyomi.util.system.notification
@ -102,11 +104,17 @@ open class App : Application(), LifecycleObserver, ImageLoaderFactory {
}
}
.launchIn(ProcessLifecycleOwner.get().lifecycleScope)
}
override fun attachBaseContext(base: Context) {
super.attachBaseContext(base)
MultiDex.install(this)
preferences.themeMode()
.asImmediateFlow {
AppCompatDelegate.setDefaultNightMode(
when (it) {
PreferenceValues.ThemeMode.light -> AppCompatDelegate.MODE_NIGHT_NO
PreferenceValues.ThemeMode.dark -> AppCompatDelegate.MODE_NIGHT_YES
PreferenceValues.ThemeMode.system -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
}
)
}.launchIn(ProcessLifecycleOwner.get().lifecycleScope)
}
override fun newImageLoader(): ImageLoader {

View file

@ -70,8 +70,11 @@ open class AnimeSourceManager(private val context: Context) {
return name
}
private fun getSourceNotInstalledException(): Exception {
return Exception(context.getString(R.string.source_not_installed, id.toString()))
private fun getSourceNotInstalledException(): SourceNotInstalledException {
return SourceNotInstalledException(id)
}
}
inner class SourceNotInstalledException(val id: Long) :
Exception(context.getString(R.string.source_not_installed, id.toString()))
}

View file

@ -316,6 +316,9 @@ class AnimelibUpdateService(
} catch (e: Throwable) {
val errorMessage = if (e is NoEpisodesException) {
getString(R.string.no_chapters_error)
} else if (e is AnimeSourceManager.SourceNotInstalledException) {
// failedUpdates will already have the source, don't need to copy it into the message
getString(R.string.loader_not_implemented_error)
} else {
e.message
}

View file

@ -11,8 +11,10 @@ interface Anime : SAnime {
var favorite: Boolean
// last time the episode list changed in any way
var last_update: Long
// predicted next update time based on latest (by date) 4 episodes' deltas
var next_update: Long
var date_added: Long

View file

@ -13,8 +13,10 @@ interface Manga : SManga {
var favorite: Boolean
// last time the chapter list changed in any way
var last_update: Long
// predicted next update time based on latest (by date) 4 chapters' deltas
var next_update: Long
var date_added: Long

View file

@ -316,6 +316,9 @@ class LibraryUpdateService(
} catch (e: Throwable) {
val errorMessage = if (e is NoChaptersException) {
getString(R.string.no_chapters_error)
} else if (e is SourceManager.SourceNotInstalledException) {
// failedUpdates will already have the source, don't need to copy it into the message
getString(R.string.loader_not_implemented_error)
} else {
e.message
}

View file

@ -148,7 +148,7 @@ class NotificationReceiver : BroadcastReceiver() {
*/
private fun shareImage(context: Context, path: String, notificationId: Int) {
dismissNotification(context, notificationId)
context.startActivity(File(path).getUriCompat(context).toShareIntent())
context.startActivity(File(path).getUriCompat(context).toShareIntent(context))
}
/**
@ -160,7 +160,7 @@ class NotificationReceiver : BroadcastReceiver() {
*/
private fun shareFile(context: Context, uri: Uri, fileMimeType: String, notificationId: Int) {
dismissNotification(context, notificationId)
context.startActivity(uri.toShareIntent(fileMimeType))
context.startActivity(uri.toShareIntent(context, fileMimeType))
}
/**

View file

@ -70,8 +70,11 @@ open class SourceManager(private val context: Context) {
return name
}
private fun getSourceNotInstalledException(): Exception {
return Exception(context.getString(R.string.source_not_installed, id.toString()))
private fun getSourceNotInstalledException(): SourceNotInstalledException {
return SourceNotInstalledException(id)
}
}
inner class SourceNotInstalledException(val id: Long) :
Exception(context.getString(R.string.source_not_installed, id.toString()))
}

View file

@ -705,12 +705,7 @@ class AnimeController :
useCoverAsBitmap(activity) { coverBitmap ->
val cover = presenter.shareCover(activity, coverBitmap)
val uri = cover.getUriCompat(activity)
startActivity(
Intent.createChooser(
uri.toShareIntent(),
activity.getString(R.string.action_share)
)
)
startActivity(uri.toShareIntent(activity))
}
} catch (e: Exception) {
Timber.e(e)

View file

@ -3,7 +3,6 @@ package eu.kanade.tachiyomi.ui.base.activity
import android.content.Context
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
import androidx.lifecycle.lifecycleScope
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.PreferenceValues
@ -25,7 +24,7 @@ abstract class BaseThemedActivity : AppCompatActivity() {
}
override fun onCreate(savedInstanceState: Bundle?) {
applyThemePreferences(preferences)
applyAppTheme(preferences)
Injekt.get<PreferencesHelper>().incognitoMode()
.asImmediateFlow {
@ -37,7 +36,7 @@ abstract class BaseThemedActivity : AppCompatActivity() {
}
companion object {
fun AppCompatActivity.applyThemePreferences(preferences: PreferencesHelper) {
fun AppCompatActivity.applyAppTheme(preferences: PreferencesHelper) {
val resIds = mutableListOf<Int>()
when (preferences.appTheme().get()) {
PreferenceValues.AppTheme.MONET -> {
@ -77,16 +76,6 @@ abstract class BaseThemedActivity : AppCompatActivity() {
resIds.forEach {
setTheme(it)
}
lifecycleScope.launchWhenCreated {
AppCompatDelegate.setDefaultNightMode(
when (preferences.themeMode().get()) {
PreferenceValues.ThemeMode.light -> AppCompatDelegate.MODE_NIGHT_NO
PreferenceValues.ThemeMode.dark -> AppCompatDelegate.MODE_NIGHT_YES
PreferenceValues.ThemeMode.system -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
}
)
}
}
}
}

View file

@ -688,12 +688,7 @@ class MangaController :
useCoverAsBitmap(activity) { coverBitmap ->
val cover = presenter.shareCover(activity, coverBitmap)
val uri = cover.getUriCompat(activity)
startActivity(
Intent.createChooser(
uri.toShareIntent(),
activity.getString(R.string.action_share)
)
)
startActivity(uri.toShareIntent(activity))
}
} catch (e: Exception) {
Timber.e(e)

View file

@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.ui.reader
import android.annotation.SuppressLint
import android.annotation.TargetApi
import android.app.ActionBar
import android.app.ProgressDialog
import android.content.ClipData
import android.content.Context
@ -45,7 +46,7 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.toggle
import eu.kanade.tachiyomi.databinding.ReaderActivityBinding
import eu.kanade.tachiyomi.ui.base.activity.BaseRxActivity
import eu.kanade.tachiyomi.ui.base.activity.BaseThemedActivity.Companion.applyThemePreferences
import eu.kanade.tachiyomi.ui.base.activity.BaseThemedActivity.Companion.applyAppTheme
import eu.kanade.tachiyomi.ui.main.MainActivity
import eu.kanade.tachiyomi.ui.manga.MangaController
import eu.kanade.tachiyomi.ui.reader.ReaderPresenter.SetAsCoverResult.AddToLibraryFirst
@ -140,7 +141,7 @@ class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>()
* Called when the activity is created. Initializes the presenter and configuration.
*/
override fun onCreate(savedInstanceState: Bundle?) {
applyThemePreferences(preferences)
applyAppTheme(preferences)
super.onCreate(savedInstanceState)
binding = ReaderActivityBinding.inflate(layoutInflater)
@ -189,6 +190,7 @@ class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>()
readingModeToast?.cancel()
progressDialog?.dismiss()
progressDialog = null
listeners = mutableListOf()
}
/**
@ -486,12 +488,23 @@ class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>()
)
}
private var listeners: MutableList<ActionBar.OnMenuVisibilityListener> = mutableListOf()
fun addOnMenuVisibilityListener(listener: ActionBar.OnMenuVisibilityListener) {
listeners.add(listener)
}
fun removeOnMenuVisibilityListener(listener: ActionBar.OnMenuVisibilityListener) {
listeners.remove(listener)
}
/**
* Sets the visibility of the menu according to [visible] and with an optional parameter to
* [animate] the views.
*/
fun setMenuVisibility(visible: Boolean, animate: Boolean = true) {
menuVisible = visible
listeners.forEach { listener -> listener.onMenuVisibilityChanged(visible) }
if (visible) {
windowInsetsController.show(WindowInsetsCompat.Type.systemBars())
binding.readerMenu.isVisible = true
@ -737,6 +750,15 @@ class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>()
}
}
/**
* Called from the viewer to hide the menu.
*/
fun hideMenu() {
if (menuVisible) {
setMenuVisibility(false)
}
}
/**
* Called from the page sheet. It delegates the call to the presenter to do some IO, which
* will call [onShareImageResult] with the path the image was saved on when it's ready.

View file

@ -1,6 +1,7 @@
package eu.kanade.tachiyomi.ui.reader.viewer.pager
import android.annotation.SuppressLint
import android.app.ActionBar
import android.graphics.PointF
import android.graphics.drawable.Animatable
import android.view.GestureDetector
@ -99,9 +100,28 @@ class PagerPageHolder(
*/
private var readImageHeaderSubscription: Subscription? = null
private var visibilityListener = ActionBar.OnMenuVisibilityListener { isVisible ->
if (isVisible.not()) {
subsamplingImageView?.setOnStateChangedListener(null)
return@OnMenuVisibilityListener
}
subsamplingImageView?.setOnStateChangedListener(
object : SubsamplingScaleImageView.OnStateChangedListener {
override fun onScaleChanged(newScale: Float, origin: Int) {
viewer.activity.hideMenu()
}
override fun onCenterChanged(newCenter: PointF?, origin: Int) {
viewer.activity.hideMenu()
}
}
)
}
init {
addView(progressIndicator)
observeStatus()
viewer.activity.addOnMenuVisibilityListener(visibilityListener)
}
/**
@ -114,6 +134,8 @@ class PagerPageHolder(
unsubscribeStatus()
unsubscribeReadImageHeader()
subsamplingImageView?.setOnImageEventListener(null)
subsamplingImageView?.setOnStateChangedListener(null)
viewer.activity.removeOnMenuVisibilityListener(visibilityListener)
}
/**

View file

@ -153,6 +153,7 @@ abstract class PagerViewer(val activity: ReaderActivity) : BaseViewer {
* Called when a new page (either a [ReaderPage] or [ChapterTransition]) is marked as active
*/
private fun onPageChange(position: Int) {
activity.hideMenu()
val page = adapter.items.getOrNull(position)
if (page != null && currentPage != page) {
val allowPreload = checkAllowPreload(page as? ReaderPage)

View file

@ -243,6 +243,7 @@ class WebtoonViewer(val activity: ReaderActivity, val isContinuous: Boolean = tr
}
fun onScrolled(pos: Int? = null) {
activity.hideMenu()
val position = pos ?: layoutManager.findLastEndVisibleItemPosition()
val item = adapter.items.getOrNull(position)
val allowPreload = checkAllowPreload(item as? ReaderPage)

View file

@ -65,7 +65,6 @@ class SettingsBackupController : SettingsController() {
onClick {
if (MiuiUtil.isMiui() && MiuiUtil.isMiuiOptimizationDisabled()) {
context.toast(R.string.restore_miui_warning, Toast.LENGTH_LONG)
return@onClick
}
if (!BackupCreateService.isRunning(context)) {
@ -85,7 +84,6 @@ class SettingsBackupController : SettingsController() {
onClick {
if (MiuiUtil.isMiui() && MiuiUtil.isMiuiOptimizationDisabled()) {
context.toast(R.string.restore_miui_warning, Toast.LENGTH_LONG)
return@onClick
}
if (!BackupRestoreService.isRunning(context)) {

View file

@ -269,8 +269,8 @@ class SettingsDownloadController : SettingsController() {
return MaterialAlertDialogBuilder(activity!!)
.setTitle(R.string.categories)
.setMessage(R.string.pref_download_new_categories_details)
.setQuadStateMultiChoiceItems(
message = R.string.pref_download_new_categories_details,
items = items,
initialSelected = selected
) { selections ->

View file

@ -112,11 +112,6 @@ class SettingsGeneralController : SettingsController() {
}
summary = "%s"
onChange {
activity?.recreate()
true
}
}
listPreference {
key = Keys.appTheme

View file

@ -358,8 +358,11 @@ class SettingsLibraryController : SettingsController() {
return MaterialAlertDialogBuilder(activity!!)
.setTitle(R.string.categories)
.setMessage(R.string.pref_library_update_categories_details)
.setQuadStateMultiChoiceItems(items = items, initialSelected = selected) { selections ->
.setQuadStateMultiChoiceItems(
message = R.string.pref_library_update_categories_details,
items = items,
initialSelected = selected
) { selections ->
selected = selections
}
.setPositiveButton(android.R.string.ok) { _, _ ->

View file

@ -111,10 +111,6 @@ fun syncChaptersWithSource(
db.updateNextUpdated(manga).executeAsBlocking()
}
if (newestDate != 0L && newestDate != manga.last_update) {
manga.last_update = newestDate
db.updateLastUpdated(manga).executeAsBlocking()
}
return Pair(emptyList(), emptyList())
}
@ -177,13 +173,8 @@ fun syncChaptersWithSource(
db.fixChaptersSourceOrder(sourceChapters).executeAsBlocking()
// Set this manga as updated since chapters were changed
val newestChapter = topChapters.getOrNull(0)
val dateFetch = newestChapter?.date_upload ?: manga.last_update
if (dateFetch == 0L) {
if (toAdd.isNotEmpty()) {
manga.last_update = Date().time
}
} else manga.last_update = dateFetch
// Note that last_update actually represents last time the chapter list changed at all
manga.last_update = Date().time
db.updateLastUpdated(manga).executeAsBlocking()
}

View file

@ -111,10 +111,6 @@ fun syncEpisodesWithSource(
db.updateNextUpdated(anime).executeAsBlocking()
}
if (newestDate != 0L && newestDate != anime.last_update) {
anime.last_update = newestDate
db.updateLastUpdated(anime).executeAsBlocking()
}
return Pair(emptyList(), emptyList())
}
@ -176,13 +172,8 @@ fun syncEpisodesWithSource(
db.fixEpisodesSourceOrder(sourceEpisodes).executeAsBlocking()
// Set this anime as updated since episodes were changed
val newestEpisode = topEpisodes.getOrNull(0)
val dateFetch = newestEpisode?.date_upload ?: anime.last_update
if (dateFetch == 0L) {
if (toAdd.isNotEmpty()) {
anime.last_update = Date().time
}
} else anime.last_update = dateFetch
// Note that last_update actually represents last time the chapter list changed at all
anime.last_update = Date().time
db.updateLastUpdated(anime).executeAsBlocking()
}

View file

@ -1,15 +1,22 @@
package eu.kanade.tachiyomi.util.system
import android.content.ClipData
import android.content.Context
import android.content.Intent
import android.net.Uri
import eu.kanade.tachiyomi.R
fun Uri.toShareIntent(type: String = "image/*"): Intent {
fun Uri.toShareIntent(context: Context, type: String = "image/*"): Intent {
val uri = this
return Intent(Intent.ACTION_SEND).apply {
val shareIntent = Intent(Intent.ACTION_SEND).apply {
putExtra(Intent.EXTRA_STREAM, uri)
clipData = ClipData.newRawUri(null, uri)
setType(type)
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION
flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
}
return Intent.createChooser(shareIntent, context.getString(R.string.action_share)).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK
}
}

View file

@ -3,7 +3,9 @@ package eu.kanade.tachiyomi.widget.materialdialogs
import android.view.LayoutInflater
import android.view.inputmethod.InputMethodManager
import android.widget.TextView
import androidx.annotation.StringRes
import androidx.core.content.getSystemService
import androidx.core.view.isVisible
import androidx.core.widget.doAfterTextChanged
import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.dialog.MaterialAlertDialogBuilder
@ -36,6 +38,7 @@ fun MaterialAlertDialogBuilder.setTextInput(
* @see eu.kanade.tachiyomi.widget.materialdialogs.QuadStateTextView
*/
fun MaterialAlertDialogBuilder.setQuadStateMultiChoiceItems(
@StringRes message: Int? = null,
items: List<CharSequence>,
initialSelected: IntArray,
disabledIndices: IntArray? = null,
@ -49,6 +52,20 @@ fun MaterialAlertDialogBuilder.setQuadStateMultiChoiceItems(
initialSelected = initialSelected,
listener = selection
)
setView(binding.root)
return this
val updateScrollIndicators = {
binding.scrollIndicatorUp.isVisible = binding.list.canScrollVertically(-1)
binding.scrollIndicatorDown.isVisible = binding.list.canScrollVertically(1)
}
binding.list.setOnScrollChangeListener { _, _, _, _, _ ->
updateScrollIndicators()
}
binding.list.post {
updateScrollIndicators()
}
if (message != null) {
binding.message.setText(message)
binding.message.isVisible = true
}
return setView(binding.root)
}

View file

@ -1,9 +1,49 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.recyclerview.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="8dp"
android:scrollIndicators="none"
tools:listitem="@layout/dialog_quadstatemultichoice_item" />
android:orientation="vertical"
android:minHeight="48dp">
<Space
android:layout_width="match_parent"
android:layout_height="@dimen/abc_dialog_title_divider_material" />
<TextView
android:id="@+id/message"
style="?attr/materialAlertDialogBodyTextStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="@dimen/abc_dialog_title_divider_material"
android:paddingHorizontal="?attr/dialogPreferredPadding"
android:visibility="gone"
tools:text="Dialog Message for quad-state dialog"
tools:visibility="visible" />
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.divider.MaterialDivider
android:id="@+id/scrollIndicatorUp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="top" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scrollIndicators="none"
tools:listitem="@layout/dialog_quadstatemultichoice_item" />
<com.google.android.material.divider.MaterialDivider
android:id="@+id/scrollIndicatorDown"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom" />
</FrameLayout>
</LinearLayout>

View file

@ -424,7 +424,7 @@
<!-- Tracking section -->
<string name="tracking_guide">Tracking guide</string>
<string name="pref_auto_update_manga_sync">Update chapter progress after reading</string>
<string name="pref_auto_update_manga_sync">Update progress after reading</string>
<string name="services">Services</string>
<string name="tracking_info">One-way sync to update the chapter progress in tracking services. Set up tracking for individual manga entries from their tracking button.</string>
<string name="enhanced_services">Enhanced services</string>
@ -464,7 +464,7 @@
<string name="backup_choice">What do you want to backup?</string>
<string name="creating_backup">Creating backup</string>
<string name="creating_backup_error">Backup failed</string>
<string name="restore_miui_warning">MIUI Optimization must be enabled for backup/restore to work correctly.</string>
<string name="restore_miui_warning">Backup/restore may not function properly if MIUI Optimization is disabled.</string>
<string name="restore_in_progress">Restore is already in progress</string>
<string name="restoring_backup">Restoring backup</string>
<string name="restoring_backup_error">Restoring backup failed</string>