Compare commits

...

9 Commits
v5.1 ... v5.1.1

Author SHA1 Message Date
gallegonovato
3778a9e1d4 Translated using Weblate (Spanish)
Currently translated at 100.0% (416 of 416 strings)

Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/
Translation: Kotatsu/Strings
2023-05-17 18:51:56 +03:00
Макар Разин
71ecd9d8e2 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (416 of 416 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (416 of 416 strings)

Translated using Weblate (Belarusian)

Currently translated at 100.0% (416 of 416 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-05-17 18:51:56 +03:00
Subham Jena
7cba8d2dc7 Translated using Weblate (Odia)
Currently translated at 3.8% (16 of 416 strings)

Translated using Weblate (Odia)

Currently translated at 2.4% (10 of 415 strings)

Co-authored-by: Subham Jena <subhamjena8465@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/or/
Translation: Kotatsu/Strings
2023-05-17 18:51:56 +03:00
GpixeL
79c2927da2 Translated using Weblate (Indonesian)
Currently translated at 96.8% (402 of 415 strings)

Co-authored-by: GpixeL <gamesfire313@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/id/
Translation: Kotatsu/Strings
2023-05-17 18:51:56 +03:00
ctntt
a4a28c7342 Translated using Weblate (German)
Currently translated at 99.2% (413 of 416 strings)

Translated using Weblate (German)

Currently translated at 99.5% (413 of 415 strings)

Co-authored-by: ctntt <pavlov_mainstreamed@slmail.me>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/de/
Translation: Kotatsu/Strings
2023-05-17 18:51:56 +03:00
Koitharu
43a92bdf08 Improve sync logging 2023-05-17 17:00:42 +03:00
Koitharu
51ff1ff7b7 Fix concurrent chapters loading in reader 2023-05-17 16:17:18 +03:00
Koitharu
2e0eb5de54 Fix handling special characters in local manga filenames 2023-05-16 13:07:11 +03:00
Koitharu
4f68e7d0e6 Handle WebView unavailability 2023-05-16 07:56:05 +03:00
24 changed files with 180 additions and 47 deletions

View File

@@ -15,8 +15,8 @@ android {
applicationId 'org.koitharu.kotatsu' applicationId 'org.koitharu.kotatsu'
minSdkVersion 21 minSdkVersion 21
targetSdkVersion 33 targetSdkVersion 33
versionCode 544 versionCode 545
versionName '5.1' versionName '5.1.1'
generatedDensities = [] generatedDensities = []
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
@@ -83,7 +83,7 @@ dependencies {
} }
implementation 'org.jetbrains.kotlin:kotlin-stdlib:1.8.21' implementation 'org.jetbrains.kotlin:kotlin-stdlib:1.8.21'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.0' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.1'
implementation 'androidx.appcompat:appcompat:1.6.1' implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'androidx.core:core-ktx:1.10.1' implementation 'androidx.core:core-ktx:1.10.1'
@@ -140,17 +140,17 @@ dependencies {
testImplementation 'junit:junit:4.13.2' testImplementation 'junit:junit:4.13.2'
testImplementation 'org.json:json:20230227' testImplementation 'org.json:json:20230227'
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.0' testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.1'
androidTestImplementation 'androidx.test:runner:1.5.2' androidTestImplementation 'androidx.test:runner:1.5.2'
androidTestImplementation 'androidx.test:rules:1.5.0' androidTestImplementation 'androidx.test:rules:1.5.0'
androidTestImplementation 'androidx.test:core-ktx:1.5.0' androidTestImplementation 'androidx.test:core-ktx:1.5.0'
androidTestImplementation 'androidx.test.ext:junit-ktx:1.1.5' androidTestImplementation 'androidx.test.ext:junit-ktx:1.1.5'
androidTestImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.0' androidTestImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.1'
androidTestImplementation 'androidx.room:room-testing:2.5.1' androidTestImplementation 'androidx.room:room-testing:2.5.1'
androidTestImplementation 'com.squareup.moshi:moshi-kotlin:1.14.0' androidTestImplementation 'com.squareup.moshi:moshi-kotlin:1.15.0'
androidTestImplementation 'com.google.dagger:hilt-android-testing:2.46.1' androidTestImplementation 'com.google.dagger:hilt-android-testing:2.46.1'
kaptAndroidTest 'com.google.dagger:hilt-android-compiler:2.46.1' kaptAndroidTest 'com.google.dagger:hilt-android-compiler:2.46.1'

View File

@@ -8,19 +8,22 @@ abstract class BoundsScrollListener(private val offsetTop: Int, private val offs
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy) super.onScrolled(recyclerView, dx, dy)
if (recyclerView.hasPendingAdapterUpdates()) {
return
}
val layoutManager = (recyclerView.layoutManager as? LinearLayoutManager) ?: return val layoutManager = (recyclerView.layoutManager as? LinearLayoutManager) ?: return
val firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition() val firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition()
if (firstVisibleItemPosition == RecyclerView.NO_POSITION) { if (firstVisibleItemPosition == RecyclerView.NO_POSITION) {
return return
} }
if (firstVisibleItemPosition <= offsetTop) {
onScrolledToStart(recyclerView)
}
val visibleItemCount = layoutManager.childCount val visibleItemCount = layoutManager.childCount
val totalItemCount = layoutManager.itemCount val totalItemCount = layoutManager.itemCount
if (visibleItemCount + firstVisibleItemPosition >= totalItemCount - offsetBottom) { if (visibleItemCount + firstVisibleItemPosition >= totalItemCount - offsetBottom) {
onScrolledToEnd(recyclerView) onScrolledToEnd(recyclerView)
} }
if (firstVisibleItemPosition <= offsetTop) {
onScrolledToStart(recyclerView)
}
} }
abstract fun onScrolledToStart(recyclerView: RecyclerView) abstract fun onScrolledToStart(recyclerView: RecyclerView)

View File

@@ -0,0 +1,30 @@
package org.koitharu.kotatsu.base.ui.list
import androidx.recyclerview.widget.RecyclerView
class ScrollListenerInvalidationObserver(
private val recyclerView: RecyclerView,
private val scrollListener: RecyclerView.OnScrollListener,
) : RecyclerView.AdapterDataObserver() {
override fun onChanged() {
super.onChanged()
invalidateScroll()
}
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
super.onItemRangeInserted(positionStart, itemCount)
invalidateScroll()
}
override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) {
super.onItemRangeRemoved(positionStart, itemCount)
invalidateScroll()
}
private fun invalidateScroll() {
recyclerView.post {
scrollListener.onScrolled(recyclerView, 0, 0)
}
}
}

View File

@@ -15,6 +15,7 @@ import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseActivity import org.koitharu.kotatsu.base.ui.BaseActivity
import org.koitharu.kotatsu.core.network.CommonHeadersInterceptor import org.koitharu.kotatsu.core.network.CommonHeadersInterceptor
import org.koitharu.kotatsu.databinding.ActivityBrowserBinding import org.koitharu.kotatsu.databinding.ActivityBrowserBinding
import org.koitharu.kotatsu.utils.ext.catchingWebViewUnavailability
import com.google.android.material.R as materialR import com.google.android.material.R as materialR
@SuppressLint("SetJavaScriptEnabled") @SuppressLint("SetJavaScriptEnabled")
@@ -24,7 +25,9 @@ class BrowserActivity : BaseActivity<ActivityBrowserBinding>(), BrowserCallback
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(ActivityBrowserBinding.inflate(layoutInflater)) if (!catchingWebViewUnavailability { setContentView(ActivityBrowserBinding.inflate(layoutInflater)) }) {
return
}
supportActionBar?.run { supportActionBar?.run {
setDisplayHomeAsUpEnabled(true) setDisplayHomeAsUpEnabled(true)
setHomeAsUpIndicator(materialR.drawable.abc_ic_clear_material) setHomeAsUpIndicator(materialR.drawable.abc_ic_clear_material)

View File

@@ -4,12 +4,15 @@ import android.content.Context
import androidx.annotation.WorkerThread import androidx.annotation.WorkerThread
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.cancelAndJoin import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.runInterruptible import kotlinx.coroutines.runInterruptible
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
import org.koitharu.kotatsu.utils.ext.processLifecycleScope import org.koitharu.kotatsu.utils.ext.processLifecycleScope
@@ -82,6 +85,15 @@ class FileLogger(
flushImpl() flushImpl()
} }
@WorkerThread
fun flushBlocking() {
if (!isEnabled) {
return
}
runBlockingSafe { flushJob?.cancelAndJoin() }
runBlockingSafe { flushImpl() }
}
private fun postFlush() { private fun postFlush() {
if (flushJob?.isActive == true) { if (flushJob?.isActive == true) {
return return
@@ -96,10 +108,10 @@ class FileLogger(
} }
} }
private suspend fun flushImpl() { private suspend fun flushImpl() = withContext(NonCancellable) {
mutex.withLock { mutex.withLock {
if (buffer.isEmpty()) { if (buffer.isEmpty()) {
return return@withContext
} }
runInterruptible(Dispatchers.IO) { runInterruptible(Dispatchers.IO) {
if (file.length() > MAX_SIZE_BYTES) { if (file.length() > MAX_SIZE_BYTES) {
@@ -131,4 +143,9 @@ class FileLogger(
} }
bakFile.delete() bakFile.delete()
} }
private inline fun runBlockingSafe(crossinline block: suspend () -> Unit) = try {
runBlocking(NonCancellable) { block() }
} catch (_: InterruptedException) {
}
} }

View File

@@ -31,7 +31,8 @@ sealed class LocalMangaInput(
} }
@JvmStatic @JvmStatic
protected fun zipUri(file: File, entryName: String) = "cbz://${file.path}#$entryName" protected fun zipUri(file: File, entryName: String): String =
Uri.fromParts("cbz", file.path, entryName).toString()
@JvmStatic @JvmStatic
protected fun Manga.copy2( protected fun Manga.copy2(

View File

@@ -252,6 +252,7 @@ class ReaderViewModel @Inject constructor(
val prevJob = stateChangeJob val prevJob = stateChangeJob
stateChangeJob = launchJob(Dispatchers.Default) { stateChangeJob = launchJob(Dispatchers.Default) {
prevJob?.cancelAndJoin() prevJob?.cancelAndJoin()
loadingJob?.join()
val pages = content.value?.pages ?: return@launchJob val pages = content.value?.pages ?: return@launchJob
pages.getOrNull(position)?.let { page -> pages.getOrNull(position)?.let { page ->
currentState.update { cs -> currentState.update { cs ->
@@ -263,12 +264,12 @@ class ReaderViewModel @Inject constructor(
return@launchJob return@launchJob
} }
ensureActive() ensureActive()
if (position <= BOUNDS_PAGE_OFFSET) {
loadPrevNextChapter(pages.first().chapterId, isNext = false)
}
if (position >= pages.lastIndex - BOUNDS_PAGE_OFFSET) { if (position >= pages.lastIndex - BOUNDS_PAGE_OFFSET) {
loadPrevNextChapter(pages.last().chapterId, isNext = true) loadPrevNextChapter(pages.last().chapterId, isNext = true)
} }
if (position <= BOUNDS_PAGE_OFFSET) {
loadPrevNextChapter(pages.first().chapterId, isNext = false)
}
if (pageLoader.isPrefetchApplicable()) { if (pageLoader.isPrefetchApplicable()) {
pageLoader.prefetch(pages.trySublist(position + 1, position + PREFETCH_LIMIT)) pageLoader.prefetch(pages.trySublist(position + 1, position + PREFETCH_LIMIT))
} }
@@ -348,7 +349,9 @@ class ReaderViewModel @Inject constructor(
@AnyThread @AnyThread
private fun loadPrevNextChapter(currentId: Long, isNext: Boolean) { private fun loadPrevNextChapter(currentId: Long, isNext: Boolean) {
val prevJob = loadingJob
loadingJob = launchLoadingJob(Dispatchers.Default) { loadingJob = launchLoadingJob(Dispatchers.Default) {
prevJob?.join()
chaptersLoader.loadPrevNextChapter(mangaData.requireValue(), currentId, isNext) chaptersLoader.loadPrevNextChapter(mangaData.requireValue(), currentId, isNext)
content.emitValue(ReaderContent(chaptersLoader.snapshot(), null)) content.emitValue(ReaderContent(chaptersLoader.snapshot(), null))
} }

View File

@@ -25,6 +25,7 @@ import org.koitharu.kotatsu.databinding.ActivityBrowserBinding
import org.koitharu.kotatsu.parsers.MangaParserAuthProvider import org.koitharu.kotatsu.parsers.MangaParserAuthProvider
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.utils.TaggedActivityResult import org.koitharu.kotatsu.utils.TaggedActivityResult
import org.koitharu.kotatsu.utils.ext.catchingWebViewUnavailability
import org.koitharu.kotatsu.utils.ext.getSerializableExtraCompat import org.koitharu.kotatsu.utils.ext.getSerializableExtraCompat
import javax.inject.Inject import javax.inject.Inject
import com.google.android.material.R as materialR import com.google.android.material.R as materialR
@@ -41,7 +42,9 @@ class SourceAuthActivity : BaseActivity<ActivityBrowserBinding>(), BrowserCallba
@SuppressLint("SetJavaScriptEnabled") @SuppressLint("SetJavaScriptEnabled")
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(ActivityBrowserBinding.inflate(layoutInflater)) if (!catchingWebViewUnavailability { setContentView(ActivityBrowserBinding.inflate(layoutInflater)) }) {
return
}
val source = intent?.getSerializableExtraCompat(EXTRA_SOURCE) as? MangaSource val source = intent?.getSerializableExtraCompat(EXTRA_SOURCE) as? MangaSource
if (source == null) { if (source == null) {
finishAfterTransition() finishAfterTransition()

View File

@@ -13,6 +13,7 @@ import androidx.annotation.WorkerThread
import androidx.core.content.contentValuesOf import androidx.core.content.contentValuesOf
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import okhttp3.Response
import org.json.JSONArray import org.json.JSONArray
import org.json.JSONObject import org.json.JSONObject
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
@@ -22,7 +23,9 @@ import org.koitharu.kotatsu.core.db.TABLE_HISTORY
import org.koitharu.kotatsu.core.db.TABLE_MANGA import org.koitharu.kotatsu.core.db.TABLE_MANGA
import org.koitharu.kotatsu.core.db.TABLE_MANGA_TAGS import org.koitharu.kotatsu.core.db.TABLE_MANGA_TAGS
import org.koitharu.kotatsu.core.db.TABLE_TAGS import org.koitharu.kotatsu.core.db.TABLE_TAGS
import org.koitharu.kotatsu.core.logs.LoggersModule
import org.koitharu.kotatsu.core.network.GZipInterceptor import org.koitharu.kotatsu.core.network.GZipInterceptor
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.parsers.util.json.mapJSONTo import org.koitharu.kotatsu.parsers.util.json.mapJSONTo
import org.koitharu.kotatsu.sync.data.SyncAuthApi import org.koitharu.kotatsu.sync.data.SyncAuthApi
import org.koitharu.kotatsu.sync.data.SyncAuthenticator import org.koitharu.kotatsu.sync.data.SyncAuthenticator
@@ -61,6 +64,7 @@ class SyncHelper(
} }
private val defaultGcPeriod: Long // gc period if sync enabled private val defaultGcPeriod: Long // gc period if sync enabled
get() = TimeUnit.DAYS.toMillis(4) get() = TimeUnit.DAYS.toMillis(4)
private val logger = LoggersModule.provideSyncLogger(context, AppSettings(context))
fun syncFavourites(syncResult: SyncResult) { fun syncFavourites(syncResult: SyncResult) {
val data = JSONObject() val data = JSONObject()
@@ -71,7 +75,7 @@ class SyncHelper(
.url("$baseUrl/resource/$TABLE_FAVOURITES") .url("$baseUrl/resource/$TABLE_FAVOURITES")
.post(data.toRequestBody()) .post(data.toRequestBody())
.build() .build()
val response = httpClient.newCall(request).execute().parseJsonOrNull() val response = httpClient.newCall(request).execute().log().parseJsonOrNull()
if (response != null) { if (response != null) {
val timestamp = response.getLong(FIELD_TIMESTAMP) val timestamp = response.getLong(FIELD_TIMESTAMP)
val categoriesResult = val categoriesResult =
@@ -93,7 +97,7 @@ class SyncHelper(
.url("$baseUrl/resource/$TABLE_HISTORY") .url("$baseUrl/resource/$TABLE_HISTORY")
.post(data.toRequestBody()) .post(data.toRequestBody())
.build() .build()
val response = httpClient.newCall(request).execute().parseJsonOrNull() val response = httpClient.newCall(request).execute().log().parseJsonOrNull()
if (response != null) { if (response != null) {
val result = upsertHistory( val result = upsertHistory(
json = response.getJSONArray(TABLE_HISTORY), json = response.getJSONArray(TABLE_HISTORY),
@@ -105,6 +109,19 @@ class SyncHelper(
gcHistory() gcHistory()
} }
fun onError(e: Throwable) {
if (logger.isEnabled) {
logger.log("Sync error", e)
}
}
fun onSyncComplete(result: SyncResult) {
if (logger.isEnabled) {
logger.log("Sync finshed: ${result.toDebugString()}")
logger.flushBlocking()
}
}
private fun upsertHistory(json: JSONArray, timestamp: Long): Array<ContentProviderResult> { private fun upsertHistory(json: JSONArray, timestamp: Long): Array<ContentProviderResult> {
val uri = uri(authorityHistory, TABLE_HISTORY) val uri = uri(authorityHistory, TABLE_HISTORY)
val operations = ArrayList<ContentProviderOperation>() val operations = ArrayList<ContentProviderOperation>()
@@ -298,4 +315,10 @@ class SyncHelper(
private fun JSONObject.removeJSONObject(name: String) = remove(name) as JSONObject private fun JSONObject.removeJSONObject(name: String) = remove(name) as JSONObject
private fun JSONObject.removeJSONArray(name: String) = remove(name) as JSONArray private fun JSONObject.removeJSONArray(name: String) = remove(name) as JSONArray
private fun Response.log() = apply {
if (logger.isEnabled) {
logger.log("$code ${request.url}")
}
}
} }

View File

@@ -14,8 +14,6 @@ import dagger.hilt.InstallIn
import dagger.hilt.android.EntryPointAccessors import dagger.hilt.android.EntryPointAccessors
import dagger.hilt.components.SingletonComponent import dagger.hilt.components.SingletonComponent
import org.koitharu.kotatsu.core.db.* import org.koitharu.kotatsu.core.db.*
import org.koitharu.kotatsu.core.logs.FileLogger
import org.koitharu.kotatsu.core.logs.SyncLogger
import java.util.concurrent.Callable import java.util.concurrent.Callable
abstract class SyncProvider : ContentProvider() { abstract class SyncProvider : ContentProvider() {
@@ -24,7 +22,6 @@ abstract class SyncProvider : ContentProvider() {
EntryPointAccessors.fromApplication(checkNotNull(context), SyncProviderEntryPoint::class.java) EntryPointAccessors.fromApplication(checkNotNull(context), SyncProviderEntryPoint::class.java)
} }
private val database by lazy { entryPoint.database } private val database by lazy { entryPoint.database }
private val logger by lazy { entryPoint.logger }
private val supportedTables = setOf( private val supportedTables = setOf(
TABLE_FAVOURITES, TABLE_FAVOURITES,
@@ -52,7 +49,6 @@ abstract class SyncProvider : ContentProvider() {
.selection(selection, selectionArgs) .selection(selection, selectionArgs)
.orderBy(sortOrder) .orderBy(sortOrder)
.create() .create()
logger.log("query: ${sqlQuery.sql} (${selectionArgs.contentToString()})")
return database.openHelper.readableDatabase.query(sqlQuery) return database.openHelper.readableDatabase.query(sqlQuery)
} }
@@ -65,7 +61,6 @@ abstract class SyncProvider : ContentProvider() {
if (values == null || table == null) { if (values == null || table == null) {
return null return null
} }
logger.log { "insert: $table [$values]" }
val db = database.openHelper.writableDatabase val db = database.openHelper.writableDatabase
if (db.insert(table, SQLiteDatabase.CONFLICT_IGNORE, values) < 0) { if (db.insert(table, SQLiteDatabase.CONFLICT_IGNORE, values) < 0) {
db.update(table, values) db.update(table, values)
@@ -75,7 +70,6 @@ abstract class SyncProvider : ContentProvider() {
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int { override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int {
val table = getTableName(uri) ?: return 0 val table = getTableName(uri) ?: return 0
logger.log { "delete: $table ($selection) : (${selectionArgs.contentToString()})" }
return database.openHelper.writableDatabase.delete(table, selection, selectionArgs) return database.openHelper.writableDatabase.delete(table, selection, selectionArgs)
} }
@@ -84,7 +78,6 @@ abstract class SyncProvider : ContentProvider() {
if (values == null || table == null) { if (values == null || table == null) {
return 0 return 0
} }
logger.log { "update: $table ($selection) : (${selectionArgs.contentToString()}) [$values]" }
return database.openHelper.writableDatabase return database.openHelper.writableDatabase
.update(table, SQLiteDatabase.CONFLICT_IGNORE, values, selection, selectionArgs) .update(table, SQLiteDatabase.CONFLICT_IGNORE, values, selection, selectionArgs)
} }
@@ -127,8 +120,5 @@ abstract class SyncProvider : ContentProvider() {
interface SyncProviderEntryPoint { interface SyncProviderEntryPoint {
val database: MangaDatabase val database: MangaDatabase
@get:SyncLogger
val logger: FileLogger
} }
} }

View File

@@ -28,6 +28,10 @@ class FavouritesSyncAdapter(context: Context) : AbstractThreadedSyncAdapter(cont
runCatchingCancellable { runCatchingCancellable {
syncHelper.syncFavourites(syncResult) syncHelper.syncFavourites(syncResult)
SyncController.setLastSync(context, account, authority, System.currentTimeMillis()) SyncController.setLastSync(context, account, authority, System.currentTimeMillis())
}.onFailure(syncResult::onError) }.onFailure { e ->
syncResult.onError(e)
syncHelper.onError(e)
}
syncHelper.onSyncComplete(syncResult)
} }
} }

View File

@@ -28,6 +28,10 @@ class HistorySyncAdapter(context: Context) : AbstractThreadedSyncAdapter(context
runCatchingCancellable { runCatchingCancellable {
syncHelper.syncHistory(syncResult) syncHelper.syncHistory(syncResult)
SyncController.setLastSync(context, account, authority, System.currentTimeMillis()) SyncController.setLastSync(context, account, authority, System.currentTimeMillis())
}.onFailure(syncResult::onError) }.onFailure { e ->
syncResult.onError(e)
syncHelper.onError(e)
}
syncHelper.onSyncComplete(syncResult)
} }
} }

View File

@@ -2,6 +2,7 @@ package org.koitharu.kotatsu.utils
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import android.widget.Toast
import androidx.core.app.ShareCompat import androidx.core.app.ShareCompat
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.BuildConfig
@@ -84,6 +85,7 @@ class ShareHelper(private val context: Context) {
fun shareLogs(loggers: Collection<FileLogger>) { fun shareLogs(loggers: Collection<FileLogger>) {
val intentBuilder = ShareCompat.IntentBuilder(context) val intentBuilder = ShareCompat.IntentBuilder(context)
.setType(TYPE_TEXT) .setType(TYPE_TEXT)
var hasLogs = false
for (logger in loggers) { for (logger in loggers) {
val logFile = logger.file val logFile = logger.file
if (!logFile.exists()) { if (!logFile.exists()) {
@@ -91,8 +93,13 @@ class ShareHelper(private val context: Context) {
} }
val uri = FileProvider.getUriForFile(context, "${BuildConfig.APPLICATION_ID}.files", logFile) val uri = FileProvider.getUriForFile(context, "${BuildConfig.APPLICATION_ID}.files", logFile)
intentBuilder.addStream(uri) intentBuilder.addStream(uri)
hasLogs = true
}
if (hasLogs) {
intentBuilder.setChooserTitle(R.string.share_logs)
intentBuilder.startChooser()
} else {
Toast.makeText(context, R.string.nothing_here, Toast.LENGTH_SHORT).show()
} }
intentBuilder.setChooserTitle(R.string.share_logs)
intentBuilder.startChooser()
} }
} }

View File

@@ -19,6 +19,7 @@ import android.provider.Settings
import android.view.View import android.view.View
import android.view.ViewPropertyAnimator import android.view.ViewPropertyAnimator
import android.view.Window import android.view.Window
import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.ActivityResultLauncher
import androidx.annotation.IntegerRes import androidx.annotation.IntegerRes
import androidx.core.app.ActivityOptionsCompat import androidx.core.app.ActivityOptionsCompat
@@ -170,3 +171,18 @@ fun Context.findActivity(): Activity? = when (this) {
is ContextWrapper -> baseContext.findActivity() is ContextWrapper -> baseContext.findActivity()
else -> null else -> null
} }
inline fun Activity.catchingWebViewUnavailability(block: () -> Unit): Boolean {
return try {
block()
true
} catch (e: Exception) {
if (e.isWebViewUnavailable()) {
Toast.makeText(this, R.string.web_view_unavailable, Toast.LENGTH_LONG).show()
finishAfterTransition()
false
} else {
throw e
}
}
}

View File

@@ -2,6 +2,7 @@ package org.koitharu.kotatsu.utils.ext
import android.content.ActivityNotFoundException import android.content.ActivityNotFoundException
import android.content.res.Resources import android.content.res.Resources
import android.util.AndroidRuntimeException
import androidx.collection.arraySetOf import androidx.collection.arraySetOf
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import okio.FileNotFoundException import okio.FileNotFoundException
@@ -94,3 +95,8 @@ inline fun <R> runCatchingCancellable(block: () -> R): Result<R> {
Result.failure(e) Result.failure(e)
} }
} }
fun Throwable.isWebViewUnavailable(): Boolean {
return (this is AndroidRuntimeException && message?.contains("WebView") == true) ||
cause?.isWebViewUnavailable() == true
}

View File

@@ -54,6 +54,11 @@ var RecyclerView.firstVisibleItemPosition: Int
} }
} }
val RecyclerView.visibleItemCount: Int
get() = (layoutManager as? LinearLayoutManager)?.run {
findLastVisibleItemPosition() - findFirstVisibleItemPosition()
} ?: 0
fun View.hasGlobalPoint(x: Int, y: Int): Boolean { fun View.hasGlobalPoint(x: Int, y: Int): Boolean {
if (visibility != View.VISIBLE) { if (visibility != View.VISIBLE) {
return false return false

View File

@@ -410,4 +410,6 @@
<string name="downloads_paused">Спампоўкі прыпыненыя</string> <string name="downloads_paused">Спампоўкі прыпыненыя</string>
<string name="downloads_removed">Спампоўкі выдалены</string> <string name="downloads_removed">Спампоўкі выдалены</string>
<string name="downloads_cancelled">Спампоўкі былі адменены</string> <string name="downloads_cancelled">Спампоўкі былі адменены</string>
<string name="translations">Пераклады</string>
<string name="web_view_unavailable">WebView недаступны: праверце, ці ўсталяваны пастаўшчык WebView</string>
</resources> </resources>

View File

@@ -68,7 +68,7 @@
<string name="zoom_mode_keep_start">Am Anfang ausrichten</string> <string name="zoom_mode_keep_start">Am Anfang ausrichten</string>
<string name="zoom_mode_fit_width">An Breite anpassen</string> <string name="zoom_mode_fit_width">An Breite anpassen</string>
<string name="zoom_mode_fit_height">An Höhe anpassen</string> <string name="zoom_mode_fit_height">An Höhe anpassen</string>
<string name="black_dark_theme_summary">Spart Energie bei AMOLED-Bildschirme</string> <string name="black_dark_theme_summary">Spart Energie auf AMOLED-Bildschirmen</string>
<string name="black_dark_theme">Reines Schwarz</string> <string name="black_dark_theme">Reines Schwarz</string>
<string name="right_to_left">Von rechts nach links</string> <string name="right_to_left">Von rechts nach links</string>
<string name="create_category">Neue Kategorie</string> <string name="create_category">Neue Kategorie</string>
@@ -91,17 +91,17 @@
<string name="confirm">Bestätigen</string> <string name="confirm">Bestätigen</string>
<string name="protect_application_subtitle">Gib ein Passwort ein, mit dem die App gestartet werden soll</string> <string name="protect_application_subtitle">Gib ein Passwort ein, mit dem die App gestartet werden soll</string>
<string name="next">Weiter</string> <string name="next">Weiter</string>
<string name="text_clear_search_history_prompt">Möchtest du wirklich alle letzten Suchanfragen entfernen\?</string> <string name="text_clear_search_history_prompt">Möchtest du alle letzten Suchanfragen unwiderruflich entfernen\?</string>
<string name="read_more">Mehr erfahren</string> <string name="read_more">Mehr erfahren</string>
<string name="tracker_warning">Einige Geräte haben ein anderes Systemverhalten, welches womöglich Hintergrundprozesse unterbricht.</string> <string name="tracker_warning">Einige Geräte haben ein anderes Systemverhalten, welches womöglich Hintergrundprozesse unterbricht.</string>
<string name="backup_saved">Sicherung erfolgreich gespeichert</string> <string name="backup_saved">Backup gespeichert</string>
<string name="welcome">Willkommen</string> <string name="welcome">Willkommen</string>
<string name="remove_category">Entfernen</string> <string name="remove_category">Entfernen</string>
<string name="rotate_screen">Bildschirm drehen</string> <string name="rotate_screen">Bildschirm drehen</string>
<string name="size_s">Größe: %s</string> <string name="size_s">Größe: %s</string>
<string name="new_version_s">Neue Version: %s</string> <string name="new_version_s">Neue Version: %s</string>
<string name="search_results">Suchergebnisse</string> <string name="search_results">Suchergebnisse</string>
<string name="text_feed_holder">Hier siehst du die neuen Kapitel des Mangas, den du gerade liest</string> <string name="text_feed_holder">Hier siehst du neue Kapitel der Mangas, die du liest</string>
<string name="read_later">Später lesen</string> <string name="read_later">Später lesen</string>
<string name="favourites_category_empty">Diese Kategorie ist leer</string> <string name="favourites_category_empty">Diese Kategorie ist leer</string>
<string name="all_favourites">Alle Favoriten</string> <string name="all_favourites">Alle Favoriten</string>
@@ -115,7 +115,7 @@
<string name="text_local_holder_secondary">Speicher Manga aus Online-Quellen oder importiere Dateien.</string> <string name="text_local_holder_secondary">Speicher Manga aus Online-Quellen oder importiere Dateien.</string>
<string name="text_local_holder_primary">Speichere erst etwas</string> <string name="text_local_holder_primary">Speichere erst etwas</string>
<string name="text_history_holder_secondary">Was du lesen kannst, findest du im Seitenmenü.</string> <string name="text_history_holder_secondary">Was du lesen kannst, findest du im Seitenmenü.</string>
<string name="text_search_holder_secondary">Versuche, die Abfrage umzuformulieren.</string> <string name="text_search_holder_secondary">Versuche, die Anfrage umzuformulieren.</string>
<string name="favourites_categories">Favoriten-Kategorien</string> <string name="favourites_categories">Favoriten-Kategorien</string>
<string name="vibration">Vibration</string> <string name="vibration">Vibration</string>
<string name="light_indicator">LED-Anzeige</string> <string name="light_indicator">LED-Anzeige</string>
@@ -130,7 +130,7 @@
<string name="open_in_browser">Im Browser öffnen</string> <string name="open_in_browser">Im Browser öffnen</string>
<string name="external_storage">Externer Speicher</string> <string name="external_storage">Externer Speicher</string>
<string name="internal_storage">Interner Speicher</string> <string name="internal_storage">Interner Speicher</string>
<string name="search_history_cleared">Suchverlauf gelöscht</string> <string name="search_history_cleared">Gelöscht</string>
<string name="clear_search_history">Suchverlauf löschen</string> <string name="clear_search_history">Suchverlauf löschen</string>
<string name="clear_thumbs_cache">Miniaturansichten Cache löschen</string> <string name="clear_thumbs_cache">Miniaturansichten Cache löschen</string>
<string name="error">Fehler</string> <string name="error">Fehler</string>
@@ -158,14 +158,14 @@
<string name="clear">Löschen</string> <string name="clear">Löschen</string>
<string name="taps_on_edges">Rand antippen</string> <string name="taps_on_edges">Rand antippen</string>
<string name="captcha_solve">Lösen</string> <string name="captcha_solve">Lösen</string>
<string name="captcha_required">CAPTCHA ist erforderlich</string> <string name="captcha_required">CAPTCHA erforderlich</string>
<string name="silent">Stumm</string> <string name="silent">Stumm</string>
<string name="reader_mode_hint">Die gewählte Konfiguration wird für diesen Manga gespeichert</string> <string name="reader_mode_hint">Die gewählte Konfiguration wird für diesen Manga gespeichert</string>
<string name="tap_to_try_again">Tippe, um es erneut zu versuchen</string> <string name="tap_to_try_again">Tippe, um es erneut zu versuchen</string>
<string name="today">Heute</string> <string name="today">Heute</string>
<string name="long_ago">Vor langer Zeit</string> <string name="long_ago">Vor langer Zeit</string>
<string name="yesterday">Gestern</string> <string name="yesterday">Gestern</string>
<string name="backup_information">Du kannst eine Sicherung deines Verlaufs und deiner Favoriten erstellen und diese wiederherstellen</string> <string name="backup_information">Du kannst ein Backup deines Verlaufs und deiner Favoriten erstellen und wiederherstellen</string>
<string name="data_restored_with_errors">Die Daten wurden wiederhergestellt, aber es gibt Fehler</string> <string name="data_restored_with_errors">Die Daten wurden wiederhergestellt, aber es gibt Fehler</string>
<string name="data_restored_success">Alle Daten wiederhergestellt</string> <string name="data_restored_success">Alle Daten wiederhergestellt</string>
<string name="zoom_mode_fit_center">An Zentrum anpassen</string> <string name="zoom_mode_fit_center">An Zentrum anpassen</string>
@@ -181,11 +181,11 @@
<string name="dont_check">Nicht prüfen</string> <string name="dont_check">Nicht prüfen</string>
<string name="domain">Domäne</string> <string name="domain">Domäne</string>
<string name="gestures_only">Nur Gesten</string> <string name="gestures_only">Nur Gesten</string>
<string name="chapter_is_missing">Kapitel fehlt</string> <string name="chapter_is_missing">Das Kapitel fehlt</string>
<string name="chapter_is_missing_text">Lade dieses fehlende Kapitel herunter oder lese es online.</string> <string name="chapter_is_missing_text">Lade dieses fehlende Kapitel herunter oder lese es online.</string>
<string name="queued">In Warteschlange</string> <string name="queued">In Warteschlange</string>
<string name="about_app_translation">Übersetzung</string> <string name="about_app_translation">Übersetzung</string>
<string name="about_app_translation_summary">Übersetze diese App</string> <string name="about_app_translation_summary">Übersetze die App</string>
<string name="text_clear_cookies_prompt">Du wirst von allen Quellen abgemeldet</string> <string name="text_clear_cookies_prompt">Du wirst von allen Quellen abgemeldet</string>
<string name="genres">Genres</string> <string name="genres">Genres</string>
<string name="auth_not_supported_by">Anmeldung bei %s wird nicht unterstützt</string> <string name="auth_not_supported_by">Anmeldung bei %s wird nicht unterstützt</string>
@@ -316,7 +316,7 @@
<string name="reorder">Neu anordnen</string> <string name="reorder">Neu anordnen</string>
<string name="empty">Leer</string> <string name="empty">Leer</string>
<string name="explore">Erkunden</string> <string name="explore">Erkunden</string>
<string name="confirm_exit">Drücke zum Verlassen erneut auf Zurück</string> <string name="confirm_exit">Drücke zum Beenden erneut Zurück</string>
<string name="exit_confirmation_summary">Drücke zweimal Zurück, um die App zu beenden</string> <string name="exit_confirmation_summary">Drücke zweimal Zurück, um die App zu beenden</string>
<string name="feed">Feed</string> <string name="feed">Feed</string>
<string name="incognito_mode">Inkognito-Modus</string> <string name="incognito_mode">Inkognito-Modus</string>
@@ -398,16 +398,18 @@
<string name="remove_completed">Entfernung abgeschlossen</string> <string name="remove_completed">Entfernung abgeschlossen</string>
<string name="cancel_all">Alle abbrechen</string> <string name="cancel_all">Alle abbrechen</string>
<string name="downloads_wifi_only">Nur über Wi-Fi herunterladen</string> <string name="downloads_wifi_only">Nur über Wi-Fi herunterladen</string>
<string name="downloads_wifi_only_summary">Beenden des Herunterladens beim Wechsel zu einem Mobilfunknetz</string> <string name="downloads_wifi_only_summary">Beende das Herunterladen beim Wechsel zu einem Mobilfunknetz</string>
<string name="suggestions_notifications_summary">Zeige ab und zu Benachrichtigungen mit Manga-Vorschlägen</string> <string name="suggestions_notifications_summary">Zeige ab und zu Benachrichtigungen mit Manga-Vorschlägen</string>
<string name="downloads_removed">Downloads wurden entfernt</string> <string name="downloads_removed">Downloads wurden entfernt</string>
<string name="suggestion_manga">Vorschlag: %s</string> <string name="suggestion_manga">Vorschlag: %s</string>
<string name="more">Mehr</string> <string name="more">Mehr</string>
<string name="cancel_all_downloads_confirm">Alle Downloads werden abgebrochen, teilweise heruntergeladene Dateien gehen verloren</string> <string name="cancel_all_downloads_confirm">Alle Downloads werden abgebrochen, teilweise heruntergeladene Dateien gehen verloren</string>
<string name="remove_completed_downloads_confirm">Dein Downloads-Verlauf wird unwiderruflich gelöscht sein</string> <string name="remove_completed_downloads_confirm">Dein Downloads-Verlauf wird unwiderruflich gelöscht</string>
<string name="text_downloads_list_holder">Du hast keine Downloads</string> <string name="text_downloads_list_holder">Du hast keine Downloads</string>
<string name="downloads_resumed">Downloads wurden fortgesetzt</string> <string name="downloads_resumed">Downloads wurden fortgesetzt</string>
<string name="downloads_paused">Downloads wurden pausiert</string> <string name="downloads_paused">Downloads wurden pausiert</string>
<string name="downloads_cancelled">Downloads wurden abgebrochen</string> <string name="downloads_cancelled">Downloads wurden abgebrochen</string>
<string name="suggestions_enable_prompt">Willst du personalisierte Manga-Vorschläge erhalten\?</string> <string name="suggestions_enable_prompt">Willst du personalisierte Manga-Vorschläge erhalten\?</string>
<string name="translations">Übersetzungen</string>
<string name="web_view_unavailable">WebView nicht verfügbar: überprüfe, ob WebView installiert ist</string>
</resources> </resources>

View File

@@ -410,4 +410,6 @@
<string name="downloads_removed">Las descargas se han eliminado</string> <string name="downloads_removed">Las descargas se han eliminado</string>
<string name="downloads_cancelled">Las descargas se han cancelado</string> <string name="downloads_cancelled">Las descargas se han cancelado</string>
<string name="suggestions_enable_prompt">¿Quieres recibir sugerencias sobre mangas personalizadas\?</string> <string name="suggestions_enable_prompt">¿Quieres recibir sugerencias sobre mangas personalizadas\?</string>
<string name="translations">Traducciones</string>
<string name="web_view_unavailable">WebView no está disponible: comprueba si el proveedor de WebView está instalado</string>
</resources> </resources>

View File

@@ -263,7 +263,7 @@
<string name="tracking">Pelacakan</string> <string name="tracking">Pelacakan</string>
<string name="logout">Keluar</string> <string name="logout">Keluar</string>
<string name="sync">Sinkronisasi</string> <string name="sync">Sinkronisasi</string>
<string name="send">Terkirim</string> <string name="send">Kirim</string>
<string name="status_reading">Dibaca</string> <string name="status_reading">Dibaca</string>
<string name="status_on_hold">Ditunda</string> <string name="status_on_hold">Ditunda</string>
<string name="invalid_domain_message">Domain tidak valid</string> <string name="invalid_domain_message">Domain tidak valid</string>

View File

@@ -4,4 +4,11 @@
<string name="details">ଵିଵରଣୀ</string> <string name="details">ଵିଵରଣୀ</string>
<string name="settings">ସେଟିଂ</string> <string name="settings">ସେଟିଂ</string>
<string name="suggestion_manga">ପରାମର୍ଶ: %s</string> <string name="suggestion_manga">ପରାମର୍ଶ: %s</string>
<string name="done">ହେଲା</string>
<string name="services">ସେଵା</string>
<string name="got_it">ବୁଝିଗଲି</string>
<string name="black_dark_theme">କଳା</string>
<string name="theme">ଥିମ୍</string>
<string name="speed">ଵେଗ</string>
<string name="download_started">ଡାଉନଲୋଡ୍ ଆରମ୍ଭ ହେଲା</string>
</resources> </resources>

View File

@@ -410,4 +410,6 @@
<string name="suggestions_notifications_summary">Иногда показывать уведомления с рекомендуемой мангой</string> <string name="suggestions_notifications_summary">Иногда показывать уведомления с рекомендуемой мангой</string>
<string name="remove_completed_downloads_confirm">Ваша история загрузок будет удалена</string> <string name="remove_completed_downloads_confirm">Ваша история загрузок будет удалена</string>
<string name="suggestions_enable_prompt">Хотите ли Вы получать персонализированные рекомендации манги\?</string> <string name="suggestions_enable_prompt">Хотите ли Вы получать персонализированные рекомендации манги\?</string>
<string name="translations">Переводы</string>
<string name="web_view_unavailable">WebView недоступен: проверьте, установлен ли провайдер WebView</string>
</resources> </resources>

View File

@@ -410,4 +410,6 @@
<string name="downloads_removed">Завантаження видалено</string> <string name="downloads_removed">Завантаження видалено</string>
<string name="downloads_cancelled">Завантаження скасовано</string> <string name="downloads_cancelled">Завантаження скасовано</string>
<string name="suggestions_enable_prompt">Хочете отримувати персоналізовані пропозиції щодо манги\?</string> <string name="suggestions_enable_prompt">Хочете отримувати персоналізовані пропозиції щодо манги\?</string>
<string name="web_view_unavailable">WebView недоступний: перевірте, чи встановлено провайдер WebView</string>
<string name="translations">Переклади</string>
</resources> </resources>

View File

@@ -415,4 +415,5 @@
<string name="downloads_cancelled">Downloads have been cancelled</string> <string name="downloads_cancelled">Downloads have been cancelled</string>
<string name="suggestions_enable_prompt">Do you want to receive personalized manga suggestions?</string> <string name="suggestions_enable_prompt">Do you want to receive personalized manga suggestions?</string>
<string name="translations">Translations</string> <string name="translations">Translations</string>
<string name="web_view_unavailable">WebView not available: check if WebView provider is installed</string>
</resources> </resources>