Compare commits

...

11 Commits
v6.0 ... v6.0.1

Author SHA1 Message Date
Koitharu
4b6b2c3e12 Fix favorites selector 2023-08-30 14:43:57 +03:00
Koitharu
51300e30bd Improve favicon loading 2023-08-30 14:41:44 +03:00
Koitharu
399ac07fb3 Fix storage usage calculation 2023-08-30 14:21:09 +03:00
Eryk Michalak
eeba161235 Translated using Weblate (Polish)
Currently translated at 100.0% (7 of 7 strings)

Translated using Weblate (Polish)

Currently translated at 95.5% (455 of 476 strings)

Co-authored-by: Eryk Michalak <gnu.ewm@protonmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/pl/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pl/
Translation: Kotatsu/Strings
Translation: Kotatsu/plurals
2023-08-30 12:45:08 +03:00
Nayuki
088a388812 Translated using Weblate (Thai)
Currently translated at 49.5% (236 of 476 strings)

Translated using Weblate (Thai)

Currently translated at 42.8% (204 of 476 strings)

Co-authored-by: Nayuki <me@nayuki.cyou>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/th/
Translation: Kotatsu/Strings
2023-08-30 12:45:08 +03:00
gallegonovato
943bba3ee8 Translated using Weblate (Spanish)
Currently translated at 100.0% (476 of 476 strings)

Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/
Translation: Kotatsu/Strings
2023-08-30 12:45:08 +03:00
Koitharu
18c3229200 Handle TooManyRequestsException during downloading 2023-08-29 20:23:20 +03:00
Koitharu
9b6f511ac6 Do not discard image requests in onViewRecycled 2023-08-29 17:35:00 +03:00
Koitharu
74ca19a931 Improve widgets ui #457 2023-08-26 18:35:49 +03:00
Koitharu
2684a7384e Restore covers using interceptor 2023-08-26 16:44:09 +03:00
Koitharu
2c561824ef Fix default reader mode option #468 #466 2023-08-25 13:26:48 +03:00
56 changed files with 620 additions and 303 deletions

View File

@@ -16,8 +16,8 @@ android {
applicationId 'org.koitharu.kotatsu'
minSdk = 21
targetSdk = 34
versionCode = 573
versionName = '6.0'
versionCode = 574
versionName = '6.0.1'
generatedDensities = []
testInstrumentationRunner "org.koitharu.kotatsu.HiltTestRunner"
ksp {

View File

@@ -148,13 +148,21 @@
android:name="org.koitharu.kotatsu.favourites.ui.categories.FavouriteCategoriesActivity"
android:label="@string/manage_categories" />
<activity
android:name="org.koitharu.kotatsu.widget.shelf.ShelfConfigActivity"
android:name="org.koitharu.kotatsu.widget.shelf.ShelfWidgetConfigActivity"
android:exported="true"
android:label="@string/manga_shelf">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
</intent-filter>
</activity>
<activity
android:name="org.koitharu.kotatsu.widget.recent.RecentWidgetConfigActivity"
android:exported="true"
android:label="@string/recent_manga">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
</intent-filter>
</activity>
<activity
android:name="org.koitharu.kotatsu.search.ui.multi.MultiSearchActivity"
android:label="@string/search" />

View File

@@ -9,7 +9,6 @@ import org.koitharu.kotatsu.core.ui.image.CoverSizeResolver
import org.koitharu.kotatsu.core.ui.list.AdapterDelegateClickListenerAdapter
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.util.ext.decodeRegion
import org.koitharu.kotatsu.core.util.ext.disposeImageRequest
import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.core.util.ext.source
@@ -40,8 +39,4 @@ fun bookmarkListAD(
enqueueWith(coil)
}
}
onViewRecycled {
binding.imageViewThumb.disposeImageRequest()
}
}

View File

@@ -9,7 +9,6 @@ import org.koitharu.kotatsu.core.ui.image.CoverSizeResolver
import org.koitharu.kotatsu.core.ui.list.AdapterDelegateClickListenerAdapter
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.util.ext.decodeRegion
import org.koitharu.kotatsu.core.util.ext.disposeImageRequest
import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.core.util.ext.source
@@ -42,8 +41,4 @@ fun bookmarkLargeAD(
}
binding.progressView.percent = item.percent
}
onViewRecycled {
binding.imageViewThumb.disposeImageRequest()
}
}

View File

@@ -1,28 +1,28 @@
package org.koitharu.kotatsu.browser.cloudflare
import android.annotation.SuppressLint
import android.content.Context
import androidx.core.app.NotificationChannelCompat
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.app.PendingIntentCompat
import androidx.core.net.toUri
import coil.EventListener
import coil.request.ErrorResult
import coil.request.ImageRequest
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException
import org.koitharu.kotatsu.core.util.ext.checkNotificationPermission
import org.koitharu.kotatsu.parsers.model.ContentType
class CaptchaNotifier(
private val context: Context,
) : ImageRequest.Listener {
) : EventListener {
@SuppressLint("MissingPermission")
fun notify(exception: CloudFlareProtectedException) {
val manager = NotificationManagerCompat.from(context)
if (!manager.areNotificationsEnabled()) {
if (!context.checkNotificationPermission()) {
return
}
val manager = NotificationManagerCompat.from(context)
val channel = NotificationChannelCompat.Builder(CHANNEL_ID, NotificationManagerCompat.IMPORTANCE_DEFAULT)
.setName(context.getString(R.string.captcha_required))
.setShowBadge(true)

View File

@@ -13,7 +13,6 @@ import coil.decode.SvgDecoder
import coil.disk.DiskCache
import coil.util.DebugLogger
import dagger.Binds
import dagger.Lazy
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@@ -26,6 +25,7 @@ import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import okhttp3.OkHttpClient
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.browser.cloudflare.CaptchaNotifier
import org.koitharu.kotatsu.core.cache.ContentCache
import org.koitharu.kotatsu.core.cache.MemoryContentCache
import org.koitharu.kotatsu.core.cache.StubContentCache
@@ -47,7 +47,7 @@ import org.koitharu.kotatsu.local.data.CacheDir
import org.koitharu.kotatsu.local.data.CbzFetcher
import org.koitharu.kotatsu.local.data.LocalStorageChanges
import org.koitharu.kotatsu.local.domain.model.LocalManga
import org.koitharu.kotatsu.main.domain.CoverRestorer
import org.koitharu.kotatsu.main.domain.CoverRestoreInterceptor
import org.koitharu.kotatsu.main.ui.protect.AppProtectHelper
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.reader.ui.thumbnails.MangaPageFetcher
@@ -91,7 +91,7 @@ interface AppModule {
mangaRepositoryFactory: MangaRepository.Factory,
imageProxyInterceptor: ImageProxyInterceptor,
pageFetcherFactory: MangaPageFetcher.Factory,
coverRestorerProvider: Lazy<CoverRestorer>,
coverRestoreInterceptor: CoverRestoreInterceptor,
): ImageLoader {
val diskCacheFactory = {
val rootDir = context.externalCacheDir ?: context.cacheDir
@@ -108,7 +108,7 @@ interface AppModule {
.diskCache(diskCacheFactory)
.logger(if (BuildConfig.DEBUG) DebugLogger() else null)
.allowRgb565(context.isLowRamDevice())
.eventListenerFactory { coverRestorerProvider.get() }
.eventListener(CaptchaNotifier(context))
.components(
ComponentRegistry.Builder()
.add(SvgDecoder.Factory())
@@ -116,6 +116,7 @@ interface AppModule {
.add(FaviconFetcher.Factory(context, okHttpClient, mangaRepositoryFactory))
.add(pageFetcherFactory)
.add(imageProxyInterceptor)
.add(coverRestoreInterceptor)
.build(),
).build()
}

View File

@@ -6,4 +6,8 @@ import java.util.Date
class TooManyRequestExceptions(
val url: String,
val retryAt: Date?,
) : IOException()
) : IOException() {
val retryAfter: Long
get() = if (retryAt == null) 0 else (retryAt.time - System.currentTimeMillis()).coerceAtLeast(0)
}

View File

@@ -14,6 +14,7 @@ import coil.network.HttpException
import coil.request.Options
import coil.size.Size
import coil.size.pxOrElse
import kotlinx.coroutines.ensureActive
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
@@ -25,11 +26,13 @@ import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException
import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
import org.koitharu.kotatsu.core.util.ext.writeAllCancellable
import org.koitharu.kotatsu.local.data.CacheDir
import org.koitharu.kotatsu.local.data.util.withExtraCloseable
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.util.await
import java.net.HttpURLConnection
import kotlin.coroutines.coroutineContext
private const val FALLBACK_SIZE = 9999 // largest icon
@@ -55,13 +58,16 @@ class FaviconFetcher(
options.size.height.pxOrElse { FALLBACK_SIZE },
)
var favicons = repo.getFavicons()
var lastError: Exception? = null
while (favicons.isNotEmpty()) {
val icon = favicons.find(sizePx) ?: throwNSEE()
coroutineContext.ensureActive()
val icon = favicons.find(sizePx) ?: throwNSEE(lastError)
val response = try {
loadIcon(icon.url, mangaSource)
} catch (e: CloudFlareProtectedException) {
throw e
} catch (e: HttpException) {
lastError = e
favicons -= icon
continue
}
@@ -75,7 +81,7 @@ class FaviconFetcher(
dataSource = response.toDataSource(),
)
}
throwNSEE()
throwNSEE(lastError)
}
private suspend fun loadIcon(url: String, source: MangaSource): Response {
@@ -105,14 +111,14 @@ class FaviconFetcher(
)
}
private fun writeToDiskCache(body: ResponseBody): DiskCache.Snapshot? {
private suspend fun writeToDiskCache(body: ResponseBody): DiskCache.Snapshot? {
if (!options.diskCachePolicy.writeEnabled || body.contentLength() == 0L) {
return null
}
val editor = diskCache.value?.openEditor(diskCacheKey) ?: return null
try {
fileSystem.write(editor.data) {
body.source().readAll(this)
writeAllCancellable(body.source())
}
return editor.commitAndOpenSnapshot()
} catch (e: Throwable) {
@@ -154,7 +160,13 @@ class FaviconFetcher(
append(height.toString())
}
private fun throwNSEE(): Nothing = throw NoSuchElementException("No favicons found")
private fun throwNSEE(lastError: Exception?): Nothing {
if (lastError != null) {
throw lastError
} else {
throw NoSuchElementException("No favicons found")
}
}
class Factory(
context: Context,

View File

@@ -1,15 +1,38 @@
package org.koitharu.kotatsu.core.prefs
import android.appwidget.AppWidgetProvider
import android.content.Context
import android.os.Build
import androidx.core.content.edit
private const val CATEGORY_ID = "cat_id"
private const val BACKGROUND = "bg"
class AppWidgetConfig(context: Context, val widgetId: Int) {
class AppWidgetConfig(
context: Context,
cls: Class<out AppWidgetProvider>,
val widgetId: Int,
) {
private val prefs = context.getSharedPreferences("appwidget_$widgetId", Context.MODE_PRIVATE)
private val prefs = context.getSharedPreferences("appwidget_${cls.simpleName}_$widgetId", Context.MODE_PRIVATE)
var categoryId: Long
get() = prefs.getLong(CATEGORY_ID, 0L)
set(value) = prefs.edit { putLong(CATEGORY_ID, value) }
var hasBackground: Boolean
get() = prefs.getBoolean(BACKGROUND, Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
set(value) = prefs.edit { putBoolean(BACKGROUND, value) }
fun clear() {
prefs.edit { clear() }
}
fun copyFrom(other: AppWidgetConfig) {
prefs.edit {
clear()
putLong(CATEGORY_ID, other.categoryId)
putBoolean(BACKGROUND, other.hasBackground)
}
}
}

View File

@@ -3,8 +3,8 @@ package org.koitharu.kotatsu.core.prefs
enum class ReaderMode(val id: Int) {
STANDARD(1),
WEBTOON(2),
REVERSED(3);
REVERSED(3),
WEBTOON(2);
companion object {

View File

@@ -0,0 +1,51 @@
package org.koitharu.kotatsu.core.ui
import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProvider
import android.content.Context
import android.widget.RemoteViews
import androidx.annotation.CallSuper
import org.koitharu.kotatsu.core.prefs.AppWidgetConfig
abstract class BaseAppWidgetProvider : AppWidgetProvider() {
@CallSuper
override fun onUpdate(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetIds: IntArray
) {
appWidgetIds.forEach { id ->
val config = AppWidgetConfig(context, javaClass, id)
val views = onUpdateWidget(context, config)
appWidgetManager.updateAppWidget(id, views)
}
}
override fun onDeleted(context: Context, appWidgetIds: IntArray) {
super.onDeleted(context, appWidgetIds)
for (id in appWidgetIds) {
AppWidgetConfig(context, javaClass, id).clear()
}
}
override fun onRestored(context: Context, oldWidgetIds: IntArray, newWidgetIds: IntArray) {
super.onRestored(context, oldWidgetIds, newWidgetIds)
if (oldWidgetIds.size != newWidgetIds.size) {
return
}
for (i in oldWidgetIds.indices) {
val oldId = oldWidgetIds[i]
val newId = newWidgetIds[i]
val oldConfig = AppWidgetConfig(context, javaClass, oldId)
val newConfig = AppWidgetConfig(context, javaClass, newId)
newConfig.copyFrom(oldConfig)
oldConfig.clear()
}
}
protected abstract fun onUpdateWidget(
context: Context,
config: AppWidgetConfig,
): RemoteViews
}

View File

@@ -0,0 +1,43 @@
package org.koitharu.kotatsu.core.util
import androidx.collection.ArrayMap
import kotlinx.coroutines.sync.Mutex
class CompositeMutex2<T : Any> : Set<T> {
private val delegates = ArrayMap<T, Mutex>()
override val size: Int
get() = delegates.size
override fun contains(element: T): Boolean {
return delegates.containsKey(element)
}
override fun containsAll(elements: Collection<T>): Boolean {
return elements.all { x -> delegates.containsKey(x) }
}
override fun isEmpty(): Boolean {
return delegates.isEmpty
}
override fun iterator(): Iterator<T> {
return delegates.keys.iterator()
}
suspend fun lock(element: T) {
val mutex = synchronized(delegates) {
delegates.getOrPut(element) {
Mutex()
}
}
mutex.lock()
}
fun unlock(element: T) {
synchronized(delegates) {
delegates.remove(element)?.unlock()
}
}
}

View File

@@ -12,7 +12,6 @@ import coil.request.SuccessResult
import coil.util.CoilUtils
import com.google.android.material.progressindicator.BaseProgressIndicator
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.browser.cloudflare.CaptchaNotifier
import org.koitharu.kotatsu.core.ui.image.RegionBitmapDecoder
import org.koitharu.kotatsu.core.util.progress.ImageRequestIndicatorListener
import org.koitharu.kotatsu.parsers.model.MangaSource
@@ -29,7 +28,6 @@ fun ImageView.newImageRequest(lifecycleOwner: LifecycleOwner, data: Any?): Image
.data(data)
.lifecycle(lifecycleOwner)
.crossfade(context)
.addListener(CaptchaNotifier(context.applicationContext))
.target(this)
}

View File

@@ -5,7 +5,6 @@ import androidx.lifecycle.LifecycleOwner
import coil.ImageLoader
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.util.ext.disposeImageRequest
import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.databinding.ItemScrobblingInfoBinding
@@ -37,8 +36,4 @@ fun scrobblingInfoAD(
context.resources.getStringArray(R.array.scrobbling_statuses).getOrNull(it.ordinal)
}
}
onViewRecycled {
binding.imageViewCover.disposeImageRequest()
}
}

View File

@@ -8,7 +8,6 @@ import coil.ImageLoader
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.image.TrimTransformation
import org.koitharu.kotatsu.core.util.ext.disposeImageRequest
import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.core.util.ext.source
@@ -135,8 +134,4 @@ fun downloadItemAD(
}
}
}
onViewRecycled {
binding.imageViewCover.disposeImageRequest()
}
}

View File

@@ -37,6 +37,7 @@ import okio.IOException
import okio.buffer
import okio.sink
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.exceptions.TooManyRequestExceptions
import org.koitharu.kotatsu.core.network.CommonHeaders
import org.koitharu.kotatsu.core.network.MangaHttpClient
import org.koitharu.kotatsu.core.parser.MangaDataRepository
@@ -277,7 +278,12 @@ class DownloadWorker @AssistedInject constructor(
publishState(currentState.copy(isPaused = false, error = null))
} else {
countDown--
delay(DOWNLOAD_ERROR_DELAY)
val retryDelay = if (e is TooManyRequestExceptions) {
e.retryAfter + DOWNLOAD_ERROR_DELAY
} else {
DOWNLOAD_ERROR_DELAY
}
delay(retryDelay)
}
}
}

View File

@@ -12,7 +12,6 @@ import org.koitharu.kotatsu.core.ui.image.FaviconDrawable
import org.koitharu.kotatsu.core.ui.image.TrimTransformation
import org.koitharu.kotatsu.core.ui.list.AdapterDelegateClickListenerAdapter
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.util.ext.disposeImageRequest
import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.getThemeColor
import org.koitharu.kotatsu.core.util.ext.newImageRequest
@@ -82,10 +81,6 @@ fun exploreRecommendationItemAD(
enqueueWith(coil)
}
}
onViewRecycled {
binding.imageViewCover.disposeImageRequest()
}
}
fun exploreSourceListItemAD(
@@ -113,10 +108,6 @@ fun exploreSourceListItemAD(
enqueueWith(coil)
}
}
onViewRecycled {
binding.imageViewIcon.disposeImageRequest()
}
}
fun exploreSourceGridItemAD(
@@ -144,8 +135,4 @@ fun exploreSourceGridItemAD(
enqueueWith(coil)
}
}
onViewRecycled {
binding.imageViewIcon.disposeImageRequest()
}
}

View File

@@ -16,7 +16,6 @@ import androidx.lifecycle.LifecycleOwner
import coil.ImageLoader
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.util.ext.disposeImageRequest
import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.getAnimationDuration
import org.koitharu.kotatsu.core.util.ext.getThemeColor
@@ -89,10 +88,4 @@ fun categoryAD(
}
}
}
onViewRecycled {
coverViews.forEach {
it.disposeImageRequest()
}
}
}

View File

@@ -3,7 +3,6 @@ package org.koitharu.kotatsu.list.ui.adapter
import androidx.lifecycle.LifecycleOwner
import coil.ImageLoader
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.core.util.ext.disposeImageRequest
import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.core.util.ext.setTextAndVisible
@@ -27,8 +26,4 @@ fun emptyHintAD(
binding.textSecondary.setTextAndVisible(item.textSecondary)
binding.buttonRetry.setTextAndVisible(item.actionStringRes)
}
onViewRecycled {
binding.icon.disposeImageRequest()
}
}

View File

@@ -3,7 +3,6 @@ package org.koitharu.kotatsu.list.ui.adapter
import androidx.lifecycle.LifecycleOwner
import coil.ImageLoader
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.core.util.ext.disposeImageRequest
import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.core.util.ext.setTextAndVisible
@@ -31,8 +30,4 @@ fun emptyStateListAD(
binding.buttonRetry.setTextAndVisible(item.actionStringRes)
}
}
onViewRecycled {
binding.icon.disposeImageRequest()
}
}

View File

@@ -8,12 +8,10 @@ import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.image.CoverSizeResolver
import org.koitharu.kotatsu.core.ui.image.TrimTransformation
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.util.ext.disposeImageRequest
import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.core.util.ext.source
import org.koitharu.kotatsu.databinding.ItemMangaGridBinding
import org.koitharu.kotatsu.history.data.PROGRESS_NONE
import org.koitharu.kotatsu.list.ui.ListModelDiffCallback
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.list.ui.model.MangaGridModel
@@ -54,11 +52,4 @@ fun mangaGridItemAD(
}
badge = itemView.bindBadge(badge, item.counter)
}
onViewRecycled {
itemView.clearBadge(badge)
binding.progressView.percent = PROGRESS_NONE
badge = null
binding.imageViewCover.disposeImageRequest()
}
}

View File

@@ -11,13 +11,11 @@ import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.image.CoverSizeResolver
import org.koitharu.kotatsu.core.ui.image.TrimTransformation
import org.koitharu.kotatsu.core.ui.widgets.ChipsView
import org.koitharu.kotatsu.core.util.ext.disposeImageRequest
import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.core.util.ext.source
import org.koitharu.kotatsu.core.util.ext.textAndVisible
import org.koitharu.kotatsu.databinding.ItemMangaListDetailsBinding
import org.koitharu.kotatsu.history.data.PROGRESS_NONE
import org.koitharu.kotatsu.list.ui.ListModelDiffCallback
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.list.ui.model.MangaListDetailedModel
@@ -73,11 +71,4 @@ fun mangaListDetailedItemAD(
binding.ratingBar.rating = binding.ratingBar.numStars * item.manga.rating
badge = itemView.bindBadge(badge, item.counter)
}
onViewRecycled {
itemView.clearBadge(badge)
binding.progressView.percent = PROGRESS_NONE
badge = null
binding.imageViewCover.disposeImageRequest()
}
}

View File

@@ -7,7 +7,6 @@ import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.image.TrimTransformation
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.util.ext.disposeImageRequest
import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.core.util.ext.source
@@ -48,10 +47,4 @@ fun mangaListItemAD(
}
badge = itemView.bindBadge(badge, item.counter)
}
onViewRecycled {
itemView.clearBadge(badge)
badge = null
binding.imageViewCover.disposeImageRequest()
}
}

View File

@@ -53,11 +53,11 @@ class LocalStorageManager @Inject constructor(
}
suspend fun computeStorageSize() = withContext(Dispatchers.IO) {
getAvailableStorageDirs().sumOf { it.computeSize() }
getConfiguredStorageDirs().sumOf { it.computeSize() }
}
suspend fun computeAvailableSize() = runInterruptible(Dispatchers.IO) {
getAvailableStorageDirs().mapToSet { it.freeSpace }.sum()
getConfiguredStorageDirs().mapToSet { it.freeSpace }.sum()
}
suspend fun clearCache(cache: CacheDir) = runInterruptible(Dispatchers.IO) {

View File

@@ -1,64 +1,65 @@
package org.koitharu.kotatsu.main.domain
import androidx.collection.ArraySet
import androidx.lifecycle.coroutineScope
import coil.EventListener
import coil.ImageLoader
import coil.intercept.Interceptor
import coil.network.HttpException
import coil.request.ErrorResult
import coil.request.ImageRequest
import kotlinx.coroutines.launch
import coil.request.ImageResult
import org.jsoup.HttpStatusException
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
import org.koitharu.kotatsu.bookmarks.domain.BookmarksRepository
import org.koitharu.kotatsu.core.parser.MangaDataRepository
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.ifNullOrEmpty
import org.koitharu.kotatsu.parsers.exception.ParseException
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import java.util.Collections
import javax.inject.Inject
import javax.inject.Provider
class CoverRestorer @Inject constructor(
class CoverRestoreInterceptor @Inject constructor(
private val dataRepository: MangaDataRepository,
private val bookmarksRepository: BookmarksRepository,
private val repositoryFactory: MangaRepository.Factory,
private val coilProvider: Provider<ImageLoader>,
) : EventListener {
) : Interceptor {
private val blacklist = ArraySet<String>()
private val blacklist = Collections.synchronizedSet(ArraySet<String>())
override fun onError(request: ImageRequest, result: ErrorResult) {
super.onError(request, result)
if (!result.throwable.shouldRestore()) {
return
}
request.tags.tag<Manga>()?.let {
restoreManga(it, request)
}
request.tags.tag<Bookmark>()?.let {
restoreBookmark(it, request)
}
}
private fun restoreManga(manga: Manga, request: ImageRequest) {
val key = manga.publicUrl
if (key in blacklist) {
return
}
request.lifecycle.coroutineScope.launch {
val restored = runCatchingCancellable {
restoreMangaImpl(manga)
}.getOrDefault(false)
if (restored) {
request.newBuilder().enqueueWith(coilProvider.get())
} else {
blacklist.add(key)
override suspend fun intercept(chain: Interceptor.Chain): ImageResult {
val request = chain.request
val result = chain.proceed(request)
if (result is ErrorResult && result.throwable.shouldRestore()) {
request.tags.tag<Manga>()?.let {
if (restoreManga(it)) {
return chain.proceed(request.newBuilder().build())
} else {
return result
}
}
request.tags.tag<Bookmark>()?.let {
if (restoreBookmark(it)) {
return chain.proceed(request.newBuilder().build())
} else {
return result
}
}
}
return result
}
private suspend fun restoreManga(manga: Manga): Boolean {
val key = manga.publicUrl
if (!blacklist.add(key)) {
return false
}
val restored = runCatchingCancellable {
restoreMangaImpl(manga)
}.getOrDefault(false)
if (restored) {
blacklist.remove(key)
}
return restored
}
private suspend fun restoreMangaImpl(manga: Manga): Boolean {
@@ -75,21 +76,18 @@ class CoverRestorer @Inject constructor(
}
}
private fun restoreBookmark(bookmark: Bookmark, request: ImageRequest) {
private suspend fun restoreBookmark(bookmark: Bookmark): Boolean {
val key = bookmark.imageUrl
if (key in blacklist) {
return
if (!blacklist.add(key)) {
return false
}
request.lifecycle.coroutineScope.launch {
val restored = runCatchingCancellable {
restoreBookmarkImpl(bookmark)
}.getOrDefault(false)
if (restored) {
request.newBuilder().enqueueWith(coilProvider.get())
} else {
blacklist.add(key)
}
val restored = runCatchingCancellable {
restoreBookmarkImpl(bookmark)
}.getOrDefault(false)
if (restored) {
blacklist.remove(key)
}
return restored
}
private suspend fun restoreBookmarkImpl(bookmark: Bookmark): Boolean {

View File

@@ -9,7 +9,6 @@ import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.list.AdapterDelegateClickListenerAdapter
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.util.ext.decodeRegion
import org.koitharu.kotatsu.core.util.ext.disposeImageRequest
import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.core.util.ext.setTextColorAttr
@@ -56,8 +55,4 @@ fun pageThumbnailAD(
text = (item.number).toString()
}
}
onViewRecycled {
binding.imageViewThumb.disposeImageRequest()
}
}

View File

@@ -6,7 +6,6 @@ import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.list.AdapterDelegateClickListenerAdapter
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.util.ext.disposeImageRequest
import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.databinding.ItemScrobblingMangaBinding
@@ -34,8 +33,4 @@ fun scrobblingMangaAD(
binding.textViewTitle.text = item.title
binding.ratingBar.rating = item.rating * binding.ratingBar.numStars
}
onViewRecycled {
binding.imageViewCover.disposeImageRequest()
}
}

View File

@@ -5,7 +5,6 @@ import coil.ImageLoader
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.util.ext.disposeImageRequest
import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.core.util.ext.textAndVisible
@@ -35,8 +34,4 @@ fun scrobblingMangaAD(
enqueueWith(coil)
}
}
onViewRecycled {
binding.imageViewCover.disposeImageRequest()
}
}

View File

@@ -7,7 +7,6 @@ import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.parser.favicon.faviconUri
import org.koitharu.kotatsu.core.ui.image.FaviconDrawable
import org.koitharu.kotatsu.core.util.ext.disposeImageRequest
import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.core.util.ext.source
@@ -51,8 +50,4 @@ fun searchSuggestionSourceAD(
enqueueWith(coil)
}
}
onViewRecycled {
binding.imageViewCover.disposeImageRequest()
}
}

View File

@@ -11,7 +11,6 @@ import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.list.decor.SpacingItemDecoration
import org.koitharu.kotatsu.core.util.RecyclerViewScrollCallback
import org.koitharu.kotatsu.core.util.ext.disposeImageRequest
import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.core.util.ext.source
@@ -66,10 +65,6 @@ private fun searchSuggestionMangaGridAD(
}
binding.textViewTitle.text = item.title
}
onViewRecycled {
binding.imageViewCover.disposeImageRequest()
}
}
private class SuggestionMangaDiffCallback : DiffUtil.ItemCallback<Manga>() {

View File

@@ -20,7 +20,6 @@ import org.koitharu.kotatsu.core.parser.favicon.faviconUri
import org.koitharu.kotatsu.core.ui.image.FaviconDrawable
import org.koitharu.kotatsu.core.ui.list.OnTipCloseListener
import org.koitharu.kotatsu.core.util.ext.crossfade
import org.koitharu.kotatsu.core.util.ext.disposeImageRequest
import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.getThemeColor
import org.koitharu.kotatsu.core.util.ext.newImageRequest
@@ -94,10 +93,6 @@ fun sourceConfigItemCheckableDelegate(
enqueueWith(coil)
}
}
onViewRecycled {
binding.imageViewIcon.disposeImageRequest()
}
}
fun sourceConfigItemDelegate2(
@@ -143,10 +138,6 @@ fun sourceConfigItemDelegate2(
enqueueWith(coil)
}
}
onViewRecycled {
binding.imageViewIcon.disposeImageRequest()
}
}
fun sourceConfigTipDelegate(

View File

@@ -5,7 +5,6 @@ import coil.ImageLoader
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.util.ext.disposeImageRequest
import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.isBold
import org.koitharu.kotatsu.core.util.ext.newImageRequest
@@ -44,8 +43,4 @@ fun feedItemAD(
item.count,
)
}
onViewRecycled {
binding.imageViewCover.disposeImageRequest()
}
}

View File

@@ -55,6 +55,7 @@ class RecentListFactory(
.data(item.coverUrl)
.size(coverSize)
.tag(item.source)
.tag(item)
.transformations(transformation)
.build(),
).getDrawableOrThrow().toBitmap()

View File

@@ -0,0 +1,74 @@
package org.koitharu.kotatsu.widget.recent
import android.app.Activity
import android.appwidget.AppWidgetManager
import android.content.Intent
import android.os.Bundle
import android.view.View
import androidx.core.graphics.Insets
import androidx.core.view.updatePadding
import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.prefs.AppWidgetConfig
import org.koitharu.kotatsu.core.ui.BaseActivity
import org.koitharu.kotatsu.databinding.ActivityAppwidgetRecentBinding
import com.google.android.material.R as materialR
@AndroidEntryPoint
class RecentWidgetConfigActivity :
BaseActivity<ActivityAppwidgetRecentBinding>(),
View.OnClickListener {
private lateinit var config: AppWidgetConfig
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(ActivityAppwidgetRecentBinding.inflate(layoutInflater))
supportActionBar?.run {
setDisplayHomeAsUpEnabled(true)
setHomeAsUpIndicator(materialR.drawable.abc_ic_clear_material)
}
viewBinding.buttonDone.setOnClickListener(this)
val appWidgetId = intent?.getIntExtra(
AppWidgetManager.EXTRA_APPWIDGET_ID,
AppWidgetManager.INVALID_APPWIDGET_ID,
) ?: AppWidgetManager.INVALID_APPWIDGET_ID
if (appWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID) {
finishAfterTransition()
return
}
config = AppWidgetConfig(this, RecentWidgetProvider::class.java, appWidgetId)
viewBinding.switchBackground.isChecked = config.hasBackground
}
override fun onClick(v: View) {
when (v.id) {
R.id.button_done -> {
config.hasBackground = viewBinding.switchBackground.isChecked
updateWidget()
setResult(
Activity.RESULT_OK,
Intent().putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, config.widgetId),
)
finish()
}
}
}
override fun onWindowInsetsChanged(insets: Insets) {
viewBinding.root.updatePadding(
left = insets.left,
right = insets.right,
bottom = insets.bottom,
top = insets.top,
)
}
private fun updateWidget() {
val intent = Intent(this, RecentWidgetProvider::class.java)
intent.action = AppWidgetManager.ACTION_APPWIDGET_UPDATE
val ids = intArrayOf(config.widgetId)
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, ids)
sendBroadcast(intent)
}
}

View File

@@ -2,43 +2,52 @@ package org.koitharu.kotatsu.widget.recent
import android.app.PendingIntent
import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProvider
import android.content.Context
import android.content.Intent
import android.graphics.Color
import android.net.Uri
import android.widget.RemoteViews
import androidx.core.app.PendingIntentCompat
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.prefs.AppWidgetConfig
import org.koitharu.kotatsu.core.ui.BaseAppWidgetProvider
import org.koitharu.kotatsu.reader.ui.ReaderActivity
class RecentWidgetProvider : AppWidgetProvider() {
class RecentWidgetProvider : BaseAppWidgetProvider() {
override fun onUpdate(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetIds: IntArray
) {
appWidgetIds.forEach { id ->
val views = RemoteViews(context.packageName, R.layout.widget_recent)
val adapter = Intent(context, RecentWidgetService::class.java)
adapter.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, id)
adapter.data = Uri.parse(adapter.toUri(Intent.URI_INTENT_SCHEME))
views.setRemoteAdapter(R.id.stackView, adapter)
val intent = Intent(context, ReaderActivity::class.java)
intent.action = ReaderActivity.ACTION_MANGA_READ
views.setPendingIntentTemplate(
R.id.stackView,
PendingIntentCompat.getActivity(
context,
0,
intent,
PendingIntent.FLAG_UPDATE_CURRENT,
true,
),
)
views.setEmptyView(R.id.stackView, R.id.textView_holder)
appWidgetManager.updateAppWidget(id, views)
}
super.onUpdate(context, appWidgetManager, appWidgetIds)
appWidgetManager.notifyAppWidgetViewDataChanged(appWidgetIds, R.id.stackView)
}
override fun onUpdateWidget(context: Context, config: AppWidgetConfig): RemoteViews {
val views = RemoteViews(context.packageName, R.layout.widget_recent)
if (!config.hasBackground) {
views.setInt(R.id.widget_root, "setBackgroundColor", Color.TRANSPARENT)
} else {
views.setInt(R.id.widget_root, "setBackgroundResource", R.drawable.bg_appwidget_root)
}
val adapter = Intent(context, RecentWidgetService::class.java)
adapter.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, config.widgetId)
adapter.data = Uri.parse(adapter.toUri(Intent.URI_INTENT_SCHEME))
views.setRemoteAdapter(R.id.stackView, adapter)
val intent = Intent(context, ReaderActivity::class.java)
intent.action = ReaderActivity.ACTION_MANGA_READ
views.setPendingIntentTemplate(
R.id.stackView,
PendingIntentCompat.getActivity(
context,
0,
intent,
PendingIntent.FLAG_UPDATE_CURRENT,
true,
),
)
views.setEmptyView(R.id.stackView, R.id.textView_holder)
return views
}
}

View File

@@ -14,6 +14,7 @@ import kotlinx.coroutines.runBlocking
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.parser.MangaIntent
import org.koitharu.kotatsu.core.prefs.AppWidgetConfig
import org.koitharu.kotatsu.core.ui.image.TrimTransformation
import org.koitharu.kotatsu.core.util.ext.getDrawableOrThrow
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
import org.koitharu.kotatsu.parsers.model.Manga
@@ -27,7 +28,7 @@ class ShelfListFactory(
) : RemoteViewsService.RemoteViewsFactory {
private val dataSet = ArrayList<Manga>()
private val config = AppWidgetConfig(context, widgetId)
private val config = AppWidgetConfig(context, ShelfWidgetProvider::class.java, widgetId)
private val transformation = RoundedCornersTransformation(
context.resources.getDimension(R.dimen.appwidget_corner_radius_inner),
)
@@ -66,7 +67,8 @@ class ShelfListFactory(
.data(item.coverUrl)
.size(coverSize)
.tag(item.source)
.transformations(transformation)
.tag(item)
.transformations(transformation, TrimTransformation())
.build(),
).getDrawableOrThrow().toBitmap()
}.onSuccess { cover ->

View File

@@ -8,7 +8,6 @@ import android.view.View
import android.view.ViewGroup
import androidx.activity.viewModels
import androidx.core.graphics.Insets
import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding
import dagger.hilt.android.AndroidEntryPoint
@@ -19,14 +18,14 @@ import org.koitharu.kotatsu.core.ui.BaseActivity
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.observeEvent
import org.koitharu.kotatsu.databinding.ActivityCategoriesBinding
import org.koitharu.kotatsu.databinding.ActivityAppwidgetShelfBinding
import org.koitharu.kotatsu.widget.shelf.adapter.CategorySelectAdapter
import org.koitharu.kotatsu.widget.shelf.model.CategoryItem
import com.google.android.material.R as materialR
@AndroidEntryPoint
class ShelfConfigActivity :
BaseActivity<ActivityCategoriesBinding>(),
class ShelfWidgetConfigActivity :
BaseActivity<ActivityAppwidgetShelfBinding>(),
OnListItemClickListener<CategoryItem>,
View.OnClickListener {
@@ -37,16 +36,14 @@ class ShelfConfigActivity :
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(ActivityCategoriesBinding.inflate(layoutInflater))
setContentView(ActivityAppwidgetShelfBinding.inflate(layoutInflater))
supportActionBar?.run {
setDisplayHomeAsUpEnabled(true)
setHomeAsUpIndicator(materialR.drawable.abc_ic_clear_material)
}
adapter = CategorySelectAdapter(this)
viewBinding.recyclerView.adapter = adapter
viewBinding.buttonDone.isVisible = true
viewBinding.buttonDone.setOnClickListener(this)
viewBinding.fabAdd.hide()
val appWidgetId = intent?.getIntExtra(
AppWidgetManager.EXTRA_APPWIDGET_ID,
AppWidgetManager.INVALID_APPWIDGET_ID,
@@ -55,8 +52,9 @@ class ShelfConfigActivity :
finishAfterTransition()
return
}
config = AppWidgetConfig(this, appWidgetId)
config = AppWidgetConfig(this, ShelfWidgetProvider::class.java, appWidgetId)
viewModel.checkedId = config.categoryId
viewBinding.switchBackground.isChecked = config.hasBackground
viewModel.content.observe(this, this::onContentChanged)
viewModel.onError.observeEvent(this, SnackbarErrorObserver(viewBinding.recyclerView, null))
@@ -66,6 +64,7 @@ class ShelfConfigActivity :
when (v.id) {
R.id.button_done -> {
config.categoryId = viewModel.checkedId
config.hasBackground = viewBinding.switchBackground.isChecked
updateWidget()
setResult(
Activity.RESULT_OK,
@@ -81,11 +80,6 @@ class ShelfConfigActivity :
}
override fun onWindowInsetsChanged(insets: Insets) {
viewBinding.fabAdd.updateLayoutParams<ViewGroup.MarginLayoutParams> {
rightMargin = topMargin + insets.right
leftMargin = topMargin + insets.left
bottomMargin = topMargin + insets.bottom
}
viewBinding.recyclerView.updatePadding(
left = insets.left,
right = insets.right,

View File

@@ -2,43 +2,52 @@ package org.koitharu.kotatsu.widget.shelf
import android.app.PendingIntent
import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProvider
import android.content.Context
import android.content.Intent
import android.graphics.Color
import android.net.Uri
import android.widget.RemoteViews
import androidx.core.app.PendingIntentCompat
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.prefs.AppWidgetConfig
import org.koitharu.kotatsu.core.ui.BaseAppWidgetProvider
import org.koitharu.kotatsu.reader.ui.ReaderActivity
class ShelfWidgetProvider : AppWidgetProvider() {
class ShelfWidgetProvider : BaseAppWidgetProvider() {
override fun onUpdate(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetIds: IntArray
) {
appWidgetIds.forEach { id ->
val views = RemoteViews(context.packageName, R.layout.widget_shelf)
val adapter = Intent(context, ShelfWidgetService::class.java)
adapter.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, id)
adapter.data = Uri.parse(adapter.toUri(Intent.URI_INTENT_SCHEME))
views.setRemoteAdapter(R.id.gridView, adapter)
val intent = Intent(context, ReaderActivity::class.java)
intent.action = ReaderActivity.ACTION_MANGA_READ
views.setPendingIntentTemplate(
R.id.gridView,
PendingIntentCompat.getActivity(
context,
0,
intent,
PendingIntent.FLAG_UPDATE_CURRENT,
true,
),
)
views.setEmptyView(R.id.gridView, R.id.textView_holder)
appWidgetManager.updateAppWidget(id, views)
}
super.onUpdate(context, appWidgetManager, appWidgetIds)
appWidgetManager.notifyAppWidgetViewDataChanged(appWidgetIds, R.id.gridView)
}
override fun onUpdateWidget(context: Context, config: AppWidgetConfig): RemoteViews {
val views = RemoteViews(context.packageName, R.layout.widget_shelf)
if (!config.hasBackground) {
views.setInt(R.id.widget_root, "setBackgroundColor", Color.TRANSPARENT)
} else {
views.setInt(R.id.widget_root, "setBackgroundResource", R.drawable.bg_appwidget_root)
}
val adapter = Intent(context, ShelfWidgetService::class.java)
adapter.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, config.widgetId)
adapter.data = Uri.parse(adapter.toUri(Intent.URI_INTENT_SCHEME))
views.setRemoteAdapter(R.id.gridView, adapter)
val intent = Intent(context, ReaderActivity::class.java)
intent.action = ReaderActivity.ACTION_MANGA_READ
views.setPendingIntentTemplate(
R.id.gridView,
PendingIntentCompat.getActivity(
context,
0,
intent,
PendingIntent.FLAG_UPDATE_CURRENT,
true,
),
)
views.setEmptyView(R.id.gridView, R.id.textView_holder)
return views
}
}

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<shape
xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners android:radius="@dimen/appwidget_corner_radius_inner" />
<solid android:color="@android:color/system_accent2_100" />
</shape>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<shape
xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners android:radius="@dimen/appwidget_corner_radius_background" />
<solid android:color="@android:color/system_accent2_50" />
</shape>

View File

@@ -3,5 +3,5 @@
xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners android:radius="@dimen/appwidget_corner_radius_inner" />
<solid android:color="?android:panelColorBackground" />
</shape>
<solid android:color="@color/kotatsu_primaryContainer" />
</shape>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<shape
xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners android:radius="@dimen/appwidget_corner_radius_background" />
<solid android:color="@color/kotatsu_background" />
</shape>

View File

@@ -8,7 +8,7 @@
<item
android:id="@+id/checked"
android:drawable="@drawable/ic_heart_outline"
android:drawable="@drawable/ic_heart"
android:state_checked="true" />
<transition

View File

@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.google.android.material.appbar.MaterialToolbar
android:id="@id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:layout_collapseMode="pin"
tools:title="@string/recent_manga">
<Button
android:id="@+id/button_done"
style="@style/Widget.Material3.Button.UnelevatedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:layout_marginHorizontal="@dimen/toolbar_button_margin"
android:text="@string/done" />
</com.google.android.material.appbar.MaterialToolbar>
<com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/switch_background"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:paddingStart="?listPreferredItemPaddingStart"
android:paddingEnd="?listPreferredItemPaddingEnd"
android:text="@string/background"
android:textAppearance="?textAppearanceBodyMedium"
app:layout_scrollFlags="scroll" />
</LinearLayout>

View File

@@ -0,0 +1,70 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fitsSystemWindows="true">
<com.google.android.material.appbar.MaterialToolbar
android:id="@id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:layout_collapseMode="pin"
tools:title="@string/manga_shelf">
<Button
android:id="@+id/button_done"
style="@style/Widget.Material3.Button.UnelevatedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:layout_marginHorizontal="@dimen/toolbar_button_margin"
android:text="@string/done" />
</com.google.android.material.appbar.MaterialToolbar>
<com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/switch_background"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:checked="true"
android:paddingStart="?listPreferredItemPaddingStart"
android:paddingEnd="?listPreferredItemPaddingEnd"
android:text="@string/background"
android:textAppearance="?textAppearanceBodyMedium"
app:layout_scrollFlags="scroll" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical|start"
android:paddingStart="?listPreferredItemPaddingStart"
android:paddingTop="6dp"
android:paddingEnd="?listPreferredItemPaddingEnd"
android:singleLine="true"
android:text="@string/favourites_categories"
android:textAppearance="@style/TextAppearance.Kotatsu.SectionHeader" />
</com.google.android.material.appbar.AppBarLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:orientation="vertical"
android:paddingVertical="@dimen/list_spacing_normal"
android:scrollbars="vertical"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"
tools:listitem="@layout/item_checkable_single" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@@ -5,7 +5,8 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="4dp"
android:theme="@style/Theme.Kotatsu.AppWidgetContainer">
android:theme="@style/Theme.Kotatsu.AppWidgetContainer"
tools:layout_width="116dp">
<LinearLayout
android:id="@+id/rootLayout"
@@ -13,14 +14,15 @@
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:background="@drawable/bg_appwidget_card"
android:clipToOutline="true"
android:foreground="?android:selectableItemBackground"
android:orientation="vertical"
android:padding="4dp"
android:outlineProvider="background"
tools:ignore="UnusedAttribute,UselessParent">
<ImageView
android:id="@+id/imageView_cover"
android:layout_width="@dimen/widget_cover_width"
android:layout_width="match_parent"
android:layout_height="@dimen/widget_cover_height"
android:scaleType="centerCrop"
tools:ignore="ContentDescription"
@@ -33,8 +35,11 @@
android:elegantTextHeight="false"
android:ellipsize="end"
android:lines="2"
android:padding="2dp"
android:paddingHorizontal="6dp"
android:paddingTop="2dp"
android:paddingBottom="4dp"
android:textColor="?android:attr/textColorPrimary"
android:textSize="12sp"
tools:text="@tools:sample/lorem" />
</LinearLayout>

View File

@@ -1,14 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?android:attr/colorBackground"
android:clipToOutline="true"
android:outlineProvider="background"
android:padding="4dp"
android:theme="@style/Theme.Kotatsu.AppWidgetContainer">
android:theme="@style/Theme.Kotatsu.AppWidgetContainer"
tools:background="@drawable/bg_appwidget_root">
<StackView
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/stackView"
android:layout_width="match_parent"
android:layout_height="match_parent"
@@ -23,4 +25,4 @@
android:text="@string/history_is_empty"
android:textColor="?android:textColorPrimary" />
</FrameLayout>
</FrameLayout>

View File

@@ -1,21 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/widget_root"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?android:attr/colorBackground"
android:padding="4dp"
android:theme="@style/Theme.Kotatsu.AppWidgetContainer">
android:clipToOutline="true"
android:outlineProvider="background"
android:theme="@style/Theme.Kotatsu.AppWidgetContainer"
tools:background="?android:attr/colorBackground">
<GridView
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/gridView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:columnWidth="92dp"
android:gravity="center_horizontal"
android:numColumns="auto_fit"
android:orientation="horizontal"
android:padding="4dp"
tools:listitem="@layout/item_shelf" />
<TextView
@@ -27,4 +31,4 @@
android:text="@string/you_have_not_favourites_yet"
android:textColor="?android:attr/textColorPrimary" />
</FrameLayout>
</FrameLayout>

View File

@@ -469,4 +469,7 @@
<string name="disable_nsfw">Desactivar NSFW</string>
<string name="too_many_requests_message">Demasiadas solicitudes. Vuelve a intentarlo más tarde</string>
<string name="related_manga_summary">Muestra una lista de mangas relacionados. En algunos casos puede ser inexacta o faltar</string>
<string name="advanced">Avanzado</string>
<string name="default_section">Sección predeterminada</string>
<string name="manga_list">Lista de mangas</string>
</resources>

View File

@@ -1,33 +1,39 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<plurals name="new_chapters">
<item quantity="one">%1$d nowy rozdział</item>
<item quantity="few">%1$d nowe rozdziały</item>
<item quantity="many">%1$d nowych rozdziałów</item>
</plurals>
<plurals name="minutes_ago">
<item quantity="one">%1$d minutę temu</item>
<item quantity="few">%1$d minuty temu</item>
<item quantity="many">%1$d minut temu</item>
</plurals>
<plurals name="hours_ago">
<item quantity="one">%1$d godzinę temu</item>
<item quantity="few">%1$d godziny temu</item>
<item quantity="many">%1$d godzin temu</item>
</plurals>
<plurals name="days_ago">
<item quantity="one">%1$d dzień temu</item>
<item quantity="few">%1$d dni temu</item>
<item quantity="many">%1$d dni temu</item>
</plurals>
<plurals name="items">
<item quantity="one">%1$d przedmiot</item>
<item quantity="few">%1$d przedmioty</item>
<item quantity="many">%1$d przedmiotów</item>
</plurals>
<plurals name="chapters">
<item quantity="one">%1$d rozdział</item>
<item quantity="few">%1$d rozdziały</item>
<item quantity="many">%1$d rozdziałów</item>
</plurals>
</resources>
<plurals name="new_chapters">
<item quantity="one">%1$d nowy rozdział</item>
<item quantity="few">%1$d nowe rozdziały</item>
<item quantity="many">%1$d nowych rozdziałów</item>
</plurals>
<plurals name="minutes_ago">
<item quantity="one">%1$d minutę temu</item>
<item quantity="few">%1$d minuty temu</item>
<item quantity="many">%1$d minut temu</item>
</plurals>
<plurals name="hours_ago">
<item quantity="one">%1$d godzinę temu</item>
<item quantity="few">%1$d godziny temu</item>
<item quantity="many">%1$d godzin temu</item>
</plurals>
<plurals name="days_ago">
<item quantity="one">%1$d dzień temu</item>
<item quantity="few">%1$d dni temu</item>
<item quantity="many">%1$d dni temu</item>
</plurals>
<plurals name="items">
<item quantity="one">%1$d przedmiot</item>
<item quantity="few">%1$d przedmioty</item>
<item quantity="many">%1$d przedmiotów</item>
</plurals>
<plurals name="chapters">
<item quantity="one">%1$d rozdział</item>
<item quantity="few">%1$d rozdziały</item>
<item quantity="many">%1$d rozdziałów</item>
</plurals>
<plurals name="months_ago">
<item quantity="one">%1$d miesiąc temu</item>
<item quantity="few">%1$d miesiące temu</item>
<item quantity="many">%1$d miesięcy temu</item>
<item quantity="other">%1$d miesięcy temu</item>
</plurals>
</resources>

View File

@@ -443,4 +443,17 @@
<string name="pick_custom_directory">Wybierz katalog niestandardowy</string>
<string name="no_access_to_file">Nie masz dostępu do tego pliku lub katalogu</string>
<string name="local_manga_directories">Lokalne katalogi mangi</string>
<string name="advanced">Zaawansowane</string>
<string name="this_month">Ten miesiąc</string>
<string name="voice_search">Wyszukiwanie głosowe</string>
<string name="color_light">Jasny</string>
<string name="color_dark">Ciemny</string>
<string name="color_white">Biały</string>
<string name="color_black">Czarny</string>
<string name="background">Tło</string>
<string name="progress">Postęp</string>
<string name="order_added">Dodano</string>
<string name="description">Opis</string>
<string name="languages">Języki</string>
<string name="unknown">Nieznane</string>
</resources>

View File

@@ -121,7 +121,7 @@
<string name="invert_colors">กลับสี</string>
<string name="pages_animation_summary">อนิเมชั่นการเปลี่ยนหน้า</string>
<string name="check_new_chapters_title">ค้นหาบทใหม่และแจ้งเตือน</string>
<string name="check_for_new_chapters">ค้นหาบทใหม่</string>
<string name="check_for_new_chapters">ตรวจสอบบทความใหม่</string>
<string name="chapter_is_missing">บทที่หายไป</string>
<string name="search_chapters">ค้นหาบท</string>
<string name="chapters_empty">ไม่มีบทในมังงะนี้</string>
@@ -177,7 +177,7 @@
<string name="clear_cookies">เคลียร์คุกกี้</string>
<string name="sign_in">ลงชื่อเข้าใช้</string>
<string name="auth_required">ลงชื่อเข้าใช้เพื่อเข้าสู่เนื้อหานี้</string>
<string name="default_s">ค่าดั้งเดิม: %s</string>
<string name="default_s">ค่าเริ่มต้น: %s</string>
<string name="next">ต่อไป</string>
<string name="protect_application_subtitle">ใส่รหัสผ่านเพื่อเริ่มแอปด้วย</string>
<string name="confirm">ยืนยัน</string>
@@ -199,4 +199,36 @@
<string name="progress">ความคืบหน้า</string>
<string name="search_hint">ใส่ชื่อเรื่องมังงะ,แนวหรือชื่อแหล่ง</string>
<string name="this_month">เดือนนี้</string>
<string name="reader_settings">ตั้งค่าการแสดงผล</string>
<string name="default_section">ส่วนเริ่มต้น</string>
<string name="light_indicator">ไฟ LED</string>
<string name="genres">ประเภท</string>
<string name="exclude_nsfw_from_history">ไม่รวมมังงะ NSFW ออกจากประวัติ</string>
<string name="state_finished">เสร็จสิ้น</string>
<string name="state_ongoing">กำลังดำเนินการอยู่</string>
<string name="system_default">ค่าเริ่มต้น</string>
<string name="show_pages_numbers">จํานวนหน้า</string>
<string name="enabled_sources">แหล่งที่มาที่ใช้</string>
<string name="screenshots_policy">นโยบายการบันทึกภาพหน้าจอ</string>
<string name="available_sources">แหล่งที่มาที่ใช้ได้</string>
<string name="screenshots_allow">อนุญาต</string>
<string name="screenshots_block_nsfw">บล็อกบน NSFW</string>
<string name="screenshots_block_all">บล็อกตลอด</string>
<string name="suggestions">คําแนะนํา</string>
<string name="suggestions_enable">เปิดใช้งานคำแนะนำ</string>
<string name="manga_shelf">ชั้น</string>
<string name="not_available">ไม่สามารถใช้ได้</string>
<string name="backup_information">คุณสามารถสํารองข้อมูลประวัติและรายการโปรดและกู้คืน</string>
<string name="tap_to_try_again">แตะเพื่อลองอีกครั้ง</string>
<string name="silent">เงียบ</string>
<string name="captcha_required">ต้องมี CAPTCHA ถึงจะเข้าสำเร็จ</string>
<string name="captcha_solve">แก้ไข</string>
<string name="text_clear_updates_feed_prompt">ล้างประวัติการอัปเดตทั้งหมดอย่างถาวรไหม\?</string>
<string name="reverse">ย้อนกลับ</string>
<string name="tracker_warning">บางอุปกรณ์มีการทำงานของระบบที่แตกต่างกัน ซึ่งอาจทำให้งานในเบื้องหลังเสียหายได้</string>
<string name="read_more">อ่านเพิ่มเติม</string>
<string name="about_app_translation_summary">แปลแอปพลิเคชันนี้</string>
<string name="about_app_translation">การแปล</string>
<string name="auth_complete">ได้รับอนุญาต</string>
<string name="default_mode">โหมดเริ่มต้น</string>
</resources>

View File

@@ -1,4 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="appwidget_corner_radius_inner">@android:dimen/system_app_widget_inner_radius</dimen>
</resources>
<dimen name="appwidget_corner_radius_background">@android:dimen/system_app_widget_background_radius</dimen>
</resources>

View File

@@ -54,7 +54,8 @@
<dimen name="card_indicator_size_small">24dp</dimen>
<dimen name="card_indicator_offset">8dp</dimen>
<dimen name="appwidget_corner_radius_inner">8dp</dimen>
<dimen name="appwidget_corner_radius_inner">20dp</dimen>
<dimen name="appwidget_corner_radius_background">28dp</dimen>
<!-- FastScroller -->
<dimen name="fastscroll_bubble_radius">44dp</dimen>

View File

@@ -2,6 +2,7 @@
<appwidget-provider
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:configure="org.koitharu.kotatsu.widget.recent.RecentWidgetConfigActivity"
android:description="@string/appwidget_recent_description"
android:initialLayout="@layout/widget_recent"
android:minWidth="120dp"

View File

@@ -2,7 +2,7 @@
<appwidget-provider
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:configure="org.koitharu.kotatsu.widget.shelf.ShelfConfigActivity"
android:configure="org.koitharu.kotatsu.widget.shelf.ShelfWidgetConfigActivity"
android:description="@string/appwidget_shelf_description"
android:initialLayout="@layout/widget_shelf"
android:minWidth="160dp"