Last commit merged: 1668be8587
This commit is contained in:
LuftVerbot 2023-11-19 19:38:42 +01:00
parent 3a3115304e
commit 18e04b75df
50 changed files with 673 additions and 645 deletions

View file

@ -20,7 +20,7 @@ jobs:
steps:
- name: Clone repo
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Validate Gradle Wrapper
uses: gradle/wrapper-validation-action@v1

View file

@ -22,7 +22,7 @@ jobs:
steps:
- name: Clone repo
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Validate Gradle Wrapper
uses: gradle/wrapper-validation-action@v1

View file

@ -199,34 +199,6 @@
android:name=".data.notification.NotificationReceiver"
android:exported="false" />
<receiver
android:name="tachiyomi.presentation.widget.entries.manga.MangaUpdatesGridGlanceReceiver"
android:enabled="@bool/glance_appwidget_available"
android:exported="false"
android:label="@string/label_recent_updates">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/updates_grid_glance_widget_info" />
</receiver>
<receiver
android:name="tachiyomi.presentation.widget.entries.anime.AnimeUpdatesGridGlanceReceiver"
android:enabled="@bool/glance_appwidget_available"
android:exported="false"
android:label="@string/label_recent_anime_updates">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/updates_grid_glance_widget_info" />
</receiver>
<service
android:name=".data.download.manga.MangaDownloadService"
android:exported="false" />

View file

@ -115,7 +115,7 @@ object SettingsBackupScreen : SearchableSettings {
showCreateDialog = false
flag = it
try {
chooseBackupDir.launch(Backup.getBackupFilename())
chooseBackupDir.launch(Backup.getFilename())
} catch (e: ActivityNotFoundException) {
flag = 0
context.toast(R.string.file_picker_error)

View file

@ -149,15 +149,14 @@ class BackupManager(
// Delete older backups
val numberOfBackups = backupPreferences.numberOfBackups().get()
val backupRegex = Regex("""aniyomi_\d+-\d+-\d+_\d+-\d+.proto.gz""")
dir.listFiles { _, filename -> backupRegex.matches(filename) }
dir.listFiles { _, filename -> Backup.filenameRegex.matches(filename) }
.orEmpty()
.sortedByDescending { it.name }
.drop(numberOfBackups - 1)
.forEach { it.delete() }
// Create new file to place backup
dir.createFile(Backup.getBackupFilename())
dir.createFile(Backup.getFilename())
} else {
UniFile.fromUri(context, uri)
}

View file

@ -1,5 +1,6 @@
package eu.kanade.tachiyomi.data.backup.models
import eu.kanade.tachiyomi.BuildConfig
import kotlinx.serialization.Serializable
import kotlinx.serialization.protobuf.ProtoNumber
import java.text.SimpleDateFormat
@ -23,9 +24,11 @@ data class Backup(
) {
companion object {
fun getBackupFilename(): String {
val filenameRegex = """${BuildConfig.APPLICATION_ID}_\d+-\d+-\d+_\d+-\d+.tachibk""".toRegex()
fun getFilename(): String {
val date = SimpleDateFormat("yyyy-MM-dd_HH-mm", Locale.getDefault()).format(Date())
return "aniyomi_$date.proto.gz"
return "${BuildConfig.APPLICATION_ID}_$date.tachibk"
}
}
}

View file

@ -9,9 +9,11 @@ import eu.kanade.tachiyomi.util.storage.saveTo
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import logcat.LogPriority
import okhttp3.Response
import okio.buffer
import okio.sink
import tachiyomi.core.util.system.logcat
import tachiyomi.domain.items.chapter.model.Chapter
import uy.kohesive.injekt.injectLazy
import java.io.File
@ -99,6 +101,7 @@ class ChapterCache(private val context: Context) {
editor.commit()
editor.abortUnlessCommitted()
} catch (e: Exception) {
logcat(LogPriority.WARN, e) { "Failed to put page list to cache" }
// Ignore.
} finally {
editor?.abortUnlessCommitted()
@ -176,7 +179,7 @@ class ChapterCache(private val context: Context) {
* @return status of deletion for the file.
*/
private fun removeFileFromCache(file: String): Boolean {
// Make sure we don't delete the journal file (keeps track of cache).
// Make sure we don't delete the journal file (keeps track of cache)
if (file == "journal" || file.startsWith("journal.")) {
return false
}
@ -184,9 +187,10 @@ class ChapterCache(private val context: Context) {
return try {
// Remove the extension from the file to get the key of the cache
val key = file.substringBeforeLast(".")
// Remove file from cache.
// Remove file from cache
diskCache.remove(key)
} catch (e: Exception) {
logcat(LogPriority.WARN, e) { "Failed to remove file from cache" }
false
}
}

View file

@ -8,9 +8,11 @@ import eu.kanade.tachiyomi.util.storage.DiskUtil
import eu.kanade.tachiyomi.util.storage.saveTo
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import logcat.LogPriority
import okhttp3.Response
import okio.buffer
import okio.sink
import tachiyomi.core.util.system.logcat
import tachiyomi.domain.items.episode.model.Episode
import uy.kohesive.injekt.injectLazy
import java.io.File
@ -71,7 +73,7 @@ class EpisodeCache(private val context: Context) {
* @return status of deletion for the file.
*/
fun removeFileFromCache(file: String): Boolean {
// Make sure we don't delete the journal file (keeps track of cache).
// Make sure we don't delete the journal file (keeps track of cache)
if (file == "journal" || file.startsWith("journal.")) {
return false
}
@ -79,9 +81,10 @@ class EpisodeCache(private val context: Context) {
return try {
// Remove the extension from the file to get the key of the cache
val key = file.substringBeforeLast(".")
// Remove file from cache.
// Remove file from cache
diskCache.remove(key)
} catch (e: Exception) {
logcat(LogPriority.WARN, e) { "Failed to remove file from cache" }
false
}
}
@ -114,6 +117,7 @@ class EpisodeCache(private val context: Context) {
editor.commit()
editor.abortUnlessCommitted()
} catch (e: Exception) {
logcat(LogPriority.WARN, e) { "Failed to put video list to cache" }
// Ignore.
} finally {
editor?.abortUnlessCommitted()

View file

@ -1,12 +1,12 @@
package eu.kanade.tachiyomi.ui.download.anime
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.PlayArrow
@ -32,7 +32,6 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.Velocity
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.view.ViewCompat
import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding
import androidx.recyclerview.widget.LinearLayoutManager
import eu.kanade.tachiyomi.R
@ -127,6 +126,7 @@ fun AnimeDownloadQueueScreen(
)
return@Scaffold
}
val density = LocalDensity.current
val layoutDirection = LocalLayoutDirection.current
val left = with(density) { contentPadding.calculateLeftPadding(layoutDirection).toPx().roundToInt() }
@ -136,15 +136,15 @@ fun AnimeDownloadQueueScreen(
Box(modifier = Modifier.nestedScroll(nestedScrollConnection)) {
AndroidView(
modifier = Modifier.fillMaxWidth(),
factory = { context ->
screenModel.controllerBinding = DownloadListBinding.inflate(
LayoutInflater.from(context),
)
screenModel.adapter = AnimeDownloadAdapter(screenModel.listener)
screenModel.controllerBinding.recycler.adapter = screenModel.adapter
screenModel.controllerBinding.root.adapter = screenModel.adapter
screenModel.adapter?.isHandleDragEnabled = true
screenModel.adapter?.fastScroller = screenModel.controllerBinding.fastScroller
screenModel.controllerBinding.recycler.layoutManager = LinearLayoutManager(
screenModel.controllerBinding.root.layoutManager = LinearLayoutManager(
context,
)
@ -162,7 +162,7 @@ fun AnimeDownloadQueueScreen(
screenModel.controllerBinding.root
},
update = {
screenModel.controllerBinding.recycler
screenModel.controllerBinding.root
.updatePadding(
left = left,
top = top,
@ -170,14 +170,6 @@ fun AnimeDownloadQueueScreen(
bottom = bottom,
)
screenModel.controllerBinding.fastScroller
.updateLayoutParams<ViewGroup.MarginLayoutParams> {
leftMargin = left
topMargin = top
rightMargin = right
bottomMargin = bottom
}
screenModel.adapter?.updateDataSet(downloadList)
},
)

View file

@ -231,6 +231,6 @@ class AnimeDownloadQueueScreenModel(
* @return the holder of the download or null if it's not bound.
*/
private fun getHolder(download: AnimeDownload): AnimeDownloadHolder? {
return controllerBinding.recycler.findViewHolderForItemId(download.episode.id) as? AnimeDownloadHolder
return controllerBinding.root.findViewHolderForItemId(download.episode.id) as? AnimeDownloadHolder
}
}

View file

@ -1,12 +1,12 @@
package eu.kanade.tachiyomi.ui.download.manga
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.PlayArrow
@ -32,7 +32,6 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.Velocity
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.view.ViewCompat
import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding
import androidx.recyclerview.widget.LinearLayoutManager
import eu.kanade.tachiyomi.R
@ -127,6 +126,7 @@ fun DownloadQueueScreen(
)
return@Scaffold
}
val density = LocalDensity.current
val layoutDirection = LocalLayoutDirection.current
val left = with(density) { contentPadding.calculateLeftPadding(layoutDirection).toPx().roundToInt() }
@ -136,15 +136,15 @@ fun DownloadQueueScreen(
Box(modifier = Modifier.nestedScroll(nestedScrollConnection)) {
AndroidView(
modifier = Modifier.fillMaxWidth(),
factory = { context ->
screenModel.controllerBinding = DownloadListBinding.inflate(
LayoutInflater.from(context),
)
screenModel.adapter = MangaDownloadAdapter(screenModel.listener)
screenModel.controllerBinding.recycler.adapter = screenModel.adapter
screenModel.controllerBinding.root.adapter = screenModel.adapter
screenModel.adapter?.isHandleDragEnabled = true
screenModel.adapter?.fastScroller = screenModel.controllerBinding.fastScroller
screenModel.controllerBinding.recycler.layoutManager = LinearLayoutManager(
screenModel.controllerBinding.root.layoutManager = LinearLayoutManager(
context,
)
@ -162,7 +162,7 @@ fun DownloadQueueScreen(
screenModel.controllerBinding.root
},
update = {
screenModel.controllerBinding.recycler
screenModel.controllerBinding.root
.updatePadding(
left = left,
top = top,
@ -170,14 +170,6 @@ fun DownloadQueueScreen(
bottom = bottom,
)
screenModel.controllerBinding.fastScroller
.updateLayoutParams<ViewGroup.MarginLayoutParams> {
leftMargin = left
topMargin = top
rightMargin = right
bottomMargin = bottom
}
screenModel.adapter?.updateDataSet(downloadList)
},
)

View file

@ -261,6 +261,6 @@ class MangaDownloadQueueScreenModel(
* @return the holder of the download or null if it's not bound.
*/
private fun getHolder(download: MangaDownload): MangaDownloadHolder? {
return controllerBinding.recycler.findViewHolderForItemId(download.chapter.id) as? MangaDownloadHolder
return controllerBinding.root.findViewHolderForItemId(download.chapter.id) as? MangaDownloadHolder
}
}

View file

@ -19,7 +19,7 @@ class UnlockActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
startAuthentication(
getString(R.string.unlock_app),
getString(R.string.unlock_app_title, getString(R.string.app_name)),
confirmationRequired = false,
callback = object : AuthenticatorUtil.AuthenticationCallback() {
override fun onAuthenticationError(

View file

@ -3,9 +3,7 @@ package eu.kanade.tachiyomi.util.system
import android.app.Activity
import android.content.Context
import android.content.res.Configuration
import android.content.res.Resources
import android.os.Build
import android.view.View
import eu.kanade.domain.ui.UiPreferences
import eu.kanade.domain.ui.model.TabletUiMode
import uy.kohesive.injekt.Injekt
@ -64,18 +62,6 @@ fun Context.isNightMode(): Boolean {
return resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES
}
val Resources.isLTR
get() = configuration.layoutDirection == View.LAYOUT_DIRECTION_LTR
/**
* Converts to px and takes into account LTR/RTL layout.
*/
val Float.dpToPxEnd: Float
get() = (
this * Resources.getSystem().displayMetrics.density *
if (Resources.getSystem().isLTR) 1 else -1
)
/**
* Checks whether if the device has a display cutout (i.e. notch, camera cutout, etc.).
*

View file

@ -1,92 +0,0 @@
package eu.kanade.tachiyomi.widget
import android.annotation.SuppressLint
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
import eu.kanade.tachiyomi.util.system.isLTR
class MaterialFastScroll @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
FastScroller(context, attrs) {
init {
setViewsToUse(
R.layout.material_fastscroll,
R.id.fast_scroller_bubble,
R.id.fast_scroller_handle,
)
autoHideEnabled = true
ignoreTouchesOutsideHandle = true
applyInsetter {
type(navigationBars = true) {
margin()
}
}
}
// Overridden to handle RTL
@SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(event: MotionEvent): Boolean {
if (recyclerView.computeVerticalScrollRange() <= recyclerView.computeVerticalScrollExtent()) {
return super.onTouchEvent(event)
}
when (event.action) {
MotionEvent.ACTION_DOWN -> {
// start: handle RTL differently
if (
if (context.resources.isLTR) {
event.x < handle.x - ViewCompat.getPaddingStart(handle)
} else {
event.x > handle.width + ViewCompat.getPaddingStart(handle)
}
) {
return false
}
// end
if (ignoreTouchesOutsideHandle &&
(event.y < handle.y || event.y > handle.y + handle.height)
) {
return false
}
handle.isSelected = true
notifyScrollStateChange(true)
showBubble()
showScrollbar()
val y = event.y
setBubbleAndHandlePosition(y)
setRecyclerViewPosition(y)
return true
}
MotionEvent.ACTION_MOVE -> {
val y = event.y
setBubbleAndHandlePosition(y)
setRecyclerViewPosition(y)
return true
}
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
handle.isSelected = false
notifyScrollStateChange(false)
hideBubble()
if (autoHideEnabled) hideScrollbar()
return true
}
}
return super.onTouchEvent(event)
}
override fun setBubbleAndHandlePosition(y: Float) {
super.setBubbleAndHandlePosition(y)
if (bubbleEnabled) {
bubble.y = handle.y - bubble.height / 2f + handle.height / 2f
bubble.translationX = (-45f).dpToPxEnd
}
}
}

View file

@ -1,17 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_selected="true">
<shape android:shape="rectangle">
<corners android:radius="8dp" />
<solid android:color="?attr/colorAccent" />
<size android:width="6dp" android:height="54dp" />
</shape>
</item>
<item>
<shape android:shape="rectangle">
<corners android:radius="8dp" />
<solid android:color="@color/fast_scroller_handle_idle" />
<size android:width="6dp" android:height="54dp" />
</shape>
</item>
</selector>

View file

@ -1,24 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
<androidx.recyclerview.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/frame_container"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
tools:listitem="@layout/download_item" />
<eu.kanade.tachiyomi.widget.MaterialFastScroll
android:id="@+id/fast_scroller"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="end"
app:fastScrollerBubbleEnabled="false"
tools:visibility="visible" />
</FrameLayout>

View file

@ -1,43 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content"
android:layout_height="match_parent">
<View
android:id="@+id/fast_scroller_bar"
android:layout_width="7dp"
android:layout_height="match_parent"
android:layout_gravity="end"
android:background="@null" />
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end">
<!-- No margin, use padding at the handle -->
<com.google.android.material.textview.MaterialTextView
android:id="@+id/fast_scroller_bubble"
style="@style/FloatingTextView"
android:layout_gravity="end|center_vertical"
android:layout_toStartOf="@+id/fast_scroller_handle"
android:gravity="center"
android:visibility="gone"
tools:text="A"
tools:visibility="visible" />
<!-- Padding is here to have better grab -->
<ImageView
android:id="@+id/fast_scroller_handle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:contentDescription="@null"
android:paddingStart="6dp"
android:paddingEnd="4dp"
android:src="@drawable/material_thumb_drawable" />
</RelativeLayout>
</merge>

View file

@ -56,21 +56,6 @@
</style>
<!--============-->
<!--FastScroller-->
<!--============-->
<style name="FloatingTextView" parent="TextAppearance.AppCompat">
<item name="android:layout_height">wrap_content</item>
<item name="android:layout_width">wrap_content</item>
<item name="android:elevation">5dp</item>
<item name="android:paddingStart">12dp</item>
<item name="android:paddingEnd">12dp</item>
<item name="android:paddingTop">8dp</item>
<item name="android:paddingBottom">8dp</item>
<item name="android:textColor">?attr/colorOnPrimary</item>
<item name="android:textSize">15sp</item>
</style>
<!--===========-->
<!--Preferences-->
<!--===========-->

View file

@ -1,20 +1,20 @@
[versions]
agp_version = "8.1.1"
lifecycle_version = "2.6.1"
paging_version = "3.2.0"
lifecycle_version = "2.6.2"
paging_version = "3.2.1"
[libraries]
gradle = { module = "com.android.tools.build:gradle", version.ref = "agp_version" }
annotation = "androidx.annotation:annotation:1.7.0-rc01"
annotation = "androidx.annotation:annotation:1.7.0"
appcompat = "androidx.appcompat:appcompat:1.6.1"
biometricktx = "androidx.biometric:biometric-ktx:1.2.0-alpha05"
constraintlayout = "androidx.constraintlayout:constraintlayout:2.1.4"
corektx = "androidx.core:core-ktx:1.12.0-rc01"
corektx = "androidx.core:core-ktx:1.12.0"
splashscreen = "androidx.core:core-splashscreen:1.0.1"
recyclerview = "androidx.recyclerview:recyclerview:1.3.1"
viewpager = "androidx.viewpager:viewpager:1.1.0-alpha01"
glance = "androidx.glance:glance-appwidget:1.0.0-rc01"
glance = "androidx.glance:glance-appwidget:1.0.0"
profileinstaller = "androidx.profileinstaller:profileinstaller:1.3.1"
mediasession = "androidx.media:media:1.6.0"

View file

@ -5,7 +5,7 @@ shizuku_version = "12.2.0"
sqlite = "2.3.1"
sqldelight = "2.0.0"
leakcanary = "2.12"
voyager = "1.0.0-rc06"
voyager = "1.0.0-rc07"
richtext = "0.17.0"
[libraries]
@ -63,7 +63,7 @@ swipe = "me.saket.swipe:swipe:1.2.0"
logcat = "com.squareup.logcat:logcat:0.1"
acra-http = "ch.acra:acra-http:5.11.1"
acra-http = "ch.acra:acra-http:5.11.2"
aboutLibraries-core = { module = "com.mikepenz:aboutlibraries-core", version.ref = "aboutlib_version" }
aboutLibraries-gradle = { module = "com.mikepenz.aboutlibraries.plugin:aboutlibraries-plugin", version.ref = "aboutlib_version" }
@ -82,7 +82,7 @@ sqldelight-dialects-sql = { module = "app.cash.sqldelight:sqlite-3-38-dialect",
sqldelight-gradle = { module = "app.cash.sqldelight:gradle-plugin", version.ref = "sqldelight" }
junit = "org.junit.jupiter:junit-jupiter:5.10.0"
kotest-assertions = "io.kotest:kotest-assertions-core:5.6.2"
kotest-assertions = "io.kotest:kotest-assertions-core:5.7.2"
mockk = "io.mockk:mockk:1.13.7"
voyager-navigator = { module = "cafe.adriel.voyager:voyager-navigator", version.ref = "voyager" }

View file

@ -26,7 +26,7 @@
<string name="pref_bottom_nav_no_history">Move History to the More tab</string>
<string name="pref_bottom_nav_no_updates">Move Updates to the More tab</string>
<string name="pref_bottom_nav_no_manga">Move Manga to the More tab</string>
<string name="unlock_app">Unlock Aniyomi</string>
<string name="unlock_app_title">Unlock Aniyomi</string>
<string name="action_filter_unseen">Unseen</string>
<string name="action_global_manga_search">Global Manga Search</string>
<string name="action_global_anime_search">Global Anime Search</string>
@ -77,12 +77,12 @@
<string name="default_anime_category">Default anime category</string>
<string name="pref_manga_library_update_categories_details">Manga in excluded categories will not be updated even if they are also in included categories.</string>
<string name="pref_anime_library_update_categories_details">Anime in excluded categories will not be updated even if they are also in included categories.</string>
<string name="untrusted_extension_message">This extension was signed with an untrusted certificate and wasn\'t activated.\n\nA malicious extension could read any login credentials stored in Aniyomi or execute arbitrary code.\n\nBy trusting this certificate you accept these risks.</string>
<string name="unofficial_extension_message_aniyomi">This extension is not from the official Aniyomi extensions list.</string>
<string name="untrusted_extension_message">This extension was signed with an untrusted certificate and wasn\'t activated.\n\nA malicious extension could read any stored login credentials or execute arbitrary code.\n\nBy trusting this certificate you accept these risks.</string>
<string name="unofficial_extension_message_aniyomi">This extension is not from the official list.</string>
<string name="rotation_reverse_landscape">Reverse landscape</string>
<string name="rotation_sensor_portrait">Sensor portrait</string>
<string name="rotation_sensor_landscape">Sensor landscape</string>
<string name="unofficial_anime_extension_message">This extension is not from the official Aniyomi extensions list.</string>
<string name="unofficial_anime_extension_message">This extension is not from the official list.</string>
<!-- Player section -->
<string name="pref_category_player">Player</string>
<string name="pref_category_progress">Progress</string>
@ -241,7 +241,7 @@
</plurals>
<string name="information_no_recent_anime">Nothing watched recently</string>
<!-- Do not translate "WebView" -->
<string name="information_webview_required">WebView is required for Aniyomi</string>
<string name="information_webview_required">WebView is required for the app to function</string>
<string name="episode_settings_updated">Updated default episode settings</string>
<string name="download_notifier_download_paused_chapters">Chapter download paused</string>
<string name="download_notifier_download_paused_episodes">Episode download paused</string>

View file

@ -1,2 +1,73 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest />
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<receiver
android:name="tachiyomi.presentation.widget.entries.manga.MangaUpdatesGridGlanceReceiver"
android:enabled="@bool/glance_appwidget_available"
android:exported="false"
android:label="@string/label_recent_updates">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/updates_grid_homescreen_widget_info" />
</receiver>
<receiver
android:name="tachiyomi.presentation.widget.entries.anime.AnimeUpdatesGridGlanceReceiver"
android:enabled="@bool/glance_appwidget_available"
android:exported="false"
android:label="@string/label_recent_anime_updates">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/updates_grid_homescreen_widget_info" />
</receiver>
<receiver
android:name="tachiyomi.presentation.widget.entries.manga.MangaUpdatesGridCoverScreenGlanceReceiver"
android:enabled="@bool/glance_appwidget_available"
android:exported="false"
android:label="@string/label_recent_updates">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/updates_grid_lockscreen_widget_info" />
<meta-data
android:name="com.samsung.android.appwidget.provider"
android:resource="@xml/updates_grid_samsung_cover_widget_info" />
<meta-data
android:name="com.samsung.android.sdk.subscreen.widget.support_visibility_callback"
android:value="true" />
</receiver>
<receiver
android:name="tachiyomi.presentation.widget.entries.anime.AnimeUpdatesGridCoverScreenGlanceReceiver"
android:enabled="@bool/glance_appwidget_available"
android:exported="false"
android:label="@string/label_recent_anime_updates">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/updates_grid_lockscreen_widget_info" />
<meta-data
android:name="com.samsung.android.appwidget.provider"
android:resource="@xml/updates_grid_samsung_cover_widget_info" />
<meta-data
android:name="com.samsung.android.sdk.subscreen.widget.support_visibility_callback"
android:value="true" />
</receiver>
</application>
</manifest>

View file

@ -17,25 +17,26 @@ import androidx.glance.text.TextStyle
import androidx.glance.unit.ColorProvider
import eu.kanade.tachiyomi.core.Constants
import tachiyomi.presentation.widget.R
import tachiyomi.presentation.widget.entries.anime.ContainerModifier
import tachiyomi.presentation.widget.util.stringResource
@Composable
fun LockedAnimeWidget() {
fun LockedAnimeWidget(
foreground: ColorProvider,
modifier: GlanceModifier = GlanceModifier,
) {
val intent = Intent(LocalContext.current, Class.forName(Constants.MAIN_ACTIVITY)).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
Box(
modifier = GlanceModifier
modifier = modifier
.clickable(actionStartActivity(intent))
.then(ContainerModifier)
.padding(8.dp),
contentAlignment = Alignment.Center,
) {
Text(
text = stringResource(R.string.appwidget_unavailable_locked),
style = TextStyle(
color = ColorProvider(R.color.appwidget_on_secondary_container),
color = foreground,
fontSize = 12.sp,
textAlign = TextAlign.Center,
),

View file

@ -3,6 +3,7 @@ package tachiyomi.presentation.widget.components.anime
import android.content.Intent
import android.graphics.Bitmap
import androidx.compose.runtime.Composable
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.glance.GlanceModifier
import androidx.glance.LocalContext
@ -14,30 +15,45 @@ import androidx.glance.layout.Alignment
import androidx.glance.layout.Box
import androidx.glance.layout.Column
import androidx.glance.layout.Row
import androidx.glance.layout.fillMaxHeight
import androidx.glance.layout.fillMaxWidth
import androidx.glance.layout.padding
import androidx.glance.text.Text
import androidx.glance.text.TextStyle
import androidx.glance.unit.ColorProvider
import eu.kanade.tachiyomi.core.Constants
import tachiyomi.presentation.widget.R
import tachiyomi.presentation.widget.entries.anime.ContainerModifier
import tachiyomi.presentation.widget.util.calculateRowAndColumnCount
import tachiyomi.presentation.widget.util.stringResource
@Composable
fun UpdatesAnimeWidget(data: List<Pair<Long, Bitmap?>>?) {
val (rowCount, columnCount) = LocalSize.current.calculateRowAndColumnCount()
fun UpdatesAnimeWidget(
data: List<Pair<Long, Bitmap?>>?,
modifier: GlanceModifier = GlanceModifier,
contentColor: ColorProvider,
topPadding: Dp,
bottomPadding: Dp,
) {
Box(
contentAlignment = Alignment.Center,
modifier = modifier,
) {
if (data == null) {
CircularProgressIndicator(color = contentColor)
} else if (data.isEmpty()) {
Text(
text = stringResource(R.string.information_no_recent),
style = TextStyle(color = contentColor),
)
} else {
val (rowCount, columnCount) = LocalSize.current.calculateRowAndColumnCount(topPadding, bottomPadding)
Column(
modifier = ContainerModifier,
modifier = GlanceModifier.fillMaxHeight(),
verticalAlignment = Alignment.CenterVertically,
horizontalAlignment = Alignment.CenterHorizontally,
) {
if (data == null) {
CircularProgressIndicator()
} else if (data.isEmpty()) {
Text(text = stringResource(R.string.information_no_recent))
} else {
(0 until rowCount).forEach { i ->
val coverRow = (0 until columnCount).mapNotNull { j ->
(0..<rowCount).forEach { i ->
val coverRow = (0..<columnCount).mapNotNull { j ->
data.getOrNull(j + (i * columnCount))
}
if (coverRow.isNotEmpty()) {
@ -54,10 +70,7 @@ fun UpdatesAnimeWidget(data: List<Pair<Long, Bitmap?>>?) {
.padding(horizontal = 3.dp),
contentAlignment = Alignment.Center,
) {
val intent = Intent(
LocalContext.current,
Class.forName(Constants.MAIN_ACTIVITY),
).apply {
val intent = Intent(LocalContext.current, Class.forName(Constants.MAIN_ACTIVITY)).apply {
action = Constants.SHORTCUT_ANIME
putExtra(Constants.ANIME_EXTRA, animeId)
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
@ -78,3 +91,4 @@ fun UpdatesAnimeWidget(data: List<Pair<Long, Bitmap?>>?) {
}
}
}
}

View file

@ -17,25 +17,26 @@ import androidx.glance.text.TextStyle
import androidx.glance.unit.ColorProvider
import eu.kanade.tachiyomi.core.Constants
import tachiyomi.presentation.widget.R
import tachiyomi.presentation.widget.entries.manga.ContainerModifier
import tachiyomi.presentation.widget.util.stringResource
@Composable
fun LockedMangaWidget() {
fun LockedMangaWidget(
foreground: ColorProvider,
modifier: GlanceModifier = GlanceModifier,
) {
val intent = Intent(LocalContext.current, Class.forName(Constants.MAIN_ACTIVITY)).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
Box(
modifier = GlanceModifier
modifier = modifier
.clickable(actionStartActivity(intent))
.then(ContainerModifier)
.padding(8.dp),
contentAlignment = Alignment.Center,
) {
Text(
text = stringResource(R.string.appwidget_unavailable_locked),
style = TextStyle(
color = ColorProvider(R.color.appwidget_on_secondary_container),
color = foreground,
fontSize = 12.sp,
textAlign = TextAlign.Center,
),

View file

@ -3,6 +3,7 @@ package tachiyomi.presentation.widget.components.manga
import android.content.Intent
import android.graphics.Bitmap
import androidx.compose.runtime.Composable
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.glance.GlanceModifier
import androidx.glance.LocalContext
@ -14,28 +15,43 @@ import androidx.glance.layout.Alignment
import androidx.glance.layout.Box
import androidx.glance.layout.Column
import androidx.glance.layout.Row
import androidx.glance.layout.fillMaxHeight
import androidx.glance.layout.fillMaxWidth
import androidx.glance.layout.padding
import androidx.glance.text.Text
import androidx.glance.text.TextStyle
import androidx.glance.unit.ColorProvider
import eu.kanade.tachiyomi.core.Constants
import tachiyomi.presentation.widget.R
import tachiyomi.presentation.widget.entries.manga.ContainerModifier
import tachiyomi.presentation.widget.util.calculateRowAndColumnCount
import tachiyomi.presentation.widget.util.stringResource
@Composable
fun UpdatesMangaWidget(data: List<Pair<Long, Bitmap?>>?) {
val (rowCount, columnCount) = LocalSize.current.calculateRowAndColumnCount()
fun UpdatesMangaWidget(
data: List<Pair<Long, Bitmap?>>?,
modifier: GlanceModifier = GlanceModifier,
contentColor: ColorProvider,
topPadding: Dp,
bottomPadding: Dp,
) {
Box(
contentAlignment = Alignment.Center,
modifier = modifier,
) {
if (data == null) {
CircularProgressIndicator(color = contentColor)
} else if (data.isEmpty()) {
Text(
text = stringResource(R.string.information_no_recent),
style = TextStyle(color = contentColor),
)
} else {
val (rowCount, columnCount) = LocalSize.current.calculateRowAndColumnCount(topPadding, bottomPadding)
Column(
modifier = ContainerModifier,
modifier = GlanceModifier.fillMaxHeight(),
verticalAlignment = Alignment.CenterVertically,
horizontalAlignment = Alignment.CenterHorizontally,
) {
if (data == null) {
CircularProgressIndicator()
} else if (data.isEmpty()) {
Text(text = stringResource(R.string.information_no_recent))
} else {
(0..<rowCount).forEach { i ->
val coverRow = (0..<columnCount).mapNotNull { j ->
data.getOrNull(j + (i * columnCount))
@ -54,10 +70,7 @@ fun UpdatesMangaWidget(data: List<Pair<Long, Bitmap?>>?) {
.padding(horizontal = 3.dp),
contentAlignment = Alignment.Center,
) {
val intent = Intent(
LocalContext.current,
Class.forName(Constants.MAIN_ACTIVITY),
).apply {
val intent = Intent(LocalContext.current, Class.forName(Constants.MAIN_ACTIVITY)).apply {
action = Constants.SHORTCUT_MANGA
putExtra(Constants.MANGA_EXTRA, mangaId)
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
@ -78,3 +91,4 @@ fun UpdatesMangaWidget(data: List<Pair<Long, Bitmap?>>?) {
}
}
}
}

View file

@ -0,0 +1,9 @@
package tachiyomi.presentation.widget.entries.anime
import androidx.glance.appwidget.GlanceAppWidget
import androidx.glance.appwidget.GlanceAppWidgetReceiver
class AnimeUpdatesGridCoverScreenGlanceReceiver : GlanceAppWidgetReceiver() {
override val glanceAppWidget: GlanceAppWidget
get() = AnimeUpdatesGridCoverScreenGlanceWidget()
}

View file

@ -0,0 +1,14 @@
package tachiyomi.presentation.widget.entries.anime
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.glance.ImageProvider
import androidx.glance.unit.ColorProvider
import tachiyomi.presentation.widget.R
class AnimeUpdatesGridCoverScreenGlanceWidget : BaseAnimeUpdatesGridGlanceWidget() {
override val foreground = ColorProvider(Color.White)
override val background = ImageProvider(R.drawable.appwidget_coverscreen_background)
override val topPadding = 0.dp
override val bottomPadding = 24.dp
}

View file

@ -4,5 +4,6 @@ import androidx.glance.appwidget.GlanceAppWidget
import androidx.glance.appwidget.GlanceAppWidgetReceiver
class AnimeUpdatesGridGlanceReceiver : GlanceAppWidgetReceiver() {
override val glanceAppWidget: GlanceAppWidget = AnimeUpdatesGridGlanceWidget()
override val glanceAppWidget: GlanceAppWidget
get() = AnimeUpdatesGridGlanceWidget()
}

View file

@ -1,137 +1,14 @@
package tachiyomi.presentation.widget.entries.anime
import android.app.Application
import android.content.Context
import android.graphics.Bitmap
import android.os.Build
import androidx.core.graphics.drawable.toBitmap
import androidx.glance.GlanceId
import androidx.glance.GlanceModifier
import androidx.compose.ui.unit.dp
import androidx.glance.ImageProvider
import androidx.glance.appwidget.GlanceAppWidget
import androidx.glance.appwidget.GlanceAppWidgetManager
import androidx.glance.appwidget.SizeMode
import androidx.glance.appwidget.appWidgetBackground
import androidx.glance.appwidget.provideContent
import androidx.glance.background
import androidx.glance.layout.fillMaxSize
import coil.executeBlocking
import coil.imageLoader
import coil.request.CachePolicy
import coil.request.ImageRequest
import coil.size.Precision
import coil.size.Scale
import coil.transform.RoundedCornersTransformation
import eu.kanade.tachiyomi.core.security.SecurityPreferences
import eu.kanade.tachiyomi.util.system.dpToPx
import tachiyomi.core.util.lang.withIOContext
import tachiyomi.domain.entries.anime.model.AnimeCover
import tachiyomi.domain.updates.anime.interactor.GetAnimeUpdates
import tachiyomi.domain.updates.anime.model.AnimeUpdatesWithRelations
import androidx.glance.unit.ColorProvider
import tachiyomi.presentation.widget.R
import tachiyomi.presentation.widget.components.anime.CoverHeight
import tachiyomi.presentation.widget.components.anime.CoverWidth
import tachiyomi.presentation.widget.components.anime.LockedAnimeWidget
import tachiyomi.presentation.widget.components.anime.UpdatesAnimeWidget
import tachiyomi.presentation.widget.util.appWidgetBackgroundRadius
import tachiyomi.presentation.widget.util.calculateRowAndColumnCount
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.util.Calendar
import java.util.Date
class AnimeUpdatesGridGlanceWidget(
private val context: Context = Injekt.get<Application>(),
private val getUpdates: GetAnimeUpdates = Injekt.get(),
private val preferences: SecurityPreferences = Injekt.get(),
) : GlanceAppWidget() {
private var data: List<Pair<Long, Bitmap?>>? = null
override val sizeMode = SizeMode.Exact
override suspend fun provideGlance(context: Context, id: GlanceId) {
val locked = preferences.useAuthenticator().get()
if (!locked) loadData()
provideContent {
// If app lock enabled, don't do anything
if (locked) {
LockedAnimeWidget()
return@provideContent
class AnimeUpdatesGridGlanceWidget : BaseAnimeUpdatesGridGlanceWidget() {
override val foreground = ColorProvider(R.color.appwidget_on_secondary_container)
override val background = ImageProvider(R.drawable.appwidget_background)
override val topPadding = 0.dp
override val bottomPadding = 0.dp
}
UpdatesAnimeWidget(data)
}
}
private suspend fun loadData() {
val manager = GlanceAppWidgetManager(context)
val ids = manager.getGlanceIds(this@AnimeUpdatesGridGlanceWidget::class.java)
if (ids.isEmpty()) return
withIOContext {
val updates = getUpdates.await(
seen = false,
after = DateLimit.timeInMillis,
)
val (rowCount, columnCount) = ids
.flatMap { manager.getAppWidgetSizes(it) }
.maxBy { it.height.value * it.width.value }
.calculateRowAndColumnCount()
data = prepareList(updates, rowCount * columnCount)
}
}
private fun prepareList(processList: List<AnimeUpdatesWithRelations>, take: Int): List<Pair<Long, Bitmap?>> {
// Resize to cover size
val widthPx = CoverWidth.value.toInt().dpToPx
val heightPx = CoverHeight.value.toInt().dpToPx
val roundPx = context.resources.getDimension(R.dimen.appwidget_inner_radius)
return processList
.distinctBy { it.animeId }
.take(take)
.map { animeupdatesView ->
val request = ImageRequest.Builder(context)
.data(
AnimeCover(
animeId = animeupdatesView.animeId,
sourceId = animeupdatesView.sourceId,
isAnimeFavorite = true,
url = animeupdatesView.coverData.url,
lastModified = animeupdatesView.coverData.lastModified,
),
)
.memoryCachePolicy(CachePolicy.DISABLED)
.precision(Precision.EXACT)
.size(widthPx, heightPx)
.scale(Scale.FILL)
.let {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
it.transformations(RoundedCornersTransformation(roundPx))
} else {
it // Handled by system
}
}
.build()
Pair(
animeupdatesView.animeId,
context.imageLoader.executeBlocking(request).drawable?.toBitmap(),
)
}
}
companion object {
val DateLimit: Calendar
get() = Calendar.getInstance().apply {
time = Date()
add(Calendar.MONTH, -3)
}
}
}
val ContainerModifier = GlanceModifier
.fillMaxSize()
.background(ImageProvider(R.drawable.appwidget_background))
.appWidgetBackground()
.appWidgetBackgroundRadius()

View file

@ -0,0 +1,154 @@
package tachiyomi.presentation.widget.entries.anime
import android.app.Application
import android.content.Context
import android.graphics.Bitmap
import android.os.Build
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.unit.Dp
import androidx.core.graphics.drawable.toBitmap
import androidx.glance.GlanceId
import androidx.glance.GlanceModifier
import androidx.glance.ImageProvider
import androidx.glance.appwidget.GlanceAppWidget
import androidx.glance.appwidget.GlanceAppWidgetManager
import androidx.glance.appwidget.SizeMode
import androidx.glance.appwidget.appWidgetBackground
import androidx.glance.appwidget.provideContent
import androidx.glance.background
import androidx.glance.layout.fillMaxSize
import androidx.glance.layout.padding
import androidx.glance.unit.ColorProvider
import coil.executeBlocking
import coil.imageLoader
import coil.request.CachePolicy
import coil.request.ImageRequest
import coil.size.Precision
import coil.size.Scale
import coil.transform.RoundedCornersTransformation
import eu.kanade.tachiyomi.core.security.SecurityPreferences
import eu.kanade.tachiyomi.util.system.dpToPx
import kotlinx.coroutines.flow.map
import tachiyomi.core.util.lang.withIOContext
import tachiyomi.domain.entries.anime.model.AnimeCover
import tachiyomi.domain.updates.anime.interactor.GetAnimeUpdates
import tachiyomi.domain.updates.anime.model.AnimeUpdatesWithRelations
import tachiyomi.presentation.widget.R
import tachiyomi.presentation.widget.components.anime.CoverHeight
import tachiyomi.presentation.widget.components.anime.CoverWidth
import tachiyomi.presentation.widget.components.anime.LockedAnimeWidget
import tachiyomi.presentation.widget.components.anime.UpdatesAnimeWidget
import tachiyomi.presentation.widget.util.appWidgetBackgroundRadius
import tachiyomi.presentation.widget.util.calculateRowAndColumnCount
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.util.Calendar
import java.util.Date
abstract class BaseAnimeUpdatesGridGlanceWidget(
private val context: Context = Injekt.get<Application>(),
private val getUpdates: GetAnimeUpdates = Injekt.get(),
private val preferences: SecurityPreferences = Injekt.get(),
) : GlanceAppWidget() {
override val sizeMode = SizeMode.Exact
abstract val foreground: ColorProvider
abstract val background: ImageProvider
abstract val topPadding: Dp
abstract val bottomPadding: Dp
override suspend fun provideGlance(context: Context, id: GlanceId) {
val locked = preferences.useAuthenticator().get()
val containerModifier = GlanceModifier
.fillMaxSize()
.background(background)
.appWidgetBackground()
.padding(top = topPadding, bottom = bottomPadding)
.appWidgetBackgroundRadius()
val manager = GlanceAppWidgetManager(context)
val ids = manager.getGlanceIds(javaClass)
val (rowCount, columnCount) = ids
.flatMap { manager.getAppWidgetSizes(it) }
.maxBy { it.height.value * it.width.value }
.calculateRowAndColumnCount(topPadding, bottomPadding)
provideContent {
// If app lock enabled, don't do anything
if (locked) {
LockedAnimeWidget(
foreground = foreground,
modifier = containerModifier,
)
return@provideContent
}
val flow = remember {
getUpdates
.subscribe(false, DateLimit.timeInMillis)
.map { rawData ->
rawData.prepareData(rowCount, columnCount)
}
}
val data by flow.collectAsState(initial = null)
UpdatesAnimeWidget(
data = data,
modifier = containerModifier,
contentColor = foreground,
topPadding = topPadding,
bottomPadding = bottomPadding,
)
}
}
private suspend fun List<AnimeUpdatesWithRelations>.prepareData(
rowCount: Int,
columnCount: Int,
): List<Pair<Long, Bitmap?>> {
// Resize to cover size
val widthPx = CoverWidth.value.toInt().dpToPx
val heightPx = CoverHeight.value.toInt().dpToPx
val roundPx = context.resources.getDimension(R.dimen.appwidget_inner_radius)
return withIOContext {
this@prepareData
.distinctBy { it.animeId }
.take(rowCount * columnCount)
.map { updatesView ->
val request = ImageRequest.Builder(context)
.data(
AnimeCover(
animeId = updatesView.animeId,
sourceId = updatesView.sourceId,
isAnimeFavorite = true,
url = updatesView.coverData.url,
lastModified = updatesView.coverData.lastModified,
),
)
.memoryCachePolicy(CachePolicy.DISABLED)
.precision(Precision.EXACT)
.size(widthPx, heightPx)
.scale(Scale.FILL)
.let {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
it.transformations(RoundedCornersTransformation(roundPx))
} else {
it // Handled by system
}
}
.build()
Pair(updatesView.animeId, context.imageLoader.executeBlocking(request).drawable?.toBitmap())
}
}
}
companion object {
val DateLimit: Calendar
get() = Calendar.getInstance().apply {
time = Date()
add(Calendar.MONTH, -3)
}
}
}

View file

@ -22,7 +22,7 @@ class TachiyomiAnimeWidgetManager(
combine(
getUpdates.subscribe(
seen = false,
after = AnimeUpdatesGridGlanceWidget.DateLimit.timeInMillis,
after = BaseAnimeUpdatesGridGlanceWidget.DateLimit.timeInMillis,
),
securityPreferences.useAuthenticator().changes(),
transform = { a, _ -> a },
@ -32,6 +32,7 @@ class TachiyomiAnimeWidgetManager(
.onEach {
try {
AnimeUpdatesGridGlanceWidget().updateAll(this)
AnimeUpdatesGridCoverScreenGlanceWidget().updateAll(this)
} catch (e: Exception) {
logcat(LogPriority.ERROR, e) { "Failed to update widget" }
}

View file

@ -0,0 +1,154 @@
package tachiyomi.presentation.widget.entries.manga
import android.app.Application
import android.content.Context
import android.graphics.Bitmap
import android.os.Build
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.unit.Dp
import androidx.core.graphics.drawable.toBitmap
import androidx.glance.GlanceId
import androidx.glance.GlanceModifier
import androidx.glance.ImageProvider
import androidx.glance.appwidget.GlanceAppWidget
import androidx.glance.appwidget.GlanceAppWidgetManager
import androidx.glance.appwidget.SizeMode
import androidx.glance.appwidget.appWidgetBackground
import androidx.glance.appwidget.provideContent
import androidx.glance.background
import androidx.glance.layout.fillMaxSize
import androidx.glance.layout.padding
import androidx.glance.unit.ColorProvider
import coil.executeBlocking
import coil.imageLoader
import coil.request.CachePolicy
import coil.request.ImageRequest
import coil.size.Precision
import coil.size.Scale
import coil.transform.RoundedCornersTransformation
import eu.kanade.tachiyomi.core.security.SecurityPreferences
import eu.kanade.tachiyomi.util.system.dpToPx
import kotlinx.coroutines.flow.map
import tachiyomi.core.util.lang.withIOContext
import tachiyomi.domain.entries.manga.model.MangaCover
import tachiyomi.domain.updates.manga.interactor.GetMangaUpdates
import tachiyomi.domain.updates.manga.model.MangaUpdatesWithRelations
import tachiyomi.presentation.widget.R
import tachiyomi.presentation.widget.components.manga.CoverHeight
import tachiyomi.presentation.widget.components.manga.CoverWidth
import tachiyomi.presentation.widget.components.manga.LockedMangaWidget
import tachiyomi.presentation.widget.components.manga.UpdatesMangaWidget
import tachiyomi.presentation.widget.util.appWidgetBackgroundRadius
import tachiyomi.presentation.widget.util.calculateRowAndColumnCount
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.util.Calendar
import java.util.Date
abstract class BaseMangaUpdatesGridGlanceWidget(
private val context: Context = Injekt.get<Application>(),
private val getUpdates: GetMangaUpdates = Injekt.get(),
private val preferences: SecurityPreferences = Injekt.get(),
) : GlanceAppWidget() {
override val sizeMode = SizeMode.Exact
abstract val foreground: ColorProvider
abstract val background: ImageProvider
abstract val topPadding: Dp
abstract val bottomPadding: Dp
override suspend fun provideGlance(context: Context, id: GlanceId) {
val locked = preferences.useAuthenticator().get()
val containerModifier = GlanceModifier
.fillMaxSize()
.background(background)
.appWidgetBackground()
.padding(top = topPadding, bottom = bottomPadding)
.appWidgetBackgroundRadius()
val manager = GlanceAppWidgetManager(context)
val ids = manager.getGlanceIds(javaClass)
val (rowCount, columnCount) = ids
.flatMap { manager.getAppWidgetSizes(it) }
.maxBy { it.height.value * it.width.value }
.calculateRowAndColumnCount(topPadding, bottomPadding)
provideContent {
// If app lock enabled, don't do anything
if (locked) {
LockedMangaWidget(
foreground = foreground,
modifier = containerModifier,
)
return@provideContent
}
val flow = remember {
getUpdates
.subscribe(false, DateLimit.timeInMillis)
.map { rawData ->
rawData.prepareData(rowCount, columnCount)
}
}
val data by flow.collectAsState(initial = null)
UpdatesMangaWidget(
data = data,
modifier = containerModifier,
contentColor = foreground,
topPadding = topPadding,
bottomPadding = bottomPadding,
)
}
}
private suspend fun List<MangaUpdatesWithRelations>.prepareData(
rowCount: Int,
columnCount: Int,
): List<Pair<Long, Bitmap?>> {
// Resize to cover size
val widthPx = CoverWidth.value.toInt().dpToPx
val heightPx = CoverHeight.value.toInt().dpToPx
val roundPx = context.resources.getDimension(R.dimen.appwidget_inner_radius)
return withIOContext {
this@prepareData
.distinctBy { it.mangaId }
.take(rowCount * columnCount)
.map { updatesView ->
val request = ImageRequest.Builder(context)
.data(
MangaCover(
mangaId = updatesView.mangaId,
sourceId = updatesView.sourceId,
isMangaFavorite = true,
url = updatesView.coverData.url,
lastModified = updatesView.coverData.lastModified,
),
)
.memoryCachePolicy(CachePolicy.DISABLED)
.precision(Precision.EXACT)
.size(widthPx, heightPx)
.scale(Scale.FILL)
.let {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
it.transformations(RoundedCornersTransformation(roundPx))
} else {
it // Handled by system
}
}
.build()
Pair(updatesView.mangaId, context.imageLoader.executeBlocking(request).drawable?.toBitmap())
}
}
}
companion object {
val DateLimit: Calendar
get() = Calendar.getInstance().apply {
time = Date()
add(Calendar.MONTH, -3)
}
}
}

View file

@ -0,0 +1,9 @@
package tachiyomi.presentation.widget.entries.manga
import androidx.glance.appwidget.GlanceAppWidget
import androidx.glance.appwidget.GlanceAppWidgetReceiver
class MangaUpdatesGridCoverScreenGlanceReceiver : GlanceAppWidgetReceiver() {
override val glanceAppWidget: GlanceAppWidget
get() = MangaUpdatesGridCoverScreenGlanceWidget()
}

View file

@ -0,0 +1,14 @@
package tachiyomi.presentation.widget.entries.manga
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.glance.ImageProvider
import androidx.glance.unit.ColorProvider
import tachiyomi.presentation.widget.R
class MangaUpdatesGridCoverScreenGlanceWidget : BaseMangaUpdatesGridGlanceWidget() {
override val foreground = ColorProvider(Color.White)
override val background = ImageProvider(R.drawable.appwidget_coverscreen_background)
override val topPadding = 0.dp
override val bottomPadding = 24.dp
}

View file

@ -4,5 +4,6 @@ import androidx.glance.appwidget.GlanceAppWidget
import androidx.glance.appwidget.GlanceAppWidgetReceiver
class MangaUpdatesGridGlanceReceiver : GlanceAppWidgetReceiver() {
override val glanceAppWidget: GlanceAppWidget = MangaUpdatesGridGlanceWidget()
override val glanceAppWidget: GlanceAppWidget
get() = MangaUpdatesGridGlanceWidget()
}

View file

@ -1,137 +1,13 @@
package tachiyomi.presentation.widget.entries.manga
import android.app.Application
import android.content.Context
import android.graphics.Bitmap
import android.os.Build
import androidx.core.graphics.drawable.toBitmap
import androidx.glance.GlanceId
import androidx.glance.GlanceModifier
import androidx.compose.ui.unit.dp
import androidx.glance.ImageProvider
import androidx.glance.appwidget.GlanceAppWidget
import androidx.glance.appwidget.GlanceAppWidgetManager
import androidx.glance.appwidget.SizeMode
import androidx.glance.appwidget.appWidgetBackground
import androidx.glance.appwidget.provideContent
import androidx.glance.background
import androidx.glance.layout.fillMaxSize
import coil.executeBlocking
import coil.imageLoader
import coil.request.CachePolicy
import coil.request.ImageRequest
import coil.size.Precision
import coil.size.Scale
import coil.transform.RoundedCornersTransformation
import eu.kanade.tachiyomi.core.security.SecurityPreferences
import eu.kanade.tachiyomi.util.system.dpToPx
import tachiyomi.core.util.lang.withIOContext
import tachiyomi.domain.entries.manga.model.MangaCover
import tachiyomi.domain.updates.manga.interactor.GetMangaUpdates
import tachiyomi.domain.updates.manga.model.MangaUpdatesWithRelations
import androidx.glance.unit.ColorProvider
import tachiyomi.presentation.widget.R
import tachiyomi.presentation.widget.components.manga.CoverHeight
import tachiyomi.presentation.widget.components.manga.CoverWidth
import tachiyomi.presentation.widget.components.manga.LockedMangaWidget
import tachiyomi.presentation.widget.components.manga.UpdatesMangaWidget
import tachiyomi.presentation.widget.util.appWidgetBackgroundRadius
import tachiyomi.presentation.widget.util.calculateRowAndColumnCount
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.util.Calendar
import java.util.Date
class MangaUpdatesGridGlanceWidget(
private val context: Context = Injekt.get<Application>(),
private val getUpdates: GetMangaUpdates = Injekt.get(),
private val preferences: SecurityPreferences = Injekt.get(),
) : GlanceAppWidget() {
private var data: List<Pair<Long, Bitmap?>>? = null
override val sizeMode = SizeMode.Exact
override suspend fun provideGlance(context: Context, id: GlanceId) {
val locked = preferences.useAuthenticator().get()
if (!locked) loadData()
provideContent {
// If app lock enabled, don't do anything
if (locked) {
LockedMangaWidget()
return@provideContent
class MangaUpdatesGridGlanceWidget : BaseMangaUpdatesGridGlanceWidget() {
override val foreground = ColorProvider(R.color.appwidget_on_secondary_container)
override val background = ImageProvider(R.drawable.appwidget_background)
override val topPadding = 0.dp
override val bottomPadding = 0.dp
}
UpdatesMangaWidget(data)
}
}
private suspend fun loadData() {
val manager = GlanceAppWidgetManager(context)
val ids = manager.getGlanceIds(this@MangaUpdatesGridGlanceWidget::class.java)
if (ids.isEmpty()) return
withIOContext {
val updates = getUpdates.await(
read = false,
after = DateLimit.timeInMillis,
)
val (rowCount, columnCount) = ids
.flatMap { manager.getAppWidgetSizes(it) }
.maxBy { it.height.value * it.width.value }
.calculateRowAndColumnCount()
data = prepareList(updates, rowCount * columnCount)
}
}
private fun prepareList(processList: List<MangaUpdatesWithRelations>, take: Int): List<Pair<Long, Bitmap?>> {
// Resize to cover size
val widthPx = CoverWidth.value.toInt().dpToPx
val heightPx = CoverHeight.value.toInt().dpToPx
val roundPx = context.resources.getDimension(R.dimen.appwidget_inner_radius)
return processList
.distinctBy { it.mangaId }
.take(take)
.map { updatesView ->
val request = ImageRequest.Builder(context)
.data(
MangaCover(
mangaId = updatesView.mangaId,
sourceId = updatesView.sourceId,
isMangaFavorite = true,
url = updatesView.coverData.url,
lastModified = updatesView.coverData.lastModified,
),
)
.memoryCachePolicy(CachePolicy.DISABLED)
.precision(Precision.EXACT)
.size(widthPx, heightPx)
.scale(Scale.FILL)
.let {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
it.transformations(RoundedCornersTransformation(roundPx))
} else {
it // Handled by system
}
}
.build()
Pair(
updatesView.mangaId,
context.imageLoader.executeBlocking(request).drawable?.toBitmap(),
)
}
}
companion object {
val DateLimit: Calendar
get() = Calendar.getInstance().apply {
time = Date()
add(Calendar.MONTH, -3)
}
}
}
val ContainerModifier = GlanceModifier
.fillMaxSize()
.background(ImageProvider(R.drawable.appwidget_background))
.appWidgetBackground()
.appWidgetBackgroundRadius()

View file

@ -22,7 +22,7 @@ class TachiyomiMangaWidgetManager(
combine(
getUpdates.subscribe(
read = false,
after = MangaUpdatesGridGlanceWidget.DateLimit.timeInMillis,
after = BaseMangaUpdatesGridGlanceWidget.DateLimit.timeInMillis,
),
securityPreferences.useAuthenticator().changes(),
transform = { a, _ -> a },
@ -32,6 +32,7 @@ class TachiyomiMangaWidgetManager(
.onEach {
try {
MangaUpdatesGridGlanceWidget().updateAll(this)
MangaUpdatesGridCoverScreenGlanceWidget().updateAll(this)
} catch (e: Exception) {
logcat(LogPriority.ERROR, e) { "Failed to update widget" }
}

View file

@ -2,6 +2,7 @@ package tachiyomi.presentation.widget.util
import androidx.annotation.StringRes
import androidx.compose.runtime.Composable
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpSize
import androidx.glance.GlanceModifier
import androidx.glance.LocalContext
@ -33,9 +34,13 @@ fun stringResource(@StringRes id: Int): String {
*
* @return pair of row and column count
*/
internal fun DpSize.calculateRowAndColumnCount(): Pair<Int, Int> {
fun DpSize.calculateRowAndColumnCount(
topPadding: Dp,
bottomPadding: Dp,
): Pair<Int, Int> {
// Hack: Size provided by Glance manager is not reliable so take at least 1 row and 1 column
// Set max to 10 children each direction because of Glance limitation
val height = this.height - topPadding - bottomPadding
val rowCount = (height.value / 95).toInt().coerceIn(1, 10)
val columnCount = (width.value / 64).toInt().coerceIn(1, 10)
return Pair(rowCount, columnCount)

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/appwidget_coverscreen_background" />
</shape>

View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/appwidget_coverscreen_background">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="@string/loading"
android:textColor="@android:color/white" />
</FrameLayout>

View file

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="appwidget_background">@color/tachiyomi_surface</color>
<color name="appwidget_coverscreen_background">#00000000</color>
<color name="appwidget_on_background">@color/tachiyomi_onSurface</color>
<color name="appwidget_surface_variant">@color/tachiyomi_surfaceVariant</color>
<color name="appwidget_on_surface_variant">@color/tachiyomi_onSurfaceVariant</color>

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:description="@string/appwidget_updates_description"
android:previewImage="@drawable/updates_grid_coverscreen_widget_preview"
android:initialLayout="@layout/appwidget_coverscreen_loading"
android:resizeMode="horizontal|vertical"
android:widgetCategory="keyguard" />

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<samsung-appwidget-provider
display="sub_screen"
privacyWidget="true" />

View file

@ -27,7 +27,9 @@ interface AnimeSource {
/**
* Get the updated details for a anime.
*
* @since extensions-lib 1.4
* @param anime the anime to update.
* @return the updated anime.
*/
@Suppress("DEPRECATION")
suspend fun getAnimeDetails(anime: SAnime): SAnime {
@ -37,7 +39,9 @@ interface AnimeSource {
/**
* Get all the available episodes for a anime.
*
* @since extensions-lib 1.4
* @param anime the anime to update.
* @return the episodes for the anime.
*/
@Suppress("DEPRECATION")
suspend fun getEpisodeList(anime: SAnime): List<SEpisode> {
@ -48,7 +52,9 @@ interface AnimeSource {
* Get the list of videos a episode has. Pages should be returned
* in the expected order; the index is ignored.
*
* @since extensions-lib 1.4
* @param episode the episode.
* @return the videos for the episode.
*/
@Suppress("DEPRECATION")
suspend fun getVideoList(episode: SEpisode): List<Video> {

View file

@ -27,7 +27,9 @@ interface MangaSource {
/**
* Get the updated details for a manga.
*
* @since extensions-lib 1.4
* @param manga the manga to update.
* @return the updated manga.
*/
@Suppress("DEPRECATION")
suspend fun getMangaDetails(manga: SManga): SManga {
@ -37,7 +39,9 @@ interface MangaSource {
/**
* Get all the available chapters for a manga.
*
* @since extensions-lib 1.4
* @param manga the manga to update.
* @return the chapters for the manga.
*/
@Suppress("DEPRECATION")
suspend fun getChapterList(manga: SManga): List<SChapter> {
@ -48,7 +52,9 @@ interface MangaSource {
* Get the list of pages a chapter has. Pages should be returned
* in the expected order; the index is ignored.
*
* @since extensions-lib 1.4
* @param chapter the chapter.
* @return the pages for the chapter.
*/
@Suppress("DEPRECATION")
suspend fun getPageList(chapter: SChapter): List<Page> {