Compare commits

...

21 Commits

Author SHA1 Message Date
Koitharu
14f5d5daa4 Update parsers 2022-08-01 17:14:35 +03:00
Koitharu
f342cd6b56 Fix crash on widgets update 2022-08-01 17:00:00 +03:00
Koitharu
8faacab53a Fix github url 2022-07-30 16:10:03 +03:00
Koitharu
659c327a6d Update parsers and version 2022-07-30 16:05:01 +03:00
Koitharu
bcc2f531c3 Ability to resume download after IOException 2022-07-30 16:02:13 +03:00
Koitharu
020df5c1f7 Fix saving pages from cbz 2022-07-30 14:15:12 +03:00
Koitharu
d6781e1d14 Yet another attempt to make webtoon reader great again 2022-07-29 15:39:04 +03:00
Zakhar Timoshenko
d42cd59880 Fix enabling disabled new sources
Co-authored-by: Koitharu <8948226+nv95@users.noreply.github.com>
2022-07-28 09:49:30 +03:00
Koitharu
be19c32fea Update prasers 2022-07-27 17:36:39 +03:00
Koitharu
8da0e98d23 Fix FragmentManager leak 2022-07-27 17:36:39 +03:00
Koitharu
73a2f05509 Fix FadingSnackbar text color 2022-07-27 17:36:39 +03:00
Koitharu
bb23f998e0 Fix crash on description selection 2022-07-27 17:36:35 +03:00
TheDawnOvO
75915ff366 Update activity_main.xml 2022-07-27 15:24:39 +03:00
Dpper
517e801580 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (323 of 323 strings)

Co-authored-by: Dpper <ruslan20020401@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/uk/
Translation: Kotatsu/Strings
2022-07-27 08:47:39 +03:00
J. Lavoie
12474e23f9 Translated using Weblate (Finnish)
Currently translated at 96.2% (311 of 323 strings)

Translated using Weblate (French)

Currently translated at 100.0% (323 of 323 strings)

Translated using Weblate (Italian)

Currently translated at 99.0% (320 of 323 strings)

Translated using Weblate (German)

Currently translated at 97.8% (316 of 323 strings)

Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/de/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fi/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fr/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/it/
Translation: Kotatsu/Strings
2022-07-27 08:47:39 +03:00
kuragehime
00bdd859a7 Translated using Weblate (Japanese)
Currently translated at 100.0% (323 of 323 strings)

Co-authored-by: kuragehime <kuragehime641@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ja/
Translation: Kotatsu/Strings
2022-07-27 08:47:39 +03:00
Oğuz Ersen
3a3af9ea00 Translated using Weblate (Turkish)
Currently translated at 100.0% (323 of 323 strings)

Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/tr/
Translation: Kotatsu/Strings
2022-07-27 08:47:39 +03:00
Koitharu
1803b1a2ee Remove unused WebViewClientCompat 2022-07-25 11:25:43 +03:00
Koitharu
4175c84363 Move create category button in bs to toolbar 2022-07-22 19:04:43 +03:00
Koitharu
1840d7b50e Fix get current page #165 2022-07-20 20:32:01 +03:00
Zakhar Timoshenko
37b69833b3 Update parsers 2022-07-20 19:52:56 +03:00
50 changed files with 387 additions and 259 deletions

View File

@@ -14,8 +14,8 @@ android {
applicationId 'org.koitharu.kotatsu'
minSdkVersion 21
targetSdkVersion 32
versionCode 417
versionName '3.4.5'
versionCode 420
versionName '3.4.8'
generatedDensities = []
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
@@ -79,7 +79,7 @@ afterEvaluate {
}
}
dependencies {
implementation('com.github.nv95:kotatsu-parsers:c4abb758f3') {
implementation('com.github.KotatsuApp:kotatsu-parsers:85bfe42ddf') {
exclude group: 'org.json', module: 'json'
}

View File

@@ -73,7 +73,7 @@ class KotatsuApp : Application() {
appWidgetModule,
suggestionsModule,
shikimoriModule,
bookmarksModule,
bookmarksModule
)
}
}
@@ -91,8 +91,7 @@ class KotatsuApp : Application() {
ReportField.PHONE_MODEL,
ReportField.CRASH_CONFIGURATION,
ReportField.STACK_TRACE,
ReportField.CUSTOM_DATA,
ReportField.SHARED_PREFERENCES,
ReportField.SHARED_PREFERENCES
)
dialog {
text = getString(R.string.crash_text)

View File

@@ -0,0 +1,29 @@
package org.koitharu.kotatsu.base.ui.widgets
import android.content.Context
import android.text.Selection
import android.text.Spannable
import android.util.AttributeSet
import android.view.MotionEvent
import androidx.annotation.AttrRes
import com.google.android.material.textview.MaterialTextView
class SelectableTextView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
@AttrRes defStyleAttr: Int = android.R.attr.textViewStyle,
) : MaterialTextView(context, attrs, defStyleAttr) {
override fun dispatchTouchEvent(event: MotionEvent?): Boolean {
fixSelectionRange()
return super.dispatchTouchEvent(event)
}
// https://stackoverflow.com/questions/22810147/error-when-selecting-text-from-textview-java-lang-indexoutofboundsexception-se
private fun fixSelectionRange() {
if (selectionStart < 0 || selectionEnd < 0) {
val spannableText = text as? Spannable ?: return
Selection.setSelection(spannableText, text.length)
}
}
}

View File

@@ -2,10 +2,9 @@ package org.koitharu.kotatsu.browser
import android.graphics.Bitmap
import android.webkit.WebView
import org.koin.core.component.KoinComponent
import org.koitharu.kotatsu.core.network.WebViewClientCompat
import android.webkit.WebViewClient
class BrowserClient(private val callback: BrowserCallback) : WebViewClientCompat(), KoinComponent {
class BrowserClient(private val callback: BrowserCallback) : WebViewClient() {
override fun onPageFinished(webView: WebView, url: String) {
super.onPageFinished(webView, url)

View File

@@ -2,13 +2,14 @@ package org.koitharu.kotatsu.browser
import android.webkit.WebChromeClient
import android.webkit.WebView
import android.widget.ProgressBar
import androidx.core.view.isVisible
import com.google.android.material.progressindicator.BaseProgressIndicator
import org.koitharu.kotatsu.utils.ext.setProgressCompat
private const val PROGRESS_MAX = 100
class ProgressChromeClient(
private val progressIndicator: BaseProgressIndicator<*>,
private val progressIndicator: ProgressBar,
) : WebChromeClient() {
init {
@@ -24,7 +25,7 @@ class ProgressChromeClient(
progressIndicator.isIndeterminate = false
progressIndicator.setProgressCompat(newProgress.coerceAtMost(PROGRESS_MAX), true)
} else {
progressIndicator.setIndeterminate(true)
progressIndicator.isIndeterminate = true
}
}
}

View File

@@ -2,19 +2,19 @@ package org.koitharu.kotatsu.browser.cloudflare
import android.graphics.Bitmap
import android.webkit.WebView
import android.webkit.WebViewClient
import okhttp3.HttpUrl.Companion.toHttpUrl
import org.koitharu.kotatsu.core.network.AndroidCookieJar
import org.koitharu.kotatsu.core.network.WebViewClientCompat
private const val CF_CLEARANCE = "cf_clearance"
class CloudFlareClient(
private val cookieJar: AndroidCookieJar,
private val callback: CloudFlareCallback,
private val targetUrl: String
) : WebViewClientCompat() {
private val targetUrl: String,
) : WebViewClient() {
private val oldClearance = getCookieValue(CF_CLEARANCE)
private val oldClearance = getClearance()
override fun onPageStarted(view: WebView, url: String?, favicon: Bitmap?) {
super.onPageStarted(view, url, favicon)
@@ -32,14 +32,14 @@ class CloudFlareClient(
}
private fun checkClearance() {
val clearance = getCookieValue(CF_CLEARANCE)
val clearance = getClearance()
if (clearance != null && clearance != oldClearance) {
callback.onCheckPassed()
}
}
private fun getCookieValue(name: String): String? {
private fun getClearance(): String? {
return cookieJar.loadForRequest(targetUrl.toHttpUrl())
.find { it.name == name }?.value
.find { it.name == CF_CLEARANCE }?.value
}
}

View File

@@ -1,86 +0,0 @@
package org.koitharu.kotatsu.core.network
import android.annotation.TargetApi
import android.os.Build
import android.webkit.*
@Suppress("OverridingDeprecatedMember")
abstract class WebViewClientCompat : WebViewClient() {
open fun shouldOverrideUrlCompat(view: WebView, url: String): Boolean {
return false
}
open fun shouldInterceptRequestCompat(view: WebView, url: String): WebResourceResponse? {
return null
}
open fun onReceivedErrorCompat(
view: WebView,
errorCode: Int,
description: String?,
failingUrl: String,
isMainFrame: Boolean
) {
}
@TargetApi(Build.VERSION_CODES.N)
final override fun shouldOverrideUrlLoading(
view: WebView,
request: WebResourceRequest
): Boolean = shouldOverrideUrlCompat(view, request.url.toString())
final override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean {
return shouldOverrideUrlCompat(view, url)
}
final override fun shouldInterceptRequest(
view: WebView,
request: WebResourceRequest
): WebResourceResponse? = shouldInterceptRequestCompat(view, request.url.toString())
final override fun shouldInterceptRequest(
view: WebView,
url: String
): WebResourceResponse? = shouldInterceptRequestCompat(view, url)
@TargetApi(Build.VERSION_CODES.M)
final override fun onReceivedError(
view: WebView,
request: WebResourceRequest,
error: WebResourceError
) {
onReceivedErrorCompat(
view,
error.errorCode,
error.description?.toString(),
request.url.toString(),
request.isForMainFrame
)
}
final override fun onReceivedError(
view: WebView,
errorCode: Int,
description: String?,
failingUrl: String
) {
onReceivedErrorCompat(view, errorCode, description, failingUrl, failingUrl == view.url)
}
@TargetApi(Build.VERSION_CODES.M)
final override fun onReceivedHttpError(
view: WebView,
request: WebResourceRequest,
error: WebResourceResponse
) {
onReceivedErrorCompat(
view,
error.statusCode,
error.reasonPhrase,
request.url
.toString(),
request.isForMainFrame
)
}
}

View File

@@ -103,7 +103,8 @@ class DetailsActivity :
private fun onMangaRemoved(manga: Manga) {
Toast.makeText(
this, getString(R.string._s_deleted_from_local_storage, manga.title),
this,
getString(R.string._s_deleted_from_local_storage, manga.title),
Toast.LENGTH_SHORT
).show()
finishAfterTransition()

View File

@@ -1,11 +1,11 @@
package org.koitharu.kotatsu.download.domain
import android.content.Context
import android.net.ConnectivityManager
import android.webkit.MimeTypeMap
import coil.ImageLoader
import coil.request.ImageRequest
import coil.size.Scale
import java.io.File
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.sync.Semaphore
@@ -16,6 +16,7 @@ import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.network.CommonHeaders
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.download.ui.service.PausingHandle
import org.koitharu.kotatsu.local.data.PagesCache
import org.koitharu.kotatsu.local.domain.CbzMangaOutput
import org.koitharu.kotatsu.local.domain.LocalMangaRepository
@@ -25,11 +26,9 @@ import org.koitharu.kotatsu.parsers.util.await
import org.koitharu.kotatsu.utils.ext.deleteAwait
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
import org.koitharu.kotatsu.utils.ext.referer
import org.koitharu.kotatsu.utils.ext.waitForNetwork
import org.koitharu.kotatsu.utils.progress.ProgressJob
import java.io.File
import org.koitharu.kotatsu.utils.progress.PausingProgressJob
private const val MAX_DOWNLOAD_ATTEMPTS = 3
private const val MAX_FAILSAFE_ATTEMPTS = 2
private const val DOWNLOAD_ERROR_DELAY = 500L
private const val SLOWDOWN_DELAY = 200L
@@ -43,9 +42,6 @@ class DownloadManager(
private val settings: AppSettings,
) {
private val connectivityManager = context.getSystemService(
Context.CONNECTIVITY_SERVICE
) as ConnectivityManager
private val coverWidth = context.resources.getDimensionPixelSize(
androidx.core.R.dimen.compat_notification_large_icon_max_width
)
@@ -58,21 +54,24 @@ class DownloadManager(
manga: Manga,
chaptersIds: LongArray?,
startId: Int,
): ProgressJob<DownloadState> {
): PausingProgressJob<DownloadState> {
val stateFlow = MutableStateFlow<DownloadState>(
DownloadState.Queued(startId = startId, manga = manga, cover = null)
)
val job = downloadMangaImpl(manga, chaptersIds?.takeUnless { it.isEmpty() }, stateFlow, startId)
return ProgressJob(job, stateFlow)
val pausingHandle = PausingHandle()
val job = downloadMangaImpl(manga, chaptersIds?.takeUnless { it.isEmpty() }, stateFlow, pausingHandle, startId)
return PausingProgressJob(job, stateFlow, pausingHandle)
}
private fun downloadMangaImpl(
manga: Manga,
chaptersIds: LongArray?,
outState: MutableStateFlow<DownloadState>,
pausingHandle: PausingHandle,
startId: Int,
): Job = coroutineScope.launch(Dispatchers.Default + errorStateHandler(outState)) {
@Suppress("NAME_SHADOWING") var manga = manga
@Suppress("NAME_SHADOWING")
var manga = manga
val chaptersIdsSet = chaptersIds?.toMutableSet()
val cover = loadCover(manga)
outState.value = DownloadState.Queued(startId, manga, cover)
@@ -108,38 +107,28 @@ class DownloadManager(
"${chaptersIdsSet?.size} of ${chaptersIds?.size} requested chapters not found in manga"
}
for ((chapterIndex, chapter) in chapters.withIndex()) {
val pages = repo.getPages(chapter)
val pages = runFailsafe(outState, pausingHandle) {
repo.getPages(chapter)
}
for ((pageIndex, page) in pages.withIndex()) {
var retryCounter = 0
failsafe@ while (true) {
try {
val url = repo.getPageUrl(page)
val file = cache[url] ?: downloadFile(url, page.referer, destination, tempFileName)
output.addPage(
chapter = chapter,
file = file,
pageNumber = pageIndex,
ext = MimeTypeMap.getFileExtensionFromUrl(url),
)
break@failsafe
} catch (e: IOException) {
if (retryCounter < MAX_DOWNLOAD_ATTEMPTS) {
outState.value = DownloadState.WaitingForNetwork(startId, data, cover)
delay(DOWNLOAD_ERROR_DELAY)
connectivityManager.waitForNetwork()
retryCounter++
} else {
throw e
}
}
runFailsafe(outState, pausingHandle) {
val url = repo.getPageUrl(page)
val file = cache[url] ?: downloadFile(url, page.referer, destination, tempFileName)
output.addPage(
chapter = chapter,
file = file,
pageNumber = pageIndex,
ext = MimeTypeMap.getFileExtensionFromUrl(url)
)
}
outState.value = DownloadState.Progress(
startId, data, cover,
startId = startId,
manga = data,
cover = cover,
totalChapters = chapters.size,
currentChapter = chapterIndex,
totalPages = pages.size,
currentPage = pageIndex,
currentPage = pageIndex
)
if (settings.isDownloadsSlowdownEnabled) {
@@ -157,15 +146,40 @@ class DownloadManager(
throw e
} catch (e: Throwable) {
e.printStackTraceDebug()
outState.value = DownloadState.Error(startId, manga, cover, e)
outState.value = DownloadState.Error(startId, manga, cover, e, false)
} finally {
withContext(NonCancellable) {
output?.cleanup()
File(destination, tempFileName).deleteAwait()
coroutineContext[WakeLockNode]?.release()
semaphore.release()
localMangaRepository.unlockManga(manga.id)
}
}
}
private suspend fun <R> runFailsafe(
outState: MutableStateFlow<DownloadState>,
pausingHandle: PausingHandle,
block: suspend () -> R,
): R {
var countDown = MAX_FAILSAFE_ATTEMPTS
failsafe@ while (true) {
try {
return block()
} catch (e: IOException) {
if (countDown <= 0) {
val state = outState.value
outState.value = DownloadState.Error(state.startId, state.manga, state.cover, e, true)
countDown = MAX_FAILSAFE_ATTEMPTS
pausingHandle.pause()
pausingHandle.awaitResumed()
outState.value = state
} else {
countDown--
delay(DOWNLOAD_ERROR_DELAY)
}
}
coroutineContext[WakeLockNode]?.release()
semaphore.release()
localMangaRepository.unlockManga(manga.id)
}
}
@@ -195,6 +209,7 @@ class DownloadManager(
manga = prevValue.manga,
cover = prevValue.cover,
error = throwable,
canRetry = false
)
}
@@ -225,7 +240,7 @@ class DownloadManager(
okHttp = okHttp,
cache = cache,
localMangaRepository = localMangaRepository,
settings = settings,
settings = settings
)
}
}

View File

@@ -108,6 +108,7 @@ sealed interface DownloadState {
}
}
@Deprecated("TODO: remove")
class WaitingForNetwork(
override val startId: Int,
override val manga: Manga,
@@ -170,6 +171,7 @@ sealed interface DownloadState {
override val manga: Manga,
override val cover: Drawable?,
val error: Throwable,
val canRetry: Boolean,
) : DownloadState {
override fun equals(other: Any?): Boolean {
@@ -182,6 +184,7 @@ sealed interface DownloadState {
if (manga != other.manga) return false
if (cover != other.cover) return false
if (error != other.error) return false
if (canRetry != other.canRetry) return false
return true
}
@@ -191,6 +194,7 @@ sealed interface DownloadState {
result = 31 * result + manga.hashCode()
result = 31 * result + (cover?.hashCode() ?: 0)
result = 31 * result + error.hashCode()
result = 31 * result + canRetry.hashCode()
return result
}
}

View File

@@ -29,16 +29,26 @@ class DownloadNotification(private val context: Context, startId: Int) {
context.getString(android.R.string.cancel),
PendingIntent.getBroadcast(
context,
startId,
startId * 2,
DownloadService.getCancelIntent(startId),
PendingIntent.FLAG_CANCEL_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE
)
)
private val retryAction = NotificationCompat.Action(
R.drawable.ic_restart_black,
context.getString(R.string.try_again),
PendingIntent.getBroadcast(
context,
startId * 2 + 1,
DownloadService.getResumeIntent(startId),
PendingIntent.FLAG_CANCEL_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE
)
)
private val listIntent = PendingIntent.getActivity(
context,
REQUEST_LIST,
DownloadsActivity.newIntent(context),
PendingIntentCompat.FLAG_IMMUTABLE,
PendingIntentCompat.FLAG_IMMUTABLE
)
init {
@@ -89,10 +99,14 @@ class DownloadNotification(private val context: Context, startId: Int) {
builder.setSmallIcon(android.R.drawable.stat_notify_error)
builder.setSubText(context.getString(R.string.error))
builder.setContentText(message)
builder.setAutoCancel(true)
builder.setOngoing(false)
builder.setAutoCancel(!state.canRetry)
builder.setOngoing(state.canRetry)
builder.setCategory(NotificationCompat.CATEGORY_ERROR)
builder.setStyle(NotificationCompat.BigTextStyle().bigText(message))
if (state.canRetry) {
builder.addAction(cancelAction)
builder.addAction(retryAction)
}
}
is DownloadState.PostProcessing -> {
builder.setProgress(1, 0, true)

View File

@@ -11,6 +11,7 @@ import android.widget.Toast
import androidx.core.content.ContextCompat
import androidx.lifecycle.lifecycleScope
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import java.util.concurrent.TimeUnit
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import kotlinx.coroutines.plus
@@ -28,16 +29,16 @@ import org.koitharu.kotatsu.download.domain.WakeLockNode
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.utils.ext.connectivityManager
import org.koitharu.kotatsu.utils.ext.throttle
import org.koitharu.kotatsu.utils.progress.PausingProgressJob
import org.koitharu.kotatsu.utils.progress.ProgressJob
import org.koitharu.kotatsu.utils.progress.TimeLeftEstimator
import java.util.concurrent.TimeUnit
class DownloadService : BaseService() {
private lateinit var downloadManager: DownloadManager
private lateinit var notificationSwitcher: ForegroundNotificationSwitcher
private val jobs = LinkedHashMap<Int, ProgressJob<DownloadState>>()
private val jobs = LinkedHashMap<Int, PausingProgressJob<DownloadState>>()
private val jobCount = MutableStateFlow(0)
private val controlReceiver = ControlReceiver()
private var binder: DownloadBinder? = null
@@ -49,10 +50,13 @@ class DownloadService : BaseService() {
val wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager)
.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "kotatsu:downloading")
downloadManager = get<DownloadManager.Factory>().create(
coroutineScope = lifecycleScope + WakeLockNode(wakeLock, TimeUnit.HOURS.toMillis(1)),
coroutineScope = lifecycleScope + WakeLockNode(wakeLock, TimeUnit.HOURS.toMillis(1))
)
DownloadNotification.createChannel(this)
registerReceiver(controlReceiver, IntentFilter(ACTION_DOWNLOAD_CANCEL))
val intentFilter = IntentFilter()
intentFilter.addAction(ACTION_DOWNLOAD_CANCEL)
intentFilter.addAction(ACTION_DOWNLOAD_RESUME)
registerReceiver(controlReceiver, intentFilter)
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
@@ -90,7 +94,7 @@ class DownloadService : BaseService() {
startId: Int,
manga: Manga,
chaptersIds: LongArray?,
): ProgressJob<DownloadState> {
): PausingProgressJob<DownloadState> {
val job = downloadManager.downloadManga(manga, chaptersIds, startId)
listenJob(job)
return job
@@ -144,7 +148,7 @@ class DownloadService : BaseService() {
}
private val DownloadState.isTerminal: Boolean
get() = this is DownloadState.Done || this is DownloadState.Error || this is DownloadState.Cancelled
get() = this is DownloadState.Done || this is DownloadState.Cancelled || (this is DownloadState.Error && !canRetry)
inner class ControlReceiver : BroadcastReceiver() {
@@ -155,6 +159,10 @@ class DownloadService : BaseService() {
jobs.remove(cancelId)?.cancel()
jobCount.value = jobs.size
}
ACTION_DOWNLOAD_RESUME -> {
val cancelId = intent.getIntExtra(EXTRA_CANCEL_ID, 0)
jobs[cancelId]?.resume()
}
}
}
}
@@ -173,6 +181,7 @@ class DownloadService : BaseService() {
const val ACTION_DOWNLOAD_COMPLETE = "${BuildConfig.APPLICATION_ID}.action.ACTION_DOWNLOAD_COMPLETE"
private const val ACTION_DOWNLOAD_CANCEL = "${BuildConfig.APPLICATION_ID}.action.ACTION_DOWNLOAD_CANCEL"
private const val ACTION_DOWNLOAD_RESUME = "${BuildConfig.APPLICATION_ID}.action.ACTION_DOWNLOAD_RESUME"
private const val EXTRA_MANGA = "manga"
private const val EXTRA_CHAPTERS_IDS = "chapters_ids"
@@ -219,6 +228,9 @@ class DownloadService : BaseService() {
fun getCancelIntent(startId: Int) = Intent(ACTION_DOWNLOAD_CANCEL)
.putExtra(EXTRA_CANCEL_ID, startId)
fun getResumeIntent(startId: Int) = Intent(ACTION_DOWNLOAD_RESUME)
.putExtra(EXTRA_CANCEL_ID, startId)
fun getDownloadedManga(intent: Intent?): Manga? {
if (intent?.action == ACTION_DOWNLOAD_COMPLETE) {
return intent.getParcelableExtra<ParcelableManga>(EXTRA_MANGA)?.manga

View File

@@ -0,0 +1,30 @@
package org.koitharu.kotatsu.download.ui.service
import androidx.annotation.AnyThread
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first
class PausingHandle {
private val paused = MutableStateFlow(false)
@get:AnyThread
val isPaused: Boolean
get() = paused.value
@AnyThread
suspend fun awaitResumed() {
paused.filter { !it }.first()
}
@AnyThread
fun pause() {
paused.value = true
}
@AnyThread
fun resume() {
paused.value = false
}
}

View File

@@ -2,9 +2,11 @@ package org.koitharu.kotatsu.favourites.ui.categories.select
import android.os.Bundle
import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.appcompat.widget.Toolbar
import androidx.fragment.app.FragmentManager
import org.koin.androidx.viewmodel.ext.android.viewModel
import org.koin.core.parameter.parametersOf
@@ -26,7 +28,8 @@ class FavouriteCategoriesBottomSheet :
BaseBottomSheet<DialogFavoriteCategoriesBinding>(),
OnListItemClickListener<MangaCategoryItem>,
CategoriesEditDelegate.CategoriesEditCallback,
View.OnClickListener {
View.OnClickListener,
Toolbar.OnMenuItemClickListener {
private val viewModel by viewModel<MangaCategoriesViewModel> {
parametersOf(requireNotNull(arguments?.getParcelableArrayList<ParcelableManga>(KEY_MANGA_LIST)).map { it.manga })
@@ -44,7 +47,7 @@ class FavouriteCategoriesBottomSheet :
adapter = MangaCategoriesAdapter(this)
binding.recyclerViewCategories.adapter = adapter
binding.buttonDone.setOnClickListener(this)
binding.itemCreate.setOnClickListener(this)
binding.toolbar.setOnMenuItemClickListener(this)
viewModel.content.observe(viewLifecycleOwner, this::onContentChanged)
viewModel.onError.observe(viewLifecycleOwner, ::onError)
@@ -57,11 +60,18 @@ class FavouriteCategoriesBottomSheet :
override fun onClick(v: View) {
when (v.id) {
R.id.item_create -> startActivity(FavouritesCategoryEditActivity.newIntent(requireContext()))
R.id.button_done -> dismiss()
}
}
override fun onMenuItemClick(item: MenuItem): Boolean {
when (item.itemId) {
R.id.action_create -> startActivity(FavouritesCategoryEditActivity.newIntent(requireContext()))
else -> return false
}
return true
}
override fun onItemClick(item: MangaCategoryItem, view: View) {
viewModel.setChecked(item.id, !item.isChecked)
}

View File

@@ -68,7 +68,7 @@ abstract class MangaListFragment :
override fun onInflateView(
inflater: LayoutInflater,
container: ViewGroup?
container: ViewGroup?,
) = FragmentListBinding.inflate(inflater, container, false)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@@ -76,13 +76,13 @@ abstract class MangaListFragment :
listAdapter = MangaListAdapter(
coil = get(),
lifecycleOwner = viewLifecycleOwner,
listener = this,
listener = this
)
selectionController = ListSelectionController(
activity = requireActivity(),
decoration = MangaSelectionDecoration(view.context),
registryOwner = this,
callback = this,
callback = this
)
paginationListener = PaginationScrollListener(4, this)
with(binding.recyclerView) {
@@ -97,7 +97,7 @@ abstract class MangaListFragment :
setOnRefreshListener(this@MangaListFragment)
isEnabled = isSwipeRefreshEnabled
}
addMenuProvider(MangaListMenuProvider(childFragmentManager))
addMenuProvider(MangaListMenuProvider(this))
viewModel.listMode.observe(viewLifecycleOwner, ::onListModeChanged)
viewModel.gridScale.observe(viewLifecycleOwner, ::onGridScaleChanged)
@@ -171,21 +171,21 @@ abstract class MangaListFragment :
val headerHeight = (activity as? AppBarOwner)?.appBar?.measureHeight() ?: insets.top
binding.root.updatePadding(
left = insets.left,
right = insets.right,
right = insets.right
)
if (activity is MainActivity) {
binding.recyclerView.updatePadding(
top = headerHeight,
bottom = insets.bottom,
bottom = insets.bottom
)
binding.swipeRefreshLayout.setProgressViewOffset(
true,
headerHeight + resources.resolveDp(-72),
headerHeight + resources.resolveDp(10),
headerHeight + resources.resolveDp(10)
)
} else {
binding.recyclerView.updatePadding(
bottom = insets.bottom,
bottom = insets.bottom
)
}
}

View File

@@ -4,11 +4,11 @@ import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import androidx.core.view.MenuProvider
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.Fragment
import org.koitharu.kotatsu.R
class MangaListMenuProvider(
private val fragmentManager: FragmentManager,
private val fragment: Fragment,
) : MenuProvider {
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
@@ -17,7 +17,7 @@ class MangaListMenuProvider(
override fun onMenuItemSelected(menuItem: MenuItem): Boolean = when (menuItem.itemId) {
R.id.action_list_mode -> {
ListModeSelectDialog.show(fragmentManager)
ListModeSelectDialog.show(fragment.childFragmentManager)
true
}
else -> false

View File

@@ -14,7 +14,7 @@ val readerModule
factory { MangaDataRepository(get()) }
single { PagesCache(get()) }
factory { PageSaveHelper(get(), androidContext()) }
factory { PageSaveHelper(androidContext()) }
viewModel { params ->
ReaderViewModel(
@@ -25,7 +25,7 @@ val readerModule
historyRepository = get(),
settings = get(),
pageSaveHelper = get(),
bookmarksRepository = get(),
bookmarksRepository = get()
)
}
}

View File

@@ -4,6 +4,10 @@ import android.content.Context
import android.net.Uri
import android.webkit.MimeTypeMap
import androidx.activity.result.ActivityResultLauncher
import androidx.core.net.toUri
import java.io.File
import kotlin.coroutines.Continuation
import kotlin.coroutines.resume
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runInterruptible
import kotlinx.coroutines.suspendCancellableCoroutine
@@ -11,19 +15,14 @@ import kotlinx.coroutines.withContext
import okhttp3.HttpUrl.Companion.toHttpUrl
import okio.IOException
import org.koitharu.kotatsu.base.domain.MangaUtils
import org.koitharu.kotatsu.local.data.PagesCache
import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.parsers.util.toFileNameSafe
import org.koitharu.kotatsu.reader.domain.PageLoader
import java.io.File
import kotlin.coroutines.Continuation
import kotlin.coroutines.resume
private const val MAX_FILENAME_LENGTH = 10
private const val EXTENSION_FALLBACK = "png"
class PageSaveHelper(
private val cache: PagesCache,
context: Context,
) {
@@ -61,7 +60,11 @@ class PageSaveHelper(
} != null
private suspend fun getProposedFileName(url: String, file: File): String {
var name = url.toHttpUrl().pathSegments.last()
var name = if (url.startsWith("cbz://")) {
requireNotNull(url.toUri().fragment)
} else {
url.toHttpUrl().pathSegments.last()
}
var extension = name.substringAfterLast('.', "")
name = name.substringBeforeLast('.')
if (extension.length !in 2..4) {

View File

@@ -166,10 +166,9 @@ class ReaderActivity :
}
}
R.id.action_save_page -> {
viewModel.getCurrentPage()?.also { page ->
viewModel.saveCurrentState(readerManager.currentReader?.getCurrentState())
viewModel.saveCurrentPage(page, savePageRequest)
} ?: return false
viewModel.saveCurrentState(readerManager.currentReader?.getCurrentState())
val page = viewModel.getCurrentPage() ?: return false
viewModel.saveCurrentPage(page, savePageRequest)
}
R.id.action_bookmark -> {
if (viewModel.isBookmarkAdded.value == true) {

View File

@@ -7,6 +7,8 @@ import android.view.View
import androidx.core.view.isVisible
import com.davemorrissey.labs.subscaleview.ImageSource
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.asExecutor
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
import org.koitharu.kotatsu.core.model.ZoomMode
@@ -26,6 +28,8 @@ open class PageHolder(
View.OnClickListener {
init {
binding.ssiv.setExecutor(Dispatchers.Default.asExecutor())
binding.ssiv.setEagerLoadingEnabled(!isLowRamDevice(context))
binding.ssiv.setOnImageEventListener(delegate)
@Suppress("LeakingThis")
bindingInfo.buttonRetry.setOnClickListener(this)

View File

@@ -3,13 +3,16 @@ package org.koitharu.kotatsu.reader.ui.pager.webtoon
import android.content.Context
import android.util.AttributeSet
import android.widget.FrameLayout
import androidx.annotation.AttrRes
import org.koitharu.kotatsu.R
class WebtoonFrameLayout @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
context: Context,
attrs: AttributeSet? = null,
@AttrRes defStyleAttr: Int = 0,
) : FrameLayout(context, attrs, defStyleAttr) {
private val target by lazy {
private val target by lazy(LazyThreadSafetyMode.NONE) {
findViewById<WebtoonImageView>(R.id.ssiv)
}

View File

@@ -13,14 +13,14 @@ import org.koitharu.kotatsu.databinding.ItemPageWebtoonBinding
import org.koitharu.kotatsu.reader.domain.PageLoader
import org.koitharu.kotatsu.reader.ui.pager.BasePageHolder
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
import org.koitharu.kotatsu.utils.GoneOnInvisibleListener
import org.koitharu.kotatsu.utils.ext.*
class WebtoonHolder(
binding: ItemPageWebtoonBinding,
loader: PageLoader,
settings: AppSettings,
exceptionResolver: ExceptionResolver
exceptionResolver: ExceptionResolver,
) : BasePageHolder<ItemPageWebtoonBinding>(binding, loader, settings, exceptionResolver),
View.OnClickListener {
@@ -29,6 +29,7 @@ class WebtoonHolder(
init {
binding.ssiv.setOnImageEventListener(delegate)
bindingInfo.buttonRetry.setOnClickListener(this)
GoneOnInvisibleListener(bindingInfo.progressBar).attach()
}
override fun onBind(data: ReaderPage) {
@@ -61,9 +62,9 @@ class WebtoonHolder(
override fun onImageShowing(zoom: ZoomMode) {
with(binding.ssiv) {
maxScale = 2f * width / sWidth.toFloat()
setMinimumScaleType(SubsamplingScaleImageView.SCALE_TYPE_CUSTOM)
minScale = width / sWidth.toFloat()
maxScale = minScale
scrollTo(
when {
scrollToRestore != 0 -> scrollToRestore

View File

@@ -1,11 +1,15 @@
package org.koitharu.kotatsu.reader.ui.pager.webtoon
import android.app.Activity
import android.content.Context
import android.graphics.PointF
import android.util.AttributeSet
import androidx.recyclerview.widget.RecyclerView
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.asExecutor
import org.koitharu.kotatsu.parsers.util.toIntUp
import org.koitharu.kotatsu.utils.ext.isLowRamDevice
import org.koitharu.kotatsu.utils.ext.parents
private const val SCROLL_UNKNOWN = -1
@@ -15,15 +19,15 @@ class WebtoonImageView @JvmOverloads constructor(
) : SubsamplingScaleImageView(context, attr) {
private val ct = PointF()
private val displayHeight = if (context is Activity) {
context.window.decorView.height
} else {
context.resources.displayMetrics.heightPixels
}
private var scrollPos = 0
private var scrollRange = SCROLL_UNKNOWN
init {
setExecutor(Dispatchers.Default.asExecutor())
setEagerLoadingEnabled(!isLowRamDevice(context))
}
fun scrollBy(delta: Int) {
val maxScroll = getScrollRange()
if (maxScroll == 0) {
@@ -36,6 +40,7 @@ class WebtoonImageView @JvmOverloads constructor(
fun scrollTo(y: Int) {
val maxScroll = getScrollRange()
if (maxScroll == 0) {
resetScaleAndCenter()
return
}
scrollToInternal(y.coerceIn(0, maxScroll))
@@ -58,8 +63,11 @@ class WebtoonImageView @JvmOverloads constructor(
override fun getSuggestedMinimumHeight(): Int {
var desiredHeight = super.getSuggestedMinimumHeight()
if (sHeight == 0 && desiredHeight < displayHeight) {
desiredHeight = displayHeight
if (sHeight == 0) {
val parentHeight = parentHeight()
if (desiredHeight < parentHeight) {
desiredHeight = parentHeight
}
}
return desiredHeight
}
@@ -84,7 +92,7 @@ class WebtoonImageView @JvmOverloads constructor(
}
}
width = width.coerceAtLeast(suggestedMinimumWidth)
height = height.coerceIn(suggestedMinimumHeight, displayHeight)
height = height.coerceIn(suggestedMinimumHeight, parentHeight())
setMeasuredDimension(width, height)
}
@@ -101,4 +109,8 @@ class WebtoonImageView @JvmOverloads constructor(
val totalHeight = (sHeight * minScale).toIntUp()
scrollRange = (totalHeight - height).coerceAtLeast(0)
}
private fun parentHeight(): Int {
return parents.firstNotNullOfOrNull { it as? RecyclerView }?.height ?: 0
}
}

View File

@@ -6,6 +6,7 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import kotlin.math.sign
@Suppress("unused")
class WebtoonLayoutManager : LinearLayoutManager {
private var scrollDirection: Int = 0

View File

@@ -23,7 +23,7 @@ class WebtoonReaderFragment : BaseReader<FragmentReaderWebtoonBinding>() {
override fun onInflateView(
inflater: LayoutInflater,
container: ViewGroup?
container: ViewGroup?,
) = FragmentReaderWebtoonBinding.inflate(inflater, container, false)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {

View File

@@ -3,6 +3,7 @@ package org.koitharu.kotatsu.reader.ui.thumbnails.adapter
import android.graphics.drawable.Drawable
import coil.ImageLoader
import coil.request.ImageRequest
import coil.size.Scale
import coil.size.Size
import com.google.android.material.R as materialR
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
@@ -24,7 +25,6 @@ fun pageThumbnailAD(
) = adapterDelegateViewBinding<PageThumbnail, PageThumbnail, ItemPageThumbBinding>(
{ inflater, parent -> ItemPageThumbBinding.inflate(inflater, parent, false) }
) {
var job: Job? = null
val gridWidth = itemView.context.resources.getDimensionPixelSize(R.dimen.preferred_grid_width)
val thumbSize = Size(
@@ -39,6 +39,7 @@ fun pageThumbnailAD(
.data(url)
.referer(item.page.referer)
.size(thumbSize)
.scale(Scale.FILL)
.allowRgb565(true)
.build()
).drawable

View File

@@ -33,15 +33,22 @@ class NewSourcesViewModel(
private fun buildList() {
val locales = LocaleListCompat.getDefault().mapToSet { it.language }
val hidden = settings.hiddenSources
val pendingHidden = HashSet<String>()
sources.value = initialList.map {
val locale = it.locale
val isEnabledByLocale = locale == null || locale in locales
if (!isEnabledByLocale) {
pendingHidden += it.name
}
SourceConfigItem.SourceItem(
source = it,
summary = it.getLocaleTitle(),
isEnabled = it.name !in hidden && (locale == null || locale in locales),
isDraggable = false,
isEnabled = isEnabledByLocale,
isDraggable = false
)
}
if (pendingHidden.isNotEmpty()) {
settings.hiddenSources += pendingHidden
}
}
}

View File

@@ -0,0 +1,23 @@
package org.koitharu.kotatsu.utils
import android.view.View
import android.view.ViewTreeObserver
/**
* ProgressIndicator become INVISIBLE instead of GONE by hide() call.
* It`s final so we need this workaround
*/
class GoneOnInvisibleListener(
private val view: View,
) : ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
if (view.visibility == View.INVISIBLE) {
view.visibility = View.GONE
}
}
fun attach() {
view.viewTreeObserver.addOnGlobalLayoutListener(this)
}
}

View File

@@ -1,19 +1,17 @@
package org.koitharu.kotatsu.utils.ext
import android.app.ActivityManager
import android.content.Context
import android.content.Context.ACTIVITY_SERVICE
import android.content.SharedPreferences
import android.content.pm.ResolveInfo
import android.net.ConnectivityManager
import android.net.Network
import android.net.NetworkRequest
import android.net.Uri
import android.os.Build
import androidx.activity.result.ActivityResultLauncher
import androidx.core.app.ActivityOptionsCompat
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.coroutineScope
import androidx.work.CoroutineWorker
import kotlin.coroutines.resume
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.channels.trySendBlocking
import kotlinx.coroutines.delay
@@ -22,32 +20,12 @@ import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
val Context.connectivityManager: ConnectivityManager
get() = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
suspend fun ConnectivityManager.waitForNetwork(): Network {
val request = NetworkRequest.Builder().build()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
// fast path
activeNetwork?.let { return it }
}
return suspendCancellableCoroutine { cont ->
val callback = object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
unregisterNetworkCallback(this)
if (cont.isActive) {
cont.resume(network)
}
}
}
registerNetworkCallback(request, callback)
cont.invokeOnCancellation {
unregisterNetworkCallback(callback)
}
}
}
val Context.activityManager: ActivityManager?
get() = getSystemService(ACTIVITY_SERVICE) as? ActivityManager
fun String.toUriOrNull() = if (isEmpty()) null else Uri.parse(this)
@@ -92,4 +70,8 @@ fun Lifecycle.postDelayed(runnable: Runnable, delay: Long) {
delay(delay)
runnable.run()
}
}
fun isLowRamDevice(context: Context): Boolean {
return context.activityManager?.isLowRamDevice ?: false
}

View File

@@ -4,6 +4,7 @@ import android.app.Activity
import android.graphics.Rect
import android.view.View
import android.view.ViewGroup
import android.view.ViewParent
import android.view.inputmethod.InputMethodManager
import androidx.core.view.children
import androidx.recyclerview.widget.LinearLayoutManager
@@ -138,4 +139,13 @@ fun <T : View> ViewGroup.findViewsByType(clazz: Class<T>): Sequence<T> {
}
}
}
}
}
val View.parents: Sequence<ViewParent>
get() = sequence {
var p: ViewParent? = parent
while (p != null) {
yield(p)
p = p.parent
}
}

View File

@@ -0,0 +1,26 @@
package org.koitharu.kotatsu.utils.progress
import androidx.annotation.AnyThread
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.StateFlow
import org.koitharu.kotatsu.download.ui.service.PausingHandle
class PausingProgressJob<P>(
job: Job,
progress: StateFlow<P>,
private val pausingHandle: PausingHandle,
) : ProgressJob<P>(job, progress) {
@get:AnyThread
val isPaused: Boolean
get() = pausingHandle.isPaused
@AnyThread
suspend fun awaitResumed() = pausingHandle.awaitResumed()
@AnyThread
fun pause() = pausingHandle.pause()
@AnyThread
fun resume() = pausingHandle.resume()
}

View File

@@ -4,7 +4,7 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
class ProgressJob<P>(
open class ProgressJob<P>(
private val job: Job,
private val progress: StateFlow<P>,
) : Job by job {

View File

@@ -15,12 +15,13 @@ import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.domain.MangaIntent
import org.koitharu.kotatsu.history.domain.HistoryRepository
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.util.replaceWith
import org.koitharu.kotatsu.utils.ext.requireBitmap
class RecentListFactory(
private val context: Context,
private val historyRepository: HistoryRepository,
private val coil: ImageLoader
private val coil: ImageLoader,
) : RemoteViewsService.RemoteViewsFactory {
private val dataSet = ArrayList<Manga>()
@@ -29,7 +30,7 @@ class RecentListFactory(
)
private val coverSize = Size(
context.resources.getDimensionPixelSize(R.dimen.widget_cover_width),
context.resources.getDimensionPixelSize(R.dimen.widget_cover_height),
context.resources.getDimensionPixelSize(R.dimen.widget_cover_height)
)
override fun onCreate() = Unit
@@ -39,9 +40,8 @@ class RecentListFactory(
override fun getItemId(position: Int) = dataSet[position].id
override fun onDataSetChanged() {
dataSet.clear()
val data = runBlocking { historyRepository.getList(0, 10) }
dataSet.addAll(data)
dataSet.replaceWith(data)
}
override fun hasStableIds() = true

View File

@@ -16,6 +16,7 @@ import org.koitharu.kotatsu.base.domain.MangaIntent
import org.koitharu.kotatsu.core.prefs.AppWidgetConfig
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.util.replaceWith
import org.koitharu.kotatsu.utils.ext.requireBitmap
class ShelfListFactory(
@@ -32,7 +33,7 @@ class ShelfListFactory(
)
private val coverSize = Size(
context.resources.getDimensionPixelSize(R.dimen.widget_cover_width),
context.resources.getDimensionPixelSize(R.dimen.widget_cover_height),
context.resources.getDimensionPixelSize(R.dimen.widget_cover_height)
)
override fun onCreate() = Unit
@@ -42,7 +43,6 @@ class ShelfListFactory(
override fun getItemId(position: Int) = dataSet[position].id
override fun onDataSetChanged() {
dataSet.clear()
val data = runBlocking {
val category = config.categoryId
if (category == 0L) {
@@ -51,7 +51,7 @@ class ShelfListFactory(
favouritesRepository.getManga(category)
}
}
dataSet.addAll(data)
dataSet.replaceWith(data)
}
override fun hasStableIds() = true
@@ -85,4 +85,4 @@ class ShelfListFactory(
override fun getViewTypeCount() = 1
override fun onDestroy() = Unit
}
}

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000"
android:pathData="M3 16H10V14H3M18 14V10H16V14H12V16H16V20H18V16H22V14M14 6H3V8H14M14 10H3V12H14V10Z" />
</vector>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:width="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path android:fillColor="#000" android:pathData="M12,4C14.1,4 16.1,4.8 17.6,6.3C20.7,9.4 20.7,14.5 17.6,17.6C15.8,19.5 13.3,20.2 10.9,19.9L11.4,17.9C13.1,18.1 14.9,17.5 16.2,16.2C18.5,13.9 18.5,10.1 16.2,7.7C15.1,6.6 13.5,6 12,6V10.6L7,5.6L12,0.6V4M6.3,17.6C3.7,15 3.3,11 5.1,7.9L6.6,9.4C5.5,11.6 5.9,14.4 7.8,16.2C8.3,16.7 8.9,17.1 9.6,17.4L9,19.4C8,19 7.1,18.4 6.3,17.6Z" />
</vector>

View File

@@ -217,7 +217,7 @@
app:layout_constraintTop_toBottomOf="@id/recyclerView_bookmarks"
tools:visibility="visible" />
<TextView
<org.koitharu.kotatsu.base.ui.widgets.SelectableTextView
android:id="@+id/textView_description"
android:layout_width="0dp"
android:layout_height="wrap_content"

View File

@@ -9,7 +9,7 @@
<com.google.android.material.navigation.NavigationView
android:id="@+id/navigationView"
android:layout_width="260dp"
android:layout_width="230dp"
android:layout_height="match_parent"
android:fitsSystemWindows="false"
app:drawerLayoutCornerSize="0dp"
@@ -91,4 +91,4 @@
tools:visibility="visible" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</LinearLayout>
</LinearLayout>

View File

@@ -12,6 +12,7 @@
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:menu="@menu/opt_categories"
app:title="@string/add_to_favourites">
<Button
@@ -35,15 +36,4 @@
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/item_checkable_new" />
<TextView
android:id="@+id/item_create"
style="?listItemTextViewStyle"
android:layout_width="match_parent"
android:layout_height="?android:listPreferredItemHeightSmall"
android:background="?selectableItemBackground"
android:paddingStart="?android:listPreferredItemPaddingStart"
android:paddingEnd="?android:listPreferredItemPaddingEnd"
android:text="@string/create_category"
android:textAppearance="?attr/textAppearanceButton" />
</LinearLayout>

View File

@@ -40,8 +40,6 @@
android:maxLines="4"
android:padding="@dimen/margin_normal"
android:textAlignment="viewStart"
android:textAppearance="@style/TextAppearance.Design.Snackbar.Message"
android:textColor="@android:color/white"
tools:text="Look at all the wonderful snack bar text..." />
<Button
@@ -53,7 +51,6 @@
android:paddingStart="@dimen/margin_normal"
android:paddingEnd="@dimen/margin_normal"
android:visibility="gone"
tools:targetApi="o"
tools:text="Action"
tools:visibility="visible" />

View File

@@ -214,7 +214,7 @@
app:layout_constraintTop_toBottomOf="@id/recyclerView_bookmarks"
tools:visibility="visible" />
<TextView
<org.koitharu.kotatsu.base.ui.widgets.SelectableTextView
android:id="@+id/textView_description"
android:layout_width="0dp"
android:layout_height="wrap_content"

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<menu
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_create"
android:icon="@drawable/ic_list_create"
android:title="@string/create_category"
app:showAsAction="always" />
</menu>

View File

@@ -317,4 +317,5 @@
<string name="invalid_domain_message">Ungültige Domäne</string>
<string name="status_reading">Lesen</string>
<string name="select_range">Bereich auswählen</string>
<string name="not_found_404">Inhalt nicht gefunden oder entfernt</string>
</resources>

View File

@@ -309,4 +309,5 @@
<string name="data_deletion">Tietojen poistaminen</string>
<string name="show_all">Näytä kaikki</string>
<string name="select_range">Valitse alue</string>
<string name="not_found_404">Sisältöä ei löydy tai se on poistettu</string>
</resources>

View File

@@ -319,4 +319,5 @@
<string name="status_re_reading">Relecture</string>
<string name="invalid_domain_message">Domaine invalide</string>
<string name="select_range">Sélectionner une plage</string>
<string name="not_found_404">Contenu non trouvé ou supprimé</string>
</resources>

View File

@@ -319,4 +319,5 @@
<string name="status_dropped">Abbandonato</string>
<string name="invalid_domain_message">Dominio non valido</string>
<string name="select_range">Seleziona l\'intervallo</string>
<string name="not_found_404">Contenuto non trovato o rimosso</string>
</resources>

View File

@@ -318,4 +318,6 @@
<string name="exclude_nsfw_from_history_summary">NSFWとマークされたマンガは履歴に追加されず、進行状況も保存されない</string>
<string name="show_all">すべて表示</string>
<string name="invalid_domain_message">無効なドメイン</string>
<string name="select_range">範囲を選択</string>
<string name="not_found_404">コンテンツが見つからない、または削除された</string>
</resources>

View File

@@ -319,4 +319,5 @@
<string name="show_all">Tümünü göster</string>
<string name="invalid_domain_message">Geçersiz etki alanı</string>
<string name="select_range">Aralık seç</string>
<string name="not_found_404">İçerik bulunamadı veya kaldırıldı</string>
</resources>

View File

@@ -318,4 +318,6 @@
<string name="exclude_nsfw_from_history_summary">Манґа, позначена як NSFW, ніколи не буде додана до історії і ваш прогрес не буде збережений</string>
<string name="clear_cookies_summary">Може допомогти в разі виникнення проблем. Усі авторизації будуть анульовані</string>
<string name="show_all">Показати всі</string>
<string name="select_range">Виберіть діапазон</string>
<string name="not_found_404">Вміст не знайдено або видалено</string>
</resources>

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="url_github">https://github.com/nv95/Kotatsu</string>
<string name="url_github">https://github.com/KotatsuApp/Kotatsu</string>
<string name="url_discord">https://discord.gg/NNJ5RgVBC5</string>
<string name="url_forpda">https://4pda.to/forum/index.php?showtopic=697669</string>
<string name="url_twitter">https://twitter.com/kotatsuapp</string>