Compare commits

...

31 Commits

Author SHA1 Message Date
4b6b2c3e12 Fix favorites selector 2023-08-30 14:43:57 +03:00
51300e30bd Improve favicon loading 2023-08-30 14:41:44 +03:00
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
18c3229200 Handle TooManyRequestsException during downloading 2023-08-29 20:23:20 +03:00
9b6f511ac6 Do not discard image requests in onViewRecycled 2023-08-29 17:35:00 +03:00
74ca19a931 Improve widgets ui #457 2023-08-26 18:35:49 +03:00
2684a7384e Restore covers using interceptor 2023-08-26 16:44:09 +03:00
2c561824ef Fix default reader mode option #468 #466 2023-08-25 13:26:48 +03:00
61e9796269 Fix amoled theme 2023-08-24 14:43:49 +03:00
54597eb8f0 Udpate parsers 2023-08-24 14:43:49 +03:00
Dpper
e07ea0552f Translated using Weblate (Ukrainian)
Currently translated at 100.0% (476 of 476 strings)

Co-authored-by: Dpper <ruslan20020401@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/uk/
Translation: Kotatsu/Strings
2023-08-24 13:50:13 +03:00
b8e90719ce Translated using Weblate (Russian)
Currently translated at 100.0% (476 of 476 strings)

Co-authored-by: Koitharu <nvasya95@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ru/
Translation: Kotatsu/Strings
2023-08-24 13:50:13 +03:00
Reza Almanda
2ec716973c Translated using Weblate (Indonesian)
Currently translated at 100.0% (476 of 476 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (475 of 475 strings)

Co-authored-by: Reza Almanda <rezaalmanda27@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/id/
Translation: Kotatsu/Strings
2023-08-24 13:50:13 +03:00
J. Lavoie
0000c97e6a Translated using Weblate (French)
Currently translated at 100.0% (473 of 473 strings)

Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fr/
Translation: Kotatsu/Strings
2023-08-24 13:50:13 +03:00
Nayuki
70684de683 Translated using Weblate (Thai)
Currently translated at 42.9% (203 of 473 strings)

Co-authored-by: Nayuki <me@nayuki.cyou>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/th/
Translation: Kotatsu/Strings
2023-08-24 13:50:13 +03:00
gallegonovato
5a2288eb2d Translated using Weblate (Spanish)
Currently translated at 100.0% (473 of 473 strings)

Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/
Translation: Kotatsu/Strings
2023-08-24 13:50:13 +03:00
Макар Разин
6a023fa976 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (475 of 475 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (475 of 475 strings)

Translated using Weblate (Belarusian)

Currently translated at 100.0% (475 of 475 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (473 of 473 strings)

Translated using Weblate (Belarusian)

Currently translated at 100.0% (473 of 473 strings)

Co-authored-by: Макар Разин <makarrazin14@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/be/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ru/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/uk/
Translation: Kotatsu/Strings
2023-08-24 13:50:13 +03:00
3cc0fbe7bc Update UI 2023-08-23 20:42:40 +03:00
b3b022807a Fix sources reordering 2023-08-23 17:07:14 +03:00
ba0ea5a9fc UI fixes 2023-08-22 16:58:31 +03:00
ca1380e2b1 Refactor application class 2023-08-22 16:18:34 +03:00
05dbd11fc1 Restore covers only once 2023-08-22 13:38:20 +03:00
aa650d44d3 Revert "Limit TrackWorker parallelistm when device in use"
This reverts commit 0778f34db7.
2023-08-22 13:28:38 +03:00
99b698ad12 Improve theme chooser 2023-08-22 13:13:01 +03:00
2f9c2d9ab6 Option to swap first two navigation items 2023-08-22 12:11:09 +03:00
ab753787b0 Fix page loading retry 2023-08-21 17:33:43 +03:00
478ca351eb Merge pull request #463 from thonsi/devel 2023-08-21 10:49:12 +03:00
Thonsi
559b2cfd64 update gradles to latest 2023-08-20 20:55:49 +05:30
126 changed files with 1060 additions and 481 deletions

View File

@@ -14,10 +14,10 @@ android {
defaultConfig {
applicationId 'org.koitharu.kotatsu'
minSdkVersion 21
targetSdkVersion 34
versionCode 571
versionName '6.0-a2'
minSdk = 21
targetSdk = 34
versionCode = 574
versionName = '6.0.1'
generatedDensities = []
testInstrumentationRunner "org.koitharu.kotatsu.HiltTestRunner"
ksp {
@@ -81,11 +81,11 @@ afterEvaluate {
}
dependencies {
//noinspection GradleDependency
implementation('com.github.KotatsuApp:kotatsu-parsers:06a2aa6f97') {
implementation('com.github.KotatsuApp:kotatsu-parsers:3a76504380') {
exclude group: 'org.json', module: 'json'
}
implementation 'org.jetbrains.kotlin:kotlin-stdlib:1.9.0'
implementation 'org.jetbrains.kotlin:kotlin-stdlib:1.9.10'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3'
implementation 'androidx.appcompat:appcompat:1.6.1'

View File

@@ -0,0 +1,45 @@
package org.koitharu.kotatsu
import android.content.Context
import android.os.StrictMode
import androidx.fragment.app.strictmode.FragmentStrictMode
import org.koitharu.kotatsu.core.BaseApp
import org.koitharu.kotatsu.local.data.LocalMangaRepository
import org.koitharu.kotatsu.local.data.PagesCache
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.reader.domain.PageLoader
class KotatsuApp : BaseApp() {
override fun attachBaseContext(base: Context?) {
super.attachBaseContext(base)
enableStrictMode()
}
private fun enableStrictMode() {
StrictMode.setThreadPolicy(
StrictMode.ThreadPolicy.Builder()
.detectAll()
.penaltyLog()
.build(),
)
StrictMode.setVmPolicy(
StrictMode.VmPolicy.Builder()
.detectAll()
.setClassInstanceLimit(LocalMangaRepository::class.java, 1)
.setClassInstanceLimit(PagesCache::class.java, 1)
.setClassInstanceLimit(MangaLoaderContext::class.java, 1)
.setClassInstanceLimit(PageLoader::class.java, 1)
.penaltyLog()
.build(),
)
FragmentStrictMode.defaultPolicy = FragmentStrictMode.Policy.Builder()
.penaltyDeath()
.detectFragmentReuse()
// .detectWrongFragmentContainer() FIXME: migrate to ViewPager2
.detectRetainInstanceUsage()
.detectSetUserVisibleHint()
.detectFragmentTagUsage()
.build()
}
}

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

@@ -1,11 +1,9 @@
package org.koitharu.kotatsu
package org.koitharu.kotatsu.core
import android.app.Application
import android.content.Context
import android.os.StrictMode
import androidx.annotation.WorkerThread
import androidx.appcompat.app.AppCompatDelegate
import androidx.fragment.app.strictmode.FragmentStrictMode
import androidx.hilt.work.HiltWorkerFactory
import androidx.room.InvalidationTracker
import androidx.work.Configuration
@@ -20,21 +18,19 @@ import org.acra.config.httpSender
import org.acra.data.StringFormat
import org.acra.ktx.initAcra
import org.acra.sender.HttpSender
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.os.AppValidator
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.util.WorkServiceStopHelper
import org.koitharu.kotatsu.core.util.ext.processLifecycleScope
import org.koitharu.kotatsu.local.data.LocalMangaRepository
import org.koitharu.kotatsu.local.data.PagesCache
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.reader.domain.PageLoader
import org.koitharu.kotatsu.settings.work.WorkScheduleManager
import javax.inject.Inject
import javax.inject.Provider
@HiltAndroidApp
class KotatsuApp : Application(), Configuration.Provider {
open class BaseApp : Application(), Configuration.Provider {
@Inject
lateinit var databaseObservers: Set<@JvmSuppressWildcards InvalidationTracker.Observer>
@@ -63,9 +59,6 @@ class KotatsuApp : Application(), Configuration.Provider {
override fun onCreate() {
super.onCreate()
ACRA.errorReporter.putCustomData("isOriginalApp", appValidator.isOriginalApp.toString())
if (BuildConfig.DEBUG) {
enableStrictMode()
}
AppCompatDelegate.setDefaultNightMode(settings.theme)
AppCompatDelegate.setApplicationLocales(settings.appLocales)
setupActivityLifecycleCallbacks()
@@ -136,31 +129,4 @@ class KotatsuApp : Application(), Configuration.Provider {
registerActivityLifecycleCallbacks(it)
}
}
private fun enableStrictMode() {
StrictMode.setThreadPolicy(
StrictMode.ThreadPolicy.Builder()
.detectAll()
.penaltyLog()
.build(),
)
StrictMode.setVmPolicy(
StrictMode.VmPolicy.Builder()
.detectAll()
.setClassInstanceLimit(LocalMangaRepository::class.java, 1)
.setClassInstanceLimit(PagesCache::class.java, 1)
.setClassInstanceLimit(MangaLoaderContext::class.java, 1)
.setClassInstanceLimit(PageLoader::class.java, 1)
.penaltyLog()
.build(),
)
FragmentStrictMode.defaultPolicy = FragmentStrictMode.Policy.Builder()
.penaltyDeath()
.detectFragmentReuse()
// .detectWrongFragmentContainer() FIXME: migrate to ViewPager2
.detectRetainInstanceUsage()
.detectSetUserVisibleHint()
.detectFragmentTagUsage()
.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

@@ -51,6 +51,9 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
val isAmoledTheme: Boolean
get() = prefs.getBoolean(KEY_THEME_AMOLED, false)
val isFavoritesNavItemFirst: Boolean
get() = (prefs.getString(KEY_FIRST_NAV_ITEM, null)?.toIntOrNull() ?: 0) == 1
var gridSize: Int
get() = prefs.getInt(KEY_GRID_SIZE, 100)
set(value) = prefs.edit { putInt(KEY_GRID_SIZE, value) }
@@ -452,6 +455,7 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
const val KEY_LOCAL_MANGA_DIRS = "local_manga_dirs"
const val KEY_DISABLE_NSFW = "no_nsfw"
const val KEY_RELATED_MANGA = "related_manga"
const val KEY_FIRST_NAV_ITEM = "nav_first"
// About
const val KEY_APP_UPDATE = "app_update"

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

@@ -25,4 +25,9 @@ class ActivityRecreationHandle @Inject constructor() : DefaultActivityLifecycleC
val snapshot = activities.keys.toList()
snapshot.forEach { ActivityCompat.recreate(it) }
}
fun recreate(cls: Class<out Activity>) {
val activity = activities.keys.find { x -> x.javaClass == cls } ?: return
ActivityCompat.recreate(activity)
}
}

View File

@@ -76,7 +76,7 @@ class TipView @JvmOverloads constructor(
val shapeAppearanceModel = ShapeAppearanceModel.builder(context, attrs, defStyleAttr, 0).build()
background = MaterialShapeDrawable(shapeAppearanceModel).also {
it.fillColor = getColorStateList(R.styleable.TipView_cardBackgroundColor)
?: context.getThemeColorStateList(materialR.attr.colorBackgroundFloating)
?: context.getThemeColorStateList(R.attr.m3ColorExploreButton)
it.strokeWidth = getDimension(R.styleable.TipView_strokeWidth, 0f)
it.strokeColor = getColorStateList(R.styleable.TipView_strokeColor)
it.elevation = getDimension(R.styleable.TipView_elevation, 0f)

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

@@ -102,7 +102,7 @@ fun <T> SharedPreferences.observe(key: String, valueProducer: suspend () -> T):
}
}.distinctUntilChanged()
fun Lifecycle.postDelayed(runnable: Runnable, delay: Long) {
fun Lifecycle.postDelayed(delay: Long, runnable: Runnable) {
coroutineScope.launch {
delay(delay)
runnable.run()

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

@@ -10,6 +10,7 @@ import androidx.lifecycle.ProcessLifecycleOwner
import androidx.lifecycle.lifecycleScope
import dagger.hilt.android.lifecycle.RetainedLifecycle
import kotlinx.coroutines.CancellableContinuation
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.suspendCancellableCoroutine
import org.koitharu.kotatsu.core.util.RetainedLifecycleCoroutineScope
@@ -70,3 +71,11 @@ private fun Lifecycle.removeObserverFromAnyThread(observer: LifecycleObserver) {
removeObserver(observer)
}
}
fun <T> Deferred<T>.getCompletionResultOrNull(): Result<T>? = if (isCompleted) {
getCompletionExceptionOrNull()?.let { error ->
Result.failure(error)
} ?: Result.success(getCompleted())
} else {
null
}

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

@@ -91,6 +91,14 @@ class MangaSourcesRepository @Inject constructor(
}
}
suspend fun setPositions(sources: List<MangaSource>) {
db.withTransaction {
for ((index, item) in sources.withIndex()) {
dao.setSortKey(item.name, index)
}
}
}
suspend fun setPosition(source: MangaSource, index: Int) {
db.withTransaction {
val all = dao.findAll().toMutableList()

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

@@ -33,7 +33,7 @@ class FilterSheetFragment :
val adapter = FilterAdapter(filter, this)
binding.recyclerView.adapter = adapter
filter.filterItems.observe(viewLifecycleOwner, adapter)
binding.recyclerView.addItemDecoration(TypedListSpacingDecoration(binding.root.context, false))
binding.recyclerView.addItemDecoration(TypedListSpacingDecoration(binding.root.context, true))
if (dialog == null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
binding.recyclerView.scrollIndicators = 0

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

@@ -70,4 +70,6 @@ class TypedListSpacingDecoration(
private fun Rect.set(spacing: Int) = set(spacing, spacing, spacing, spacing)
private fun ListItemType?.isEdgeToEdge() = this == ListItemType.MANGA_NESTED_GROUP
|| this == ListItemType.FILTER_SORT
|| this == ListItemType.FILTER_TAG
}

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,55 +1,65 @@
package org.koitharu.kotatsu.main.domain
import androidx.lifecycle.coroutineScope
import coil.EventListener
import coil.ImageLoader
import androidx.collection.ArraySet
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 {
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 val blacklist = Collections.synchronizedSet(ArraySet<String>())
private fun restoreManga(manga: Manga, request: ImageRequest) {
request.lifecycle.coroutineScope.launch {
val restored = runCatchingCancellable {
restoreMangaImpl(manga)
}.getOrDefault(false)
if (restored) {
request.newBuilder().enqueueWith(coilProvider.get())
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 {
@@ -66,15 +76,18 @@ class CoverRestorer @Inject constructor(
}
}
private fun restoreBookmark(bookmark: Bookmark, request: ImageRequest) {
request.lifecycle.coroutineScope.launch {
val restored = runCatchingCancellable {
restoreBookmarkImpl(bookmark)
}.getOrDefault(false)
if (restored) {
request.newBuilder().enqueueWith(coilProvider.get())
}
private suspend fun restoreBookmark(bookmark: Bookmark): Boolean {
val key = bookmark.imageUrl
if (!blacklist.add(key)) {
return false
}
val restored = runCatchingCancellable {
restoreBookmarkImpl(bookmark)
}.getOrDefault(false)
if (restored) {
blacklist.remove(key)
}
return restored
}
private suspend fun restoreBookmarkImpl(bookmark: Bookmark): Boolean {

View File

@@ -116,9 +116,10 @@ class MainActivity : BaseActivity<ActivityMainBinding>(), AppBarOwner, BottomNav
navigationDelegate = MainNavigationDelegate(
navBar = checkNotNull(bottomNav ?: viewBinding.navRail),
fragmentManager = supportFragmentManager,
settings = settings,
)
navigationDelegate.addOnFragmentChangedListener(this)
navigationDelegate.onCreate()
navigationDelegate.onCreate(savedInstanceState)
appUpdateBadge = OptionsMenuBadgeHelper(viewBinding.toolbar, R.id.action_app_update)

View File

@@ -1,14 +1,17 @@
package org.koitharu.kotatsu.main.ui
import android.os.Bundle
import android.view.MenuItem
import androidx.activity.OnBackPressedCallback
import androidx.annotation.IdRes
import androidx.core.view.isEmpty
import androidx.core.view.iterator
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import com.google.android.material.navigation.NavigationBarView
import com.google.android.material.transition.MaterialFadeThrough
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.ui.util.RecyclerViewOwner
import org.koitharu.kotatsu.core.util.ext.firstVisibleItemPosition
import org.koitharu.kotatsu.core.util.ext.isAnimationsEnabled
@@ -23,7 +26,10 @@ private const val TAG_PRIMARY = "primary"
class MainNavigationDelegate(
private val navBar: NavigationBarView,
private val fragmentManager: FragmentManager,
) : OnBackPressedCallback(false), NavigationBarView.OnItemSelectedListener, NavigationBarView.OnItemReselectedListener {
private val settings: AppSettings,
) : OnBackPressedCallback(false),
NavigationBarView.OnItemSelectedListener,
NavigationBarView.OnItemReselectedListener {
private val listeners = LinkedList<OnFragmentChangedListener>()
@@ -56,14 +62,26 @@ class MainNavigationDelegate(
navBar.selectedItemId = R.id.nav_history
}
fun onCreate() {
primaryFragment?.let {
onFragmentChanged(it, fromUser = false)
val itemId = getItemId(it)
fun onCreate(savedInstanceState: Bundle?) {
if (navBar.menu.isEmpty()) {
val menuRes = if (settings.isFavoritesNavItemFirst) R.menu.nav_bottom_alt else R.menu.nav_bottom
navBar.inflateMenu(menuRes)
}
val fragment = primaryFragment
if (fragment != null) {
onFragmentChanged(fragment, fromUser = false)
val itemId = getItemId(fragment)
if (navBar.selectedItemId != itemId) {
navBar.selectedItemId = itemId
}
} ?: onNavigationItemSelected(navBar.selectedItemId)
} else {
val itemId = if (savedInstanceState == null) {
firstItem()?.itemId ?: navBar.selectedItemId
} else {
navBar.selectedItemId
}
onNavigationItemSelected(itemId)
}
}
fun setCounterAt(position: Int, counter: Int) {

View File

@@ -34,6 +34,7 @@ import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.util.FileSize
import org.koitharu.kotatsu.core.util.RetainedLifecycleCoroutineScope
import org.koitharu.kotatsu.core.util.ext.ensureSuccess
import org.koitharu.kotatsu.core.util.ext.getCompletionResultOrNull
import org.koitharu.kotatsu.core.util.ext.isNotEmpty
import org.koitharu.kotatsu.core.util.ext.isPowerSaveMode
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
@@ -222,12 +223,9 @@ class PageLoader @Inject constructor(
}
private fun Deferred<File>.isValid(): Boolean {
return if (isCompleted) {
val file = getCompleted()
return getCompletionResultOrNull()?.map { file ->
file.exists() && file.isNotEmpty()
} else {
true
}
}?.getOrDefault(false) ?: true
}
private class InternalErrorHandler : AbstractCoroutineContextElement(CoroutineExceptionHandler),

View File

@@ -163,7 +163,7 @@ class ReaderActivity :
readerManager.replace(mode)
}
if (viewBinding.appbarTop.isVisible) {
lifecycle.postDelayed(hideUiRunnable, TimeUnit.SECONDS.toMillis(1))
lifecycle.postDelayed(TimeUnit.SECONDS.toMillis(1), hideUiRunnable)
}
viewBinding.slider.isRtl = mode == ReaderMode.REVERSED
}

View File

@@ -16,6 +16,7 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.coroutines.plus
import kotlinx.coroutines.yield
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
import org.koitharu.kotatsu.core.os.NetworkState
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
@@ -137,12 +138,13 @@ class PageHolderDelegate(
state = State.LOADING
error = null
callback.onLoadingStarted()
yield()
try {
val task = loader.loadPageAsync(data, force)
file = coroutineScope {
val progressObserver = observeProgress(this, task.progressAsFlow())
val file = task.await()
progressObserver.cancel()
progressObserver.cancelAndJoin()
file
}
state = State.LOADED

View File

@@ -116,7 +116,7 @@ open class PageHolder(
bindingInfo.progressBar.hide()
}
override fun onClick(v: View) {
final override fun onClick(v: View) {
when (v.id) {
R.id.button_retry -> delegate.retry(boundData?.toMangaPage() ?: return)
R.id.button_error_details -> delegate.showErrorDetails(boundData?.url)

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

@@ -7,6 +7,7 @@ import okhttp3.Response
import okhttp3.Route
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.core.network.CommonHeaders
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.scrobbling.common.data.ScrobblerStorage
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerService
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerType
@@ -49,8 +50,6 @@ class AniListAuthenticator @Inject constructor(
runBlocking { repository.authorize(null) }
return storage.accessToken
}.onFailure {
if (BuildConfig.DEBUG) {
it.printStackTrace()
}
it.printStackTraceDebug()
}.getOrNull()
}

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,6 +7,7 @@ import okhttp3.Response
import okhttp3.Route
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.core.network.CommonHeaders
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.scrobbling.common.data.ScrobblerStorage
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerService
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerType
@@ -49,9 +50,7 @@ class MALAuthenticator @Inject constructor(
runBlocking { repository.authorize(null) }
return storage.accessToken
}.onFailure {
if (BuildConfig.DEBUG) {
it.printStackTrace()
}
it.printStackTraceDebug()
}.getOrNull()
}

View File

@@ -7,6 +7,7 @@ import okhttp3.Response
import okhttp3.Route
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.core.network.CommonHeaders
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.scrobbling.common.data.ScrobblerStorage
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerService
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerType
@@ -49,8 +50,6 @@ class ShikimoriAuthenticator @Inject constructor(
runBlocking { repository.authorize(null) }
return storage.accessToken
}.onFailure {
if (BuildConfig.DEBUG) {
it.printStackTrace()
}
it.printStackTraceDebug()
}.getOrNull()
}

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

@@ -10,7 +10,6 @@ import android.provider.Settings
import android.view.View
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.app.LocaleManagerCompat
import androidx.core.view.postDelayed
import androidx.preference.ListPreference
import androidx.preference.Preference
import dagger.hilt.android.AndroidEntryPoint
@@ -21,8 +20,10 @@ import org.koitharu.kotatsu.core.ui.BasePreferenceFragment
import org.koitharu.kotatsu.core.ui.util.ActivityRecreationHandle
import org.koitharu.kotatsu.core.util.ext.getLocalesConfig
import org.koitharu.kotatsu.core.util.ext.map
import org.koitharu.kotatsu.core.util.ext.postDelayed
import org.koitharu.kotatsu.core.util.ext.setDefaultValueCompat
import org.koitharu.kotatsu.core.util.ext.toList
import org.koitharu.kotatsu.main.ui.MainActivity
import org.koitharu.kotatsu.parsers.util.names
import org.koitharu.kotatsu.parsers.util.toTitleCase
import org.koitharu.kotatsu.settings.utils.ActivityListPreference
@@ -92,11 +93,15 @@ class AppearanceSettingsFragment :
AppSettings.KEY_APP_LOCALE -> {
AppCompatDelegate.setApplicationLocales(settings.appLocales)
}
AppSettings.KEY_FIRST_NAV_ITEM -> {
activityRecreationHandle.recreate(MainActivity::class.java)
}
}
}
private fun postRestart() {
view?.postDelayed(400) {
viewLifecycleOwner.lifecycle.postDelayed(400) {
activityRecreationHandle.recreateAll()
}
}

View File

@@ -172,10 +172,20 @@ class SourcesManageFragment :
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder,
): Boolean = viewHolder.itemViewType == target.itemViewType && viewModel.reorderSources(
viewHolder.bindingAdapterPosition,
target.bindingAdapterPosition,
)
): Boolean = viewHolder.itemViewType == target.itemViewType
override fun onMoved(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
fromPos: Int,
target: RecyclerView.ViewHolder,
toPos: Int,
x: Int,
y: Int
) {
super.onMoved(recyclerView, viewHolder, fromPos, target, toPos, x, y)
viewModel.reorderSources(fromPos, toPos)
}
override fun canDropOver(
recyclerView: RecyclerView,

View File

@@ -4,17 +4,15 @@ import androidx.annotation.CheckResult
import androidx.core.os.LocaleListCompat
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.plus
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.getLocaleTitle
import org.koitharu.kotatsu.core.prefs.AppSettings
@@ -29,13 +27,12 @@ import org.koitharu.kotatsu.core.util.ext.toEnumSet
import org.koitharu.kotatsu.explore.data.MangaSourcesRepository
import org.koitharu.kotatsu.parsers.model.ContentType
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.util.move
import org.koitharu.kotatsu.parsers.util.toTitleCase
import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem
import java.util.Locale
import java.util.TreeMap
import javax.inject.Inject
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
private const val KEY_ENABLED = "!"
private const val TIP_REORDER = "src_reorder"
@@ -48,7 +45,7 @@ class SourcesManageViewModel @Inject constructor(
private val expandedGroups = MutableStateFlow(emptySet<String?>())
private var searchQuery = MutableStateFlow<String?>(null)
private val mutex = Mutex()
private var reorderJob: Job? = null
val content = combine(
repository.observeEnabledSources(),
@@ -62,23 +59,28 @@ class SourcesManageViewModel @Inject constructor(
val onActionDone = MutableEventFlow<ReversibleAction>()
fun reorderSources(oldPos: Int, newPos: Int): Boolean {
val snapshot = content.value
val item = (snapshot[oldPos] as? SourceConfigItem.SourceItem) ?: return false
if ((snapshot[newPos] as? SourceConfigItem.SourceItem)?.isDraggable != true) return false
launchAtomicJob(Dispatchers.Default) {
var targetPosition = 0
for ((i, x) in snapshot.withIndex()) {
if (i == newPos) {
break
}
if (x is SourceConfigItem.SourceItem) {
targetPosition++
fun reorderSources(oldPos: Int, newPos: Int) {
val snapshot = content.value.toMutableList()
val prevJob = reorderJob
reorderJob = launchJob(Dispatchers.Default) {
prevJob?.cancelAndJoin()
if ((snapshot[oldPos] as? SourceConfigItem.SourceItem)?.isDraggable != true) {
return@launchJob
}
if ((snapshot[newPos] as? SourceConfigItem.SourceItem)?.isDraggable != true) {
return@launchJob
}
delay(100)
snapshot.move(oldPos, newPos)
val newSourcesList = snapshot.mapNotNull { x ->
if (x is SourceConfigItem.SourceItem && x.isDraggable) {
x.source
} else {
null
}
}
repository.setPosition(item.source, targetPosition)
repository.setPositions(newSourcesList)
}
return true
}
fun canReorder(oldPos: Int, newPos: Int): Boolean {
@@ -88,7 +90,7 @@ class SourcesManageViewModel @Inject constructor(
}
fun setEnabled(source: MangaSource, isEnabled: Boolean) {
launchAtomicJob(Dispatchers.Default) {
launchJob(Dispatchers.Default) {
val rollback = repository.setSourceEnabled(source, isEnabled)
if (!isEnabled) {
onActionDone.call(ReversibleAction(R.string.source_disabled, rollback))
@@ -97,7 +99,7 @@ class SourcesManageViewModel @Inject constructor(
}
fun disableAll() {
launchAtomicJob(Dispatchers.Default) {
launchJob(Dispatchers.Default) {
repository.disableAllSources()
}
}
@@ -116,7 +118,7 @@ class SourcesManageViewModel @Inject constructor(
}
fun onTipClosed(item: SourceConfigItem.Tip) {
launchAtomicJob(Dispatchers.Default) {
launchJob(Dispatchers.Default) {
settings.closeTip(item.key)
}
}
@@ -203,15 +205,6 @@ class SourcesManageViewModel @Inject constructor(
return locale.getDisplayLanguage(locale).toTitleCase(locale)
}
private fun launchAtomicJob(
context: CoroutineContext = EmptyCoroutineContext,
block: suspend CoroutineScope.() -> Unit
) = launchJob(start = CoroutineStart.ATOMIC) {
mutex.withLock {
withContext(context, block)
}
}
private fun observeTip() = settings.observeAsFlow(AppSettings.KEY_TIPS_CLOSED) {
isTipEnabled(TIP_REORDER)
}

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

@@ -10,7 +10,6 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewTreeObserver
import android.widget.HorizontalScrollView
import android.widget.LinearLayout
import androidx.appcompat.view.ContextThemeWrapper
import androidx.core.view.isVisible
import androidx.customview.view.AbsSavedState
@@ -19,7 +18,9 @@ import androidx.preference.PreferenceViewHolder
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.prefs.ColorScheme
import org.koitharu.kotatsu.databinding.ItemColorSchemeBinding
import org.koitharu.kotatsu.databinding.PreferenceThemeBinding
import java.lang.ref.WeakReference
import com.google.android.material.R as materialR
class ThemeChooserPreference @JvmOverloads constructor(
context: Context,
@@ -43,38 +44,44 @@ class ThemeChooserPreference @JvmOverloads constructor(
override fun onBindViewHolder(holder: PreferenceViewHolder) {
super.onBindViewHolder(holder)
val layout = holder.findViewById(R.id.linear) as? LinearLayout ?: return
val scrollView = holder.findViewById(R.id.scrollView) as? HorizontalScrollView ?: return
val binding = PreferenceThemeBinding.bind(holder.itemView)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
scrollView.suppressLayout(true)
layout.suppressLayout(true)
binding.scrollView.suppressLayout(true)
binding.linear.suppressLayout(true)
}
layout.removeAllViews()
binding.linear.removeAllViews()
for (theme in entries) {
val context = ContextThemeWrapper(context, theme.styleResId)
val item = ItemColorSchemeBinding.inflate(LayoutInflater.from(context), layout, false)
item.card.isChecked = theme == currentValue
val item = ItemColorSchemeBinding.inflate(LayoutInflater.from(context), binding.linear, false)
val isSelected = theme == currentValue
item.card.isChecked = isSelected
item.card.strokeWidth = if (isSelected) context.resources.getDimensionPixelSize(
materialR.dimen.m3_comp_outlined_card_outline_width,
) else 0
item.textViewTitle.setText(theme.titleResId)
item.root.tag = theme
item.card.tag = theme
item.imageViewCheck.isVisible = theme == currentValue
item.root.setOnClickListener(itemClickListener)
item.card.setOnClickListener(itemClickListener)
layout.addView(item.root)
binding.linear.addView(item.root)
if (isSelected) {
item.root.requestFocus()
}
}
if (lastScrollPosition[0] >= 0) {
val scroller = Scroller(scrollView, lastScrollPosition[0])
val scroller = Scroller(binding.scrollView, lastScrollPosition[0])
scroller.run()
scrollView.post(scroller)
binding.scrollView.post(scroller)
}
scrollView.viewTreeObserver.run {
binding.scrollView.viewTreeObserver.run {
scrollPersistListener?.let { removeOnScrollChangedListener(it) }
scrollPersistListener = ScrollPersistListener(WeakReference(scrollView), lastScrollPosition)
scrollPersistListener = ScrollPersistListener(WeakReference(binding.scrollView), lastScrollPosition)
addOnScrollChangedListener(scrollPersistListener)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
layout.suppressLayout(false)
scrollView.suppressLayout(false)
binding.linear.suppressLayout(false)
binding.scrollView.suppressLayout(false)
}
}

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

@@ -53,7 +53,6 @@ import org.koitharu.kotatsu.core.logs.TrackerLogger
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.util.ext.awaitUniqueWorkInfoByName
import org.koitharu.kotatsu.core.util.ext.checkNotificationPermission
import org.koitharu.kotatsu.core.util.ext.powerManager
import org.koitharu.kotatsu.core.util.ext.toBitmapOrNull
import org.koitharu.kotatsu.core.util.ext.trySetForeground
import org.koitharu.kotatsu.details.ui.DetailsActivity
@@ -141,7 +140,7 @@ class TrackWorker @AssistedInject constructor(
}
private suspend fun checkUpdatesAsync(tracks: List<TrackingItem>): List<MangaUpdates> {
val semaphore = Semaphore(if (isDeviceInUse()) 1 else MAX_PARALLELISM)
val semaphore = Semaphore(MAX_PARALLELISM)
return channelFlow {
for ((track, channelId) in tracks) {
launch {
@@ -290,15 +289,6 @@ class TrackWorker @AssistedInject constructor(
.build()
}
private fun isDeviceInUse(): Boolean {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
return false
}
val powerManager = applicationContext.powerManager ?: return false
return !powerManager.isDeviceIdleMode
}
@Reusable
class Scheduler @Inject constructor(
private val workManager: WorkManager,

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

@@ -16,7 +16,7 @@
app:headerLayout="@layout/navigation_rail_fab"
app:labelVisibilityMode="labeled"
app:layout_constraintStart_toStartOf="parent"
app:menu="@menu/nav_bottom" />
tools:menu="@menu/nav_bottom" />
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"

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

@@ -41,6 +41,7 @@
style="@style/Widget.Material3.BottomSheet"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:backgroundTint="?attr/m3ColorBottomMenuBackground"
android:orientation="vertical"
android:visibility="gone"
app:behavior_hideable="false"
@@ -51,8 +52,7 @@
<FrameLayout
android:id="@+id/layout_bs_header"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
android:layout_height="wrap_content">
<com.google.android.material.bottomsheet.BottomSheetDragHandleView
android:id="@+id/dragHandle"

View File

@@ -92,7 +92,7 @@
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:clickable="true"
app:menu="@menu/nav_bottom"
tools:ignore="KeyboardInaccessibleWidget" />
tools:ignore="KeyboardInaccessibleWidget"
tools:menu="@menu/nav_bottom" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@@ -10,7 +10,8 @@
<androidx.fragment.app.FragmentContainerView
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent" />
android:layout_height="match_parent"
tools:background="@color/grey" />
<org.koitharu.kotatsu.reader.ui.ReaderInfoBarView
android:id="@+id/infoBar"
@@ -25,6 +26,7 @@
android:id="@+id/appbar_top"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?m3ColorBottomMenuBackground"
android:elevation="@dimen/m3_card_elevated_elevation"
app:elevation="@dimen/m3_card_elevated_elevation"
app:liftOnScroll="false">
@@ -46,6 +48,7 @@
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:layout_margin="8dp"
app:cardBackgroundColor="?m3ColorBottomMenuBackground"
app:layout_insetEdge="bottom">
<com.google.android.material.appbar.MaterialToolbar
@@ -60,6 +63,7 @@
android:layout_height="wrap_content"
android:stepSize="1"
android:valueFrom="0"
app:trackColorInactive="?attr/m3ColorBackground"
app:labelBehavior="floating"
app:tickVisible="false" />

View File

@@ -15,7 +15,8 @@
style="?materialCardViewFilledStyle"
android:layout_width="@dimen/widget_cover_width"
android:layout_height="@dimen/widget_cover_height"
android:focusableInTouchMode="false">
android:focusableInTouchMode="false"
app:strokeColor="?colorOutline">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"

View File

@@ -3,10 +3,10 @@
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"
style="@style/Widget.Material3.CardView.Outlined"
style="?materialCardViewOutlinedStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:cardBackgroundColor="?scrimBackground">
app:cardBackgroundColor="?colorBackgroundFloating">
<org.koitharu.kotatsu.core.ui.widgets.CoverImageView
android:id="@+id/imageView_thumb"

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

@@ -5,8 +5,8 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginVertical="8dp"
android:layout_marginHorizontal="8dp"
android:layout_marginVertical="8dp"
app:shapeAppearance="?shapeAppearanceCornerExtraLarge"
tools:icon="@drawable/ic_notification"
tools:primaryButtonText="@string/settings"

View File

@@ -60,6 +60,7 @@
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:clipToPadding="false"
android:descendantFocusability="blocksDescendants"
android:paddingStart="0dp"
android:paddingEnd="16dp"
android:scrollbars="none"

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

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<menu
xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/nav_favourites"
android:icon="@drawable/ic_favourites_selector"
android:title="@string/favourites" />
<item
android:id="@+id/nav_history"
android:icon="@drawable/ic_history_selector"
android:title="@string/history" />
<item
android:id="@+id/nav_explore"
android:icon="@drawable/ic_explore_selector"
android:title="@string/explore" />
<item
android:id="@+id/nav_feed"
android:icon="@drawable/ic_feed_selector"
android:title="@string/feed" />
</menu>

View File

@@ -273,7 +273,7 @@
<string name="show_reading_indicators_summary">Паказваць працэнт прачытанага ў гісторыі і абраных</string>
<string name="data_deletion">Выдаленне даных</string>
<string name="show_all">Паказаць усе</string>
<string name="exclude_nsfw_from_history_summary">Манга, пазначаная як NSFW, ніколі не будзе дададзеная ў гісторыю і ваш прагрэс не будзе захаваны</string>
<string name="exclude_nsfw_from_history_summary">Манга, пазначаная як NSFW, не будзе дададзеная ў гісторыю і ваш прагрэс не будзе захаваны</string>
<string name="clear_cookies_summary">Можа дапамагчы з некаторымі праблемам. Усе аўтарызацыі будуць ануляваныя</string>
<string name="not_found_404">Змесціва не знойдзена ці выдалена</string>
<string name="status_re_reading">Перачытваю</string>
@@ -324,7 +324,7 @@
<string name="discard">Адмяніць</string>
<string name="enable_logging">Уключыць запіс</string>
<string name="share_logs">Падзяліцца логамі</string>
<string name="enable_logging_summary">Запішыце некаторыя дзеянні для адладкі</string>
<string name="enable_logging_summary">Запішыце некаторыя дзеянні для адладкі. Уключайце толькі калі ведаеце, што робіце</string>
<string name="show_suspicious_content">Паказаць падазроны кантэнт</string>
<string name="text_shelf_holder_primary">Ваша манга будзе адлюстроўвацца тут</string>
<string name="text_shelf_holder_secondary">Знайдзіце, што пачытаць, у раздзеле «Агляд»</string>
@@ -368,7 +368,7 @@
<string name="options">Параметры</string>
<string name="user_agent">Загаловак UserAgent</string>
<string name="allow_unstable_updates">Дазволіць нестабільныя абнаўленні</string>
<string name="allow_unstable_updates_summary">Прапануйце абнаўленні да бэта-версій праграмы</string>
<string name="allow_unstable_updates_summary">Атрымліваць апавяшчэнні аб нестабільных зборках</string>
<string name="download_started">Спампоўка пачалася</string>
<string name="settings_apply_restart_required">Калі ласка, перазапусціце праграму каб прымяніць змены</string>
<string name="got_it">Зразумеў</string>
@@ -383,7 +383,7 @@
<string name="sync_settings">Налады сінхранізацыі</string>
<string name="server_address">Адрас сервера</string>
<string name="mirror_switching">Выберыце люстэрка аўтаматычна</string>
<string name="mirror_switching_summary">Аўтаматычна пераключаць дамены для аддаленых крыніц у выпадку памылак, калі даступныя люстэркі</string>
<string name="mirror_switching_summary">Аўтаматычна пераключаць дамены для крыніц мангі ў выпадку памылак, калі даступныя люстэркі</string>
<string name="ignore_ssl_errors">Ігнараваць памылкі SSL</string>
<string name="resume">Аднавіць</string>
<string name="paused">Прыпынена</string>
@@ -466,4 +466,9 @@
<string name="languages">Мовы</string>
<string name="unknown">Невядомы</string>
<string name="in_progress">У працэсе</string>
<string name="disable_nsfw">Адключыць NSFW</string>
<string name="too_many_requests_message">Занадта шмат запытаў. Паўтарыце спробу пазней</string>
<string name="related_manga_summary">Паказаць спіс звязанай мангі. У некаторых выпадках ён можа быць недакладным або адсутнічаць</string>
<string name="advanced">Пашыраныя</string>
<string name="default_section">Раздзел па змаўчанні</string>
</resources>

View File

@@ -350,7 +350,7 @@
<string name="mark_as_current">Marcar como actual</string>
<string name="language">Idioma</string>
<string name="share_logs">Compartir los registros</string>
<string name="enable_logging_summary">Grabar algunas acciones para depurar</string>
<string name="enable_logging_summary">Grabar algunas interaciones para de depurar los fallos. No lo actives si no estás seguro de lo que estás haciendo</string>
<string name="enable_logging">Activar el registro</string>
<string name="show_suspicious_content">Mostrar contenido sospechoso</string>
<string name="theme_name_dynamic">Dinámico</string>
@@ -366,7 +366,7 @@
<string name="theme_name_kanade">Canadé</string>
<string name="scrobbling_empty_hint">Para realizar un seguimiento del progreso de la lectura, seleccione Menú → Seguimiento en la pantalla de detalles del manga.</string>
<string name="services">Servicios</string>
<string name="allow_unstable_updates_summary">Actualizaciones propuestas para las versiones beta de la aplicación</string>
<string name="allow_unstable_updates_summary">Recibir notificaciones sobre versiones inestables</string>
<string name="allow_unstable_updates">Permitir actualizaciones inestables</string>
<string name="download_started">Descarga iniciada</string>
<string name="user_agent">Encabezado del agente de usuario</string>
@@ -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

@@ -350,7 +350,7 @@
<string name="mark_as_current">Marquer comme actuel</string>
<string name="share_logs">Partager les journaux</string>
<string name="enable_logging">Activer la journalisation</string>
<string name="enable_logging_summary">Enregistrer certaines actions à des fins de débogage</string>
<string name="enable_logging_summary">Enregistrer quelques actions à des fins de débogage. Ne l\'activez pas si vous ne savez pas ce que vous faites</string>
<string name="language">Langue</string>
<string name="show_suspicious_content">Afficher le contenu suspect</string>
<string name="theme_name_dynamic">Dynamique</string>
@@ -368,7 +368,7 @@
<string name="allow_unstable_updates">Autoriser les mises à jour instables</string>
<string name="download_started">Téléchargement commencé</string>
<string name="theme_name_kanade">Kanade</string>
<string name="allow_unstable_updates_summary">Proposer des mises à jour des versions bêta de l\'application</string>
<string name="allow_unstable_updates_summary">Recevoir des notifications sur les moutures instables</string>
<string name="user_agent">En-tête UserAgent</string>
<string name="settings_apply_restart_required">Veuillez redémarrer l\'application pour appliquer ces changements</string>
<string name="got_it">Compris</string>
@@ -389,7 +389,7 @@
<string name="sync_settings">Paramètres de synchronisation</string>
<string name="ignore_ssl_errors">Ignorer les erreurs SSL</string>
<string name="mirror_switching">Choisir le miroir automatiquement</string>
<string name="mirror_switching_summary">Changement automatique de domaine pour les sources distantes en cas d\'erreur si des miroirs sont disponibles</string>
<string name="mirror_switching_summary">Changement automatique de domaine pour les sources de mangas en cas d\'erreur si des miroirs sont disponibles</string>
<string name="pause">Mettre en pause</string>
<string name="resume">Reprendre</string>
<string name="paused">En pause</string>
@@ -466,4 +466,7 @@
<string name="languages">Langues</string>
<string name="unknown">Inconnu</string>
<string name="in_progress">En cours</string>
<string name="disable_nsfw">Désactiver NSFW</string>
<string name="too_many_requests_message">Trop de requêtes. Réessayez plus tard</string>
<string name="related_manga_summary">Afficher une liste de mangas connexes. Dans certains cas, elle peut être inexacte ou manquante</string>
</resources>

View File

@@ -345,7 +345,7 @@
<string name="share_logs">Bagikan log</string>
<string name="error_no_space_left">Tidak ada ruang tersisa di perangkat</string>
<string name="allow_unstable_updates">Izinkan pembaruan yang tidak stabil</string>
<string name="allow_unstable_updates_summary">Usulkan pembaruan ke versi beta</string>
<string name="allow_unstable_updates_summary">Terima pemberitahuan tentang build yang tidak stabil</string>
<string name="download_started">Unduh dimulai</string>
<string name="show_reading_indicators">Tampilkan indikator kemajuan membaca</string>
<string name="show_reading_indicators_summary">Tampilkan persentase baca dalam riwayat dan favorit</string>
@@ -354,7 +354,7 @@
<string name="zoom_mode_keep_start">Tetap di awal</string>
<string name="reader_info_pattern">Bab. %1$d/%2$d Hal. %3$d/%4$d</string>
<string name="enable_logging">Aktifkan pencatatan</string>
<string name="enable_logging_summary">Rekam beberapa tindakan untuk tujuan debug</string>
<string name="enable_logging_summary">Rekam beberapa tindakan untuk tujuan debug. Jangan nyalakan jika Anda tidak yakin dengan apa yang Anda lakukan</string>
<string name="theme_name_dynamic">Dinamis</string>
<string name="color_theme">Skema warna</string>
<string name="show_in_grid_view">Perlihatkan dalam tampilan kisi</string>
@@ -469,4 +469,7 @@
<string name="languages">Bahasa</string>
<string name="unknown">Tidak diketahui</string>
<string name="in_progress">Dalam progres</string>
<string name="advanced">Lanjutan</string>
<string name="default_section">Bagian default</string>
<string name="manga_list">Daftar manga</string>
</resources>

View File

@@ -36,6 +36,9 @@
<item name="colorOnErrorContainer">#FFDAD6</item>
<item name="colorOutline">#899391</item>
<item name="colorOutlineVariant">#3F4947</item>
<item name="m3ColorBackground">@color/background_miku</item>
<item name="m3ColorExploreButton">#1D2020</item>
<item name="m3ColorBottomMenuBackground">#272B2A</item>
</style>
<style name="Theme.Kotatsu.Asuka">
@@ -73,6 +76,9 @@
<item name="colorOnErrorContainer">#FFDAD6</item>
<item name="colorOutline">#A08C8B</item>
<item name="colorOutlineVariant">#534342</item>
<item name="m3ColorBackground">@color/background_asuka</item>
<item name="m3ColorExploreButton">#251E1E</item>
<item name="m3ColorBottomMenuBackground">#2F2828</item>
</style>
<style name="Theme.Kotatsu.Mion">
@@ -110,6 +116,9 @@
<item name="colorOnErrorContainer">#FFDAD6</item>
<item name="colorOutline">#8A938C</item>
<item name="colorOutlineVariant">#404943</item>
<item name="m3ColorBackground">@color/background_mion</item>
<item name="m3ColorExploreButton">#1D201E</item>
<item name="m3ColorBottomMenuBackground">#272B28</item>
</style>
<style name="Theme.Kotatsu.Rikka">
@@ -147,6 +156,9 @@
<item name="colorOnErrorContainer">#FFDAD6</item>
<item name="colorOutline">#9F8C8C</item>
<item name="colorOutlineVariant">#524343</item>
<item name="m3ColorBackground">@color/background_rikka</item>
<item name="m3ColorExploreButton">#241E1E</item>
<item name="m3ColorBottomMenuBackground">#2F2828</item>
</style>
<style name="Theme.Kotatsu.Sakura">
@@ -184,6 +196,9 @@
<item name="colorOnErrorContainer">#FFDAD6</item>
<item name="colorOutline">#899296</item>
<item name="colorOutlineVariant">#40484B</item>
<item name="m3ColorBackground">@color/background_sakura</item>
<item name="m3ColorExploreButton">#1D2021</item>
<item name="m3ColorBottomMenuBackground">#272A2C</item>
</style>
<style name="Theme.Kotatsu.Mamimi">
@@ -221,6 +236,9 @@
<item name="colorOnErrorContainer">#FFDAD6</item>
<item name="colorOutline">#9F8C8D</item>
<item name="colorOutlineVariant">#524344</item>
<item name="m3ColorBackground">@color/background_mamimi</item>
<item name="m3ColorExploreButton">#241E1E</item>
<item name="m3ColorBottomMenuBackground">#2F2828</item>
</style>
<style name="Theme.Kotatsu.Kanade">
@@ -258,5 +276,8 @@
<item name="colorOnErrorContainer">#FFDAD6</item>
<item name="colorOutline">#959595</item>
<item name="colorOutlineVariant">#494949</item>
<item name="m3ColorBackground">@color/background_kanade</item>
<item name="m3ColorExploreButton">#292929</item>
<item name="m3ColorBottomMenuBackground">#2C2C2C</item>
</style>
</resources>

View File

@@ -14,11 +14,11 @@
<color name="splash_primary">#0C4091</color>
<!-- Color schemes colors -->
<color name="background_miku">#272B2A</color>
<color name="background_asuka">#2F2828</color>
<color name="background_mion">#272B28</color>
<color name="background_rikka">#2F2828</color>
<color name="background_sakura">#272A2C</color>
<color name="background_mamimi">#2F2828</color>
<color name="background_kanade">#2B2B2B</color>
<color name="background_miku">#191C1C</color>
<color name="background_asuka">#201A1A</color>
<color name="background_mion">#191C1A</color>
<color name="background_rikka">#201A1A</color>
<color name="background_sakura">#191C1D</color>
<color name="background_mamimi">#201A1A</color>
<color name="background_kanade">#222222</color>
</resources>

View File

@@ -44,5 +44,4 @@
<color name="kotatsu_m3_background">#1A1B1F</color>
<color name="kotatsu_m3_exploreButton">#292A2D</color>
<color name="kotatsu_m3_bottom_menu">#2F3033</color>
<color name="kotatsu_m3_content_scrim">#2F3033</color>
</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>

Some files were not shown because too many files have changed in this diff Show More