Compare commits

..

28 Commits
v3.2 ... v3.2.3

Author SHA1 Message Date
Koitharu
10c03ff01a Update version in github templates 2022-05-11 12:28:50 +03:00
Koitharu
e85b9db118 Show stub if favourite categories empty 2022-05-11 12:20:06 +03:00
Koitharu
f6b0a7c780 Update parsers 2022-05-11 12:11:37 +03:00
Zakhar Timoshenko
fa150e45ff [Issue template] Update version 2022-05-06 20:15:29 +03:00
Koitharu
de9c1017b3 Update parsers 2022-05-06 15:45:20 +03:00
Koitharu
2709d40fc0 Fix BottomSheet edge-to-edge mode 2022-05-06 13:51:55 +03:00
Koitharu
45b42ad5bd Revert "Fix bottom sheet navbar color"
This reverts commit fdd4f5abca.
2022-05-06 12:57:37 +03:00
Koitharu
b759f8d0a0 Fix pages filename #151 2022-05-05 16:46:44 +03:00
Koitharu
23e7aa2aaa Fix images scale type 2022-05-05 15:57:05 +03:00
Koitharu
fdd4f5abca Fix bottom sheet navbar color 2022-05-05 15:51:30 +03:00
Koitharu
c695468aec Fix cold launch 2022-05-05 15:43:07 +03:00
Koitharu
9166716f2a Update version 2022-05-05 15:21:10 +03:00
Zakhar Timoshenko
3407e74e99 Fix FavouriteCategoriesDialog toolbar in album orientation 2022-05-05 15:16:30 +03:00
Koitharu
4c5314fe59 Update parsers 2022-05-02 14:53:31 +03:00
Koitharu
96be49aa83 Update monochrome launcher icon 2022-05-02 09:39:01 +03:00
Koitharu
28b556121b Show new sources on app startup 2022-05-02 09:35:21 +03:00
Koitharu
558c19e526 Update parsers and version 2022-05-01 09:24:45 +03:00
Luiz-bro
59c2d20311 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (280 of 280 strings)

Co-authored-by: Luiz-bro <luiznneto1@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt_BR/
Translation: Kotatsu/Strings
2022-05-01 09:18:16 +03:00
Luna Jernberg
fa1f2cbf51 Translated using Weblate (Swedish)
Currently translated at 98.5% (275 of 279 strings)

Co-authored-by: Luna Jernberg <droidbittin@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/sv/
Translation: Kotatsu/Strings
2022-05-01 09:18:16 +03:00
J. Lavoie
de8739f143 Translated using Weblate (Finnish)
Currently translated at 99.6% (279 of 280 strings)

Translated using Weblate (French)

Currently translated at 100.0% (280 of 280 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (280 of 280 strings)

Translated using Weblate (German)

Currently translated at 100.0% (280 of 280 strings)

Translated using Weblate (Finnish)

Currently translated at 99.6% (278 of 279 strings)

Translated using Weblate (French)

Currently translated at 100.0% (279 of 279 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (279 of 279 strings)

Translated using Weblate (German)

Currently translated at 100.0% (279 of 279 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-05-01 09:18:16 +03:00
kuragehime
9aa28f6fd2 Translated using Weblate (Japanese)
Currently translated at 100.0% (280 of 280 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (279 of 279 strings)

Co-authored-by: kuragehime <kuragehime641@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ja/
Translation: Kotatsu/Strings
2022-05-01 09:18:16 +03:00
Oğuz Ersen
a2b1699047 Translated using Weblate (Turkish)
Currently translated at 100.0% (280 of 280 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (279 of 279 strings)

Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/tr/
Translation: Kotatsu/Strings
2022-05-01 09:18:16 +03:00
mondstern
2dce65a448 Translated using Weblate (German)
Currently translated at 98.9% (276 of 279 strings)

Co-authored-by: mondstern <mondstern@snopyta.org>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/de/
Translation: Kotatsu/Strings
2022-05-01 09:18:16 +03:00
Koitharu
3d68d7c818 Reuse PageLoader in PagesThumbnailsSheet 2022-04-29 12:45:28 +03:00
Koitharu
4987d43042 Fix pages saving #151 2022-04-29 11:41:14 +03:00
Koitharu
684b494edb Fix concurrent manga downloading #154 2022-04-29 10:07:21 +03:00
Koitharu
714b708fa9 Fix npe on getExternalFilesDirs #158 2022-04-29 09:04:41 +03:00
Koitharu
c462c19a8b Option to hide 'All categories' tab from favourites 2022-04-28 16:46:55 +03:00
73 changed files with 933 additions and 234 deletions

View File

@@ -44,7 +44,7 @@ body:
label: Kotatsu version label: Kotatsu version
description: You can find your Kotatsu version in **Settings → About**. description: You can find your Kotatsu version in **Settings → About**.
placeholder: | placeholder: |
Example: "3.2" Example: "3.2.3"
validations: validations:
required: true required: true
@@ -87,7 +87,7 @@ body:
required: true required: true
- label: If this is an issue with a source, I should be opening an issue in the [parsers repository](https://github.com/nv95/kotatsu-parsers/issues/new). - label: If this is an issue with a source, I should be opening an issue in the [parsers repository](https://github.com/nv95/kotatsu-parsers/issues/new).
required: true required: true
- label: I have updated the app to version **[3.2](https://github.com/nv95/Kotatsu/releases/latest)**. - label: I have updated the app to version **[3.2.3](https://github.com/nv95/Kotatsu/releases/latest)**.
required: true required: true
- label: I will fill out all of the requested information in this form. - label: I will fill out all of the requested information in this form.
required: true required: true

View File

@@ -33,7 +33,7 @@ body:
required: true required: true
- label: If this is an issue with a source, I should be opening an issue in the [parsers repository](https://github.com/nv95/kotatsu-parsers/issues/new). - label: If this is an issue with a source, I should be opening an issue in the [parsers repository](https://github.com/nv95/kotatsu-parsers/issues/new).
required: true required: true
- label: I have updated the app to version **[3.2](https://github.com/nv95/Kotatsu/releases/latest)**. - label: I have updated the app to version **[3.2.3](https://github.com/nv95/Kotatsu/releases/latest)**.
required: true required: true
- label: I will fill out all of the requested information in this form. - label: I will fill out all of the requested information in this form.
required: true required: true

1
.gitignore vendored
View File

@@ -10,6 +10,7 @@
/.idea/navEditor.xml /.idea/navEditor.xml
/.idea/assetWizardSettings.xml /.idea/assetWizardSettings.xml
/.idea/kotlinScripting.xml /.idea/kotlinScripting.xml
/.idea/deploymentTargetDropDown.xml
.DS_Store .DS_Store
/build /build
/captures /captures

View File

@@ -1,17 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="deploymentTargetDropDown">
<targetSelectedWithDropDown>
<Target>
<type value="QUICK_BOOT_TARGET" />
<deviceKey>
<Key>
<type value="VIRTUAL_DEVICE_PATH" />
<value value="$USER_HOME$/.android/avd/Pixel_API_S.avd" />
</Key>
</deviceKey>
</Target>
</targetSelectedWithDropDown>
<timeTargetWasSelectedWithDropDown value="2021-02-19T19:02:37.198775Z" />
</component>
</project>

View File

@@ -14,8 +14,8 @@ android {
applicationId 'org.koitharu.kotatsu' applicationId 'org.koitharu.kotatsu'
minSdkVersion 21 minSdkVersion 21
targetSdkVersion 32 targetSdkVersion 32
versionCode 404 versionCode 407
versionName '3.2' versionName '3.2.3'
generatedDensities = [] generatedDensities = []
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
@@ -65,7 +65,7 @@ android {
} }
dependencies { dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar']) implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar'])
implementation('com.github.nv95:kotatsu-parsers:72cd6fbadf') { implementation('com.github.nv95:kotatsu-parsers:05a93e2380') {
exclude group: 'org.json', module: 'json' exclude group: 'org.json', module: 'json'
} }
@@ -86,7 +86,7 @@ dependencies {
implementation 'androidx.viewpager2:viewpager2:1.1.0-beta01' implementation 'androidx.viewpager2:viewpager2:1.1.0-beta01'
implementation 'androidx.preference:preference-ktx:1.2.0' implementation 'androidx.preference:preference-ktx:1.2.0'
implementation 'androidx.work:work-runtime-ktx:2.7.1' implementation 'androidx.work:work-runtime-ktx:2.7.1'
implementation 'com.google.android.material:material:1.6.0-rc01' implementation 'com.google.android.material:material:1.6.0'
//noinspection LifecycleAnnotationProcessorWithJava8 //noinspection LifecycleAnnotationProcessorWithJava8
kapt 'androidx.lifecycle:lifecycle-compiler:2.4.1' kapt 'androidx.lifecycle:lifecycle-compiler:2.4.1'
@@ -105,7 +105,7 @@ dependencies {
implementation 'com.davemorrissey.labs:subsampling-scale-image-view-androidx:3.10.0' implementation 'com.davemorrissey.labs:subsampling-scale-image-view-androidx:3.10.0'
implementation 'com.github.solkin:disk-lru-cache:1.4' implementation 'com.github.solkin:disk-lru-cache:1.4'
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.8.1' debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.9.1'
testImplementation 'junit:junit:4.13.2' testImplementation 'junit:junit:4.13.2'
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.1' testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.1'

View File

@@ -15,6 +15,7 @@ import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.parsers.model.MangaPage import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.parsers.util.await import org.koitharu.kotatsu.parsers.util.await
import org.koitharu.kotatsu.parsers.util.medianOrNull import org.koitharu.kotatsu.parsers.util.medianOrNull
import java.io.File
import java.io.InputStream import java.io.InputStream
import java.util.zip.ZipFile import java.util.zip.ZipFile
@@ -59,6 +60,14 @@ object MangaUtils : KoinComponent {
} }
} }
suspend fun getImageMimeType(file: File): String? = runInterruptible(Dispatchers.IO) {
val options = BitmapFactory.Options().apply {
inJustDecodeBounds = true
}
BitmapFactory.decodeFile(file.path, options)?.recycle()
options.outMimeType
}
private fun getBitmapSize(input: InputStream?): Size { private fun getBitmapSize(input: InputStream?): Size {
val options = BitmapFactory.Options().apply { val options = BitmapFactory.Options().apply {
inJustDecodeBounds = true inJustDecodeBounds = true

View File

@@ -9,11 +9,12 @@ import android.view.ViewGroup.LayoutParams
import androidx.appcompat.app.AppCompatDialog import androidx.appcompat.app.AppCompatDialog
import androidx.core.view.updateLayoutParams import androidx.core.view.updateLayoutParams
import androidx.viewbinding.ViewBinding import androidx.viewbinding.ViewBinding
import com.google.android.material.R as materialR
import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.dialog.AppBottomSheetDialog
import com.google.android.material.R as materialR
abstract class BaseBottomSheet<B : ViewBinding> : BottomSheetDialogFragment() { abstract class BaseBottomSheet<B : ViewBinding> : BottomSheetDialogFragment() {
@@ -43,7 +44,9 @@ abstract class BaseBottomSheet<B : ViewBinding> : BottomSheetDialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return if (resources.getBoolean(R.bool.is_tablet)) { return if (resources.getBoolean(R.bool.is_tablet)) {
AppCompatDialog(context, R.style.Theme_Kotatsu_Dialog) AppCompatDialog(context, R.style.Theme_Kotatsu_Dialog)
} else super.onCreateDialog(savedInstanceState) } else {
AppBottomSheetDialog(requireContext(), theme)
}
} }
protected abstract fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): B protected abstract fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): B

View File

@@ -0,0 +1,29 @@
package org.koitharu.kotatsu.base.ui.dialog
import android.content.Context
import android.graphics.Color
import android.view.View
import com.google.android.material.bottomsheet.BottomSheetDialog
class AppBottomSheetDialog(context: Context, theme: Int) : BottomSheetDialog(context, theme) {
/**
* https://github.com/material-components/material-components-android/issues/2582
*/
@Suppress("DEPRECATION")
override fun onAttachedToWindow() {
val window = window
val initialSystemUiVisibility = window?.decorView?.systemUiVisibility ?: 0
super.onAttachedToWindow()
if (window != null) {
// If the navigation bar is translucent at all, the BottomSheet should be edge to edge
val drawEdgeToEdge = edgeToEdgeEnabled && Color.alpha(window.navigationBarColor) < 0xFF
if (drawEdgeToEdge) {
// Copied from super.onAttachedToWindow:
val edgeToEdgeFlags = View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
// Fix super-class's window flag bug by respecting the intial system UI visibility:
window.decorView.systemUiVisibility = edgeToEdgeFlags or initialSystemUiVisibility
}
}
}
}

View File

@@ -5,6 +5,7 @@ import android.os.Parcel
import android.os.Parcelable import android.os.Parcelable
import android.os.Parcelable.Creator import android.os.Parcelable.Creator
import android.util.AttributeSet import android.util.AttributeSet
import android.view.View
import android.widget.Checkable import android.widget.Checkable
import androidx.annotation.AttrRes import androidx.annotation.AttrRes
import androidx.appcompat.widget.AppCompatImageView import androidx.appcompat.widget.AppCompatImageView
@@ -61,6 +62,12 @@ class CheckableImageView @JvmOverloads constructor(
} }
} }
class ToggleOnClickListener : OnClickListener {
override fun onClick(view: View) {
(view as? Checkable)?.toggle()
}
}
fun interface OnCheckedChangeListener { fun interface OnCheckedChangeListener {
fun onCheckedChanged(view: CheckableImageView, isChecked: Boolean) fun onCheckedChanged(view: CheckableImageView, isChecked: Boolean)

View File

@@ -11,10 +11,6 @@ import androidx.collection.arraySetOf
import androidx.core.content.edit import androidx.core.content.edit
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.google.android.material.color.DynamicColors import com.google.android.material.color.DynamicColors
import java.io.File
import java.text.DateFormat
import java.text.SimpleDateFormat
import java.util.*
import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.channels.trySendBlocking import kotlinx.coroutines.channels.trySendBlocking
import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.callbackFlow
@@ -24,6 +20,10 @@ import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.utils.ext.getEnumValue import org.koitharu.kotatsu.utils.ext.getEnumValue
import org.koitharu.kotatsu.utils.ext.putEnumValue import org.koitharu.kotatsu.utils.ext.putEnumValue
import org.koitharu.kotatsu.utils.ext.toUriOrNull import org.koitharu.kotatsu.utils.ext.toUriOrNull
import java.io.File
import java.text.DateFormat
import java.text.SimpleDateFormat
import java.util.*
class AppSettings(context: Context) { class AppSettings(context: Context) {
@@ -67,6 +67,10 @@ class AppSettings(context: Context) {
get() = prefs.getBoolean(KEY_TRAFFIC_WARNING, true) get() = prefs.getBoolean(KEY_TRAFFIC_WARNING, true)
set(value) = prefs.edit { putBoolean(KEY_TRAFFIC_WARNING, value) } set(value) = prefs.edit { putBoolean(KEY_TRAFFIC_WARNING, value) }
var isAllFavouritesVisible: Boolean
get() = prefs.getBoolean(KEY_ALL_FAVOURITES_VISIBLE, true)
set(value) = prefs.edit { putBoolean(KEY_ALL_FAVOURITES_VISIBLE, value) }
val isUpdateCheckingEnabled: Boolean val isUpdateCheckingEnabled: Boolean
get() = prefs.getBoolean(KEY_APP_UPDATE_AUTO, true) get() = prefs.getBoolean(KEY_APP_UPDATE_AUTO, true)
@@ -130,6 +134,20 @@ class AppSettings(context: Context) {
val isSourcesSelected: Boolean val isSourcesSelected: Boolean
get() = KEY_SOURCES_HIDDEN in prefs get() = KEY_SOURCES_HIDDEN in prefs
val newSources: Set<MangaSource>
get() {
val known = sourcesOrder.toSet()
val hidden = hiddenSources
return remoteMangaSources
.filterNotTo(EnumSet.noneOf(MangaSource::class.java)) { x ->
x.name in known || x.name in hidden
}
}
fun markKnownSources(sources: Collection<MangaSource>) {
sourcesOrder = sourcesOrder + sources.map { it.name }
}
val isPagesNumbersEnabled: Boolean val isPagesNumbersEnabled: Boolean
get() = prefs.getBoolean(KEY_PAGES_NUMBERS, false) get() = prefs.getBoolean(KEY_PAGES_NUMBERS, false)
@@ -278,6 +296,7 @@ class AppSettings(context: Context) {
const val KEY_SEARCH_SINGLE_SOURCE = "search_single_source" const val KEY_SEARCH_SINGLE_SOURCE = "search_single_source"
const val KEY_DOWNLOADS_PARALLELISM = "downloads_parallelism" const val KEY_DOWNLOADS_PARALLELISM = "downloads_parallelism"
const val KEY_DOWNLOADS_SLOWDOWN = "downloads_slowdown" const val KEY_DOWNLOADS_SLOWDOWN = "downloads_slowdown"
const val KEY_ALL_FAVOURITES_VISIBLE = "all_favourites_visible"
// About // About
const val KEY_APP_UPDATE = "app_update" const val KEY_APP_UPDATE = "app_update"

View File

@@ -1,7 +1,6 @@
package org.koitharu.kotatsu.download.domain package org.koitharu.kotatsu.download.domain
import android.content.Context import android.content.Context
import android.graphics.drawable.Drawable
import android.net.ConnectivityManager import android.net.ConnectivityManager
import android.webkit.MimeTypeMap import android.webkit.MimeTypeMap
import coil.ImageLoader import coil.ImageLoader
@@ -75,10 +74,12 @@ class DownloadManager(
): Job = coroutineScope.launch(Dispatchers.Default + errorStateHandler(outState)) { ): Job = coroutineScope.launch(Dispatchers.Default + errorStateHandler(outState)) {
@Suppress("NAME_SHADOWING") var manga = manga @Suppress("NAME_SHADOWING") var manga = manga
val chaptersIdsSet = chaptersIds?.toMutableSet() val chaptersIdsSet = chaptersIds?.toMutableSet()
val cover = loadCover(manga)
outState.value = DownloadState.Queued(startId, manga, cover)
localMangaRepository.lockManga(manga.id)
semaphore.acquire() semaphore.acquire()
coroutineContext[WakeLockNode]?.acquire() coroutineContext[WakeLockNode]?.acquire()
outState.value = DownloadState.Preparing(startId, manga, null) outState.value = DownloadState.Preparing(startId, manga, null)
var cover: Drawable? = null
val destination = localMangaRepository.getOutputDir() val destination = localMangaRepository.getOutputDir()
checkNotNull(destination) { context.getString(R.string.cannot_find_available_storage) } checkNotNull(destination) { context.getString(R.string.cannot_find_available_storage) }
val tempFileName = "${manga.id}_$startId.tmp" val tempFileName = "${manga.id}_$startId.tmp"
@@ -88,16 +89,6 @@ class DownloadManager(
manga = localMangaRepository.getRemoteManga(manga) ?: error("Cannot obtain remote manga instance") manga = localMangaRepository.getRemoteManga(manga) ?: error("Cannot obtain remote manga instance")
} }
val repo = MangaRepository(manga.source) val repo = MangaRepository(manga.source)
cover = runCatching {
imageLoader.execute(
ImageRequest.Builder(context)
.data(manga.coverUrl)
.referer(manga.publicUrl)
.size(coverWidth, coverHeight)
.scale(Scale.FILL)
.build()
).drawable
}.getOrNull()
outState.value = DownloadState.Preparing(startId, manga, cover) outState.value = DownloadState.Preparing(startId, manga, cover)
val data = if (manga.chapters.isNullOrEmpty()) repo.getDetails(manga) else manga val data = if (manga.chapters.isNullOrEmpty()) repo.getDetails(manga) else manga
output = CbzMangaOutput.get(destination, data) output = CbzMangaOutput.get(destination, data)
@@ -176,6 +167,7 @@ class DownloadManager(
} }
coroutineContext[WakeLockNode]?.release() coroutineContext[WakeLockNode]?.release()
semaphore.release() semaphore.release()
localMangaRepository.unlockManga(manga.id)
} }
} }
@@ -208,6 +200,17 @@ class DownloadManager(
) )
} }
private suspend fun loadCover(manga: Manga) = runCatching {
imageLoader.execute(
ImageRequest.Builder(context)
.data(manga.coverUrl)
.referer(manga.publicUrl)
.size(coverWidth, coverHeight)
.scale(Scale.FILL)
.build()
).drawable
}.getOrNull()
class Factory( class Factory(
private val context: Context, private val context: Context,
private val imageLoader: ImageLoader, private val imageLoader: ImageLoader,

View File

@@ -15,7 +15,7 @@ val favouritesModule
viewModel { categoryId -> viewModel { categoryId ->
FavouritesListViewModel(categoryId.get(), get(), get(), get()) FavouritesListViewModel(categoryId.get(), get(), get(), get())
} }
viewModel { FavouritesCategoriesViewModel(get()) } viewModel { FavouritesCategoriesViewModel(get(), get()) }
viewModel { manga -> viewModel { manga ->
MangaCategoriesViewModel(manga.get(), get()) MangaCategoriesViewModel(manga.get(), get())
} }

View File

@@ -13,7 +13,7 @@ abstract class FavouriteCategoriesDao {
abstract fun observeAll(): Flow<List<FavouriteCategoryEntity>> abstract fun observeAll(): Flow<List<FavouriteCategoryEntity>>
@Query("SELECT * FROM favourite_categories WHERE category_id = :id") @Query("SELECT * FROM favourite_categories WHERE category_id = :id")
abstract fun observe(id: Long): Flow<FavouriteCategoryEntity> abstract fun observe(id: Long): Flow<FavouriteCategoryEntity?>
@Insert(onConflict = OnConflictStrategy.ABORT) @Insert(onConflict = OnConflictStrategy.ABORT)
abstract suspend fun insert(category: FavouriteCategoryEntity): Long abstract suspend fun insert(category: FavouriteCategoryEntity): Long

View File

@@ -1,10 +1,7 @@
package org.koitharu.kotatsu.favourites.domain package org.koitharu.kotatsu.favourites.domain
import androidx.room.withTransaction import androidx.room.withTransaction
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.*
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map
import org.koitharu.kotatsu.core.db.MangaDatabase import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.db.entity.* import org.koitharu.kotatsu.core.db.entity.*
import org.koitharu.kotatsu.core.model.FavouriteCategory import org.koitharu.kotatsu.core.model.FavouriteCategory
@@ -48,6 +45,11 @@ class FavouritesRepository(private val db: MangaDatabase) {
}.distinctUntilChanged() }.distinctUntilChanged()
} }
fun observeCategory(id: Long): Flow<FavouriteCategory?> {
return db.favouriteCategoriesDao.observe(id)
.map { it?.toFavouriteCategory() }
}
fun observeCategories(mangaId: Long): Flow<List<FavouriteCategory>> { fun observeCategories(mangaId: Long): Flow<List<FavouriteCategory>> {
return db.favouritesDao.observe(mangaId).map { entity -> return db.favouritesDao.observe(mangaId).map { entity ->
entity?.categories?.map { it.toFavouriteCategory() }.orEmpty() entity?.categories?.map { it.toFavouriteCategory() }.orEmpty()
@@ -64,7 +66,7 @@ class FavouritesRepository(private val db: MangaDatabase) {
createdAt = System.currentTimeMillis(), createdAt = System.currentTimeMillis(),
sortKey = db.favouriteCategoriesDao.getNextSortKey(), sortKey = db.favouriteCategoriesDao.getNextSortKey(),
categoryId = 0, categoryId = 0,
order = SortOrder.UPDATED.name, order = SortOrder.NEWEST.name,
) )
val id = db.favouriteCategoriesDao.insert(entity) val id = db.favouriteCategoriesDao.insert(entity)
return entity.toFavouriteCategory(id) return entity.toFavouriteCategory(id)
@@ -121,6 +123,7 @@ class FavouritesRepository(private val db: MangaDatabase) {
private fun observeOrder(categoryId: Long): Flow<SortOrder> { private fun observeOrder(categoryId: Long): Flow<SortOrder> {
return db.favouriteCategoriesDao.observe(categoryId) return db.favouriteCategoriesDao.observe(categoryId)
.filterNotNull()
.map { x -> SortOrder(x.order, SortOrder.NEWEST) } .map { x -> SortOrder(x.order, SortOrder.NEWEST) }
.distinctUntilChanged() .distinctUntilChanged()
} }

View File

@@ -6,6 +6,7 @@ import androidx.appcompat.view.ActionMode
import androidx.appcompat.widget.PopupMenu import androidx.appcompat.widget.PopupMenu
import androidx.core.graphics.Insets import androidx.core.graphics.Insets
import androidx.core.view.children import androidx.core.view.children
import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
@@ -18,27 +19,29 @@ import org.koitharu.kotatsu.base.ui.util.ActionModeListener
import org.koitharu.kotatsu.core.model.FavouriteCategory import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.core.ui.titleRes import org.koitharu.kotatsu.core.ui.titleRes
import org.koitharu.kotatsu.databinding.FragmentFavouritesBinding import org.koitharu.kotatsu.databinding.FragmentFavouritesBinding
import org.koitharu.kotatsu.databinding.ItemEmptyStateBinding
import org.koitharu.kotatsu.favourites.ui.categories.CategoriesActivity import org.koitharu.kotatsu.favourites.ui.categories.CategoriesActivity
import org.koitharu.kotatsu.favourites.ui.categories.CategoriesEditDelegate import org.koitharu.kotatsu.favourites.ui.categories.CategoriesEditDelegate
import org.koitharu.kotatsu.favourites.ui.categories.FavouritesCategoriesViewModel import org.koitharu.kotatsu.favourites.ui.categories.FavouritesCategoriesViewModel
import org.koitharu.kotatsu.favourites.ui.categories.adapter.CategoryListModel
import org.koitharu.kotatsu.main.ui.AppBarOwner import org.koitharu.kotatsu.main.ui.AppBarOwner
import org.koitharu.kotatsu.parsers.model.SortOrder
import org.koitharu.kotatsu.utils.ext.getDisplayMessage import org.koitharu.kotatsu.utils.ext.getDisplayMessage
import org.koitharu.kotatsu.utils.ext.measureHeight import org.koitharu.kotatsu.utils.ext.measureHeight
import org.koitharu.kotatsu.utils.ext.resolveDp import org.koitharu.kotatsu.utils.ext.resolveDp
import java.util.*
class FavouritesContainerFragment : class FavouritesContainerFragment :
BaseFragment<FragmentFavouritesBinding>(), BaseFragment<FragmentFavouritesBinding>(),
FavouritesTabLongClickListener, FavouritesTabLongClickListener,
CategoriesEditDelegate.CategoriesEditCallback, CategoriesEditDelegate.CategoriesEditCallback,
ActionModeListener { ActionModeListener,
View.OnClickListener {
private val viewModel by viewModel<FavouritesCategoriesViewModel>() private val viewModel by viewModel<FavouritesCategoriesViewModel>()
private val editDelegate by lazy(LazyThreadSafetyMode.NONE) { private val editDelegate by lazy(LazyThreadSafetyMode.NONE) {
CategoriesEditDelegate(requireContext(), this) CategoriesEditDelegate(requireContext(), this)
} }
private var pagerAdapter: FavouritesPagerAdapter? = null private var pagerAdapter: FavouritesPagerAdapter? = null
private var stubBinding: ItemEmptyStateBinding? = null
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@@ -53,20 +56,19 @@ class FavouritesContainerFragment :
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
val adapter = FavouritesPagerAdapter(this, this) val adapter = FavouritesPagerAdapter(this, this)
viewModel.categories.value?.let { viewModel.visibleCategories.value?.let(::onCategoriesChanged)
adapter.replaceData(wrapCategories(it))
}
binding.pager.adapter = adapter binding.pager.adapter = adapter
pagerAdapter = adapter pagerAdapter = adapter
TabLayoutMediator(binding.tabs, binding.pager, adapter).attach() TabLayoutMediator(binding.tabs, binding.pager, adapter).attach()
actionModeDelegate.addListener(this, viewLifecycleOwner) actionModeDelegate.addListener(this, viewLifecycleOwner)
viewModel.categories.observe(viewLifecycleOwner, ::onCategoriesChanged) viewModel.visibleCategories.observe(viewLifecycleOwner, ::onCategoriesChanged)
viewModel.onError.observe(viewLifecycleOwner, ::onError) viewModel.onError.observe(viewLifecycleOwner, ::onError)
} }
override fun onDestroyView() { override fun onDestroyView() {
pagerAdapter = null pagerAdapter = null
stubBinding = null
super.onDestroyView() super.onDestroyView()
} }
@@ -86,7 +88,8 @@ class FavouritesContainerFragment :
top = headerHeight - insets.top top = headerHeight - insets.top
) )
binding.pager.updatePadding( binding.pager.updatePadding(
top = -headerHeight + resources.resolveDp(8) // 8 dp is needed so that the top of the list is not attached to tabs (visible when ActionMode is active) // 8 dp is needed so that the top of the list is not attached to tabs (visible when ActionMode is active)
top = -headerHeight + resources.resolveDp(8)
) )
binding.tabs.apply { binding.tabs.apply {
updatePadding( updatePadding(
@@ -99,8 +102,17 @@ class FavouritesContainerFragment :
} }
} }
private fun onCategoriesChanged(categories: List<FavouriteCategory>) { private fun onCategoriesChanged(categories: List<CategoryListModel>) {
pagerAdapter?.replaceData(wrapCategories(categories)) pagerAdapter?.replaceData(categories)
if (categories.isEmpty()) {
binding.pager.isVisible = false
binding.tabs.isVisible = false
showStub()
} else {
binding.pager.isVisible = true
binding.tabs.isVisible = true
(stubBinding?.root ?: binding.stubEmptyState).isVisible = false
}
} }
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
@@ -122,29 +134,20 @@ class FavouritesContainerFragment :
Snackbar.make(binding.pager, e.getDisplayMessage(resources), Snackbar.LENGTH_LONG).show() Snackbar.make(binding.pager, e.getDisplayMessage(resources), Snackbar.LENGTH_LONG).show()
} }
override fun onTabLongClick(tabView: View, category: FavouriteCategory): Boolean { override fun onTabLongClick(tabView: View, item: CategoryListModel): Boolean {
val menuRes = if (category.id == 0L) R.menu.popup_category_empty else R.menu.popup_category when (item) {
val menu = PopupMenu(tabView.context, tabView) is CategoryListModel.All -> showAllCategoriesMenu(tabView)
menu.inflate(menuRes) is CategoryListModel.CategoryItem -> showCategoryMenu(tabView, item.category)
createOrderSubmenu(menu.menu, category)
menu.setOnMenuItemClickListener {
when (it.itemId) {
R.id.action_remove -> editDelegate.deleteCategory(category)
R.id.action_rename -> editDelegate.renameCategory(category)
R.id.action_create -> editDelegate.createCategory()
R.id.action_order -> return@setOnMenuItemClickListener false
else -> {
val order = CategoriesActivity.SORT_ORDERS.getOrNull(it.order)
?: return@setOnMenuItemClickListener false
viewModel.setCategoryOrder(category.id, order)
}
}
true
} }
menu.show()
return true return true
} }
override fun onClick(v: View) {
when (v.id) {
R.id.button_retry -> editDelegate.createCategory()
}
}
override fun onDeleteCategory(category: FavouriteCategory) { override fun onDeleteCategory(category: FavouriteCategory) {
viewModel.deleteCategory(category.id) viewModel.deleteCategory(category.id)
} }
@@ -157,13 +160,6 @@ class FavouritesContainerFragment :
viewModel.createCategory(name) viewModel.createCategory(name)
} }
private fun wrapCategories(categories: List<FavouriteCategory>): List<FavouriteCategory> {
val data = ArrayList<FavouriteCategory>(categories.size + 1)
data += FavouriteCategory(0L, getString(R.string.all_favourites), -1, SortOrder.NEWEST, Date())
data += categories
return data
}
private fun createOrderSubmenu(menu: Menu, category: FavouriteCategory) { private fun createOrderSubmenu(menu: Menu, category: FavouriteCategory) {
val submenu = menu.findItem(R.id.action_order)?.subMenu ?: return val submenu = menu.findItem(R.id.action_order)?.subMenu ?: return
for ((i, item) in CategoriesActivity.SORT_ORDERS.withIndex()) { for ((i, item) in CategoriesActivity.SORT_ORDERS.withIndex()) {
@@ -181,6 +177,52 @@ class FavouritesContainerFragment :
} }
} }
private fun showCategoryMenu(tabView: View, category: FavouriteCategory) {
val menu = PopupMenu(tabView.context, tabView)
menu.inflate(R.menu.popup_category)
createOrderSubmenu(menu.menu, category)
menu.setOnMenuItemClickListener {
when (it.itemId) {
R.id.action_remove -> editDelegate.deleteCategory(category)
R.id.action_rename -> editDelegate.renameCategory(category)
R.id.action_create -> editDelegate.createCategory()
R.id.action_order -> return@setOnMenuItemClickListener false
else -> {
val order = CategoriesActivity.SORT_ORDERS.getOrNull(it.order)
?: return@setOnMenuItemClickListener false
viewModel.setCategoryOrder(category.id, order)
}
}
true
}
menu.show()
}
private fun showAllCategoriesMenu(tabView: View) {
val menu = PopupMenu(tabView.context, tabView)
menu.inflate(R.menu.popup_category_all)
menu.setOnMenuItemClickListener {
when (it.itemId) {
R.id.action_create -> editDelegate.createCategory()
R.id.action_hide -> viewModel.setAllCategoriesVisible(false)
}
true
}
menu.show()
}
private fun showStub() {
val stub = stubBinding ?: ItemEmptyStateBinding.bind(binding.stubEmptyState.inflate())
stub.root.isVisible = true
stub.icon.setImageResource(R.drawable.ic_heart_outline)
stub.textPrimary.setText(R.string.text_empty_holder_primary)
stub.textSecondary.setText(R.string.empty_favourite_categories)
stub.buttonRetry.setText(R.string.add)
stub.buttonRetry.isVisible = true
stub.buttonRetry.setOnClickListener(this)
stubBinding = stub
}
companion object { companion object {
fun newInstance() = FavouritesContainerFragment() fun newInstance() = FavouritesContainerFragment()

View File

@@ -7,14 +7,16 @@ import androidx.recyclerview.widget.DiffUtil
import androidx.viewpager2.adapter.FragmentStateAdapter import androidx.viewpager2.adapter.FragmentStateAdapter
import com.google.android.material.tabs.TabLayout import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayoutMediator import com.google.android.material.tabs.TabLayoutMediator
import org.koitharu.kotatsu.core.model.FavouriteCategory import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.favourites.ui.categories.adapter.CategoryListModel
import org.koitharu.kotatsu.favourites.ui.list.FavouritesListFragment import org.koitharu.kotatsu.favourites.ui.list.FavouritesListFragment
class FavouritesPagerAdapter( class FavouritesPagerAdapter(
fragment: Fragment, fragment: Fragment,
private val longClickListener: FavouritesTabLongClickListener private val longClickListener: FavouritesTabLongClickListener
) : FragmentStateAdapter(fragment.childFragmentManager, fragment.viewLifecycleOwner.lifecycle), ) : FragmentStateAdapter(fragment.childFragmentManager, fragment.viewLifecycleOwner.lifecycle),
TabLayoutMediator.TabConfigurationStrategy, View.OnLongClickListener { TabLayoutMediator.TabConfigurationStrategy,
View.OnLongClickListener {
private val differ = AsyncListDiffer(this, DiffCallback()) private val differ = AsyncListDiffer(this, DiffCallback())
@@ -35,12 +37,15 @@ class FavouritesPagerAdapter(
override fun onConfigureTab(tab: TabLayout.Tab, position: Int) { override fun onConfigureTab(tab: TabLayout.Tab, position: Int) {
val item = differ.currentList[position] val item = differ.currentList[position]
tab.text = item.title tab.text = when (item) {
is CategoryListModel.All -> tab.view.context.getString(R.string.all_favourites)
is CategoryListModel.CategoryItem -> item.category.title
}
tab.view.tag = item.id tab.view.tag = item.id
tab.view.setOnLongClickListener(this) tab.view.setOnLongClickListener(this)
} }
fun replaceData(data: List<FavouriteCategory>) { fun replaceData(data: List<CategoryListModel>) {
differ.submitList(data) differ.submitList(data)
} }
@@ -50,16 +55,22 @@ class FavouritesPagerAdapter(
return longClickListener.onTabLongClick(v, item) return longClickListener.onTabLongClick(v, item)
} }
private class DiffCallback : DiffUtil.ItemCallback<FavouriteCategory>() { private class DiffCallback : DiffUtil.ItemCallback<CategoryListModel>() {
override fun areItemsTheSame( override fun areItemsTheSame(
oldItem: FavouriteCategory, oldItem: CategoryListModel,
newItem: FavouriteCategory newItem: CategoryListModel
): Boolean = oldItem.id == newItem.id ): Boolean = when {
oldItem is CategoryListModel.All && newItem is CategoryListModel.All -> true
oldItem is CategoryListModel.CategoryItem && newItem is CategoryListModel.CategoryItem -> {
oldItem.category.id == newItem.category.id
}
else -> false
}
override fun areContentsTheSame( override fun areContentsTheSame(
oldItem: FavouriteCategory, oldItem: CategoryListModel,
newItem: FavouriteCategory newItem: CategoryListModel
): Boolean = oldItem.id == newItem.id && oldItem.title == newItem.title ): Boolean = oldItem == newItem
} }
} }

View File

@@ -1,9 +1,9 @@
package org.koitharu.kotatsu.favourites.ui package org.koitharu.kotatsu.favourites.ui
import android.view.View import android.view.View
import org.koitharu.kotatsu.core.model.FavouriteCategory import org.koitharu.kotatsu.favourites.ui.categories.adapter.CategoryListModel
fun interface FavouritesTabLongClickListener { fun interface FavouritesTabLongClickListener {
fun onTabLongClick(tabView: View, category: FavouriteCategory): Boolean fun onTabLongClick(tabView: View, item: CategoryListModel): Boolean
} }

View File

@@ -0,0 +1,6 @@
package org.koitharu.kotatsu.favourites.ui.categories
interface AllCategoriesToggleListener {
fun onAllCategoriesToggle(isVisible: Boolean)
}

View File

@@ -21,6 +21,7 @@ import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.model.FavouriteCategory import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.core.ui.titleRes import org.koitharu.kotatsu.core.ui.titleRes
import org.koitharu.kotatsu.databinding.ActivityCategoriesBinding import org.koitharu.kotatsu.databinding.ActivityCategoriesBinding
import org.koitharu.kotatsu.favourites.ui.categories.adapter.CategoryListModel
import org.koitharu.kotatsu.parsers.model.SortOrder import org.koitharu.kotatsu.parsers.model.SortOrder
import org.koitharu.kotatsu.utils.ext.getDisplayMessage import org.koitharu.kotatsu.utils.ext.getDisplayMessage
import org.koitharu.kotatsu.utils.ext.measureHeight import org.koitharu.kotatsu.utils.ext.measureHeight
@@ -29,7 +30,7 @@ class CategoriesActivity :
BaseActivity<ActivityCategoriesBinding>(), BaseActivity<ActivityCategoriesBinding>(),
OnListItemClickListener<FavouriteCategory>, OnListItemClickListener<FavouriteCategory>,
View.OnClickListener, View.OnClickListener,
CategoriesEditDelegate.CategoriesEditCallback { CategoriesEditDelegate.CategoriesEditCallback, AllCategoriesToggleListener {
private val viewModel by viewModel<FavouritesCategoriesViewModel>() private val viewModel by viewModel<FavouritesCategoriesViewModel>()
@@ -41,7 +42,7 @@ class CategoriesActivity :
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(ActivityCategoriesBinding.inflate(layoutInflater)) setContentView(ActivityCategoriesBinding.inflate(layoutInflater))
supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.setDisplayHomeAsUpEnabled(true)
adapter = CategoriesAdapter(this) adapter = CategoriesAdapter(this, this)
editDelegate = CategoriesEditDelegate(this, this) editDelegate = CategoriesEditDelegate(this, this)
binding.recyclerView.setHasFixedSize(true) binding.recyclerView.setHasFixedSize(true)
binding.recyclerView.adapter = adapter binding.recyclerView.adapter = adapter
@@ -49,7 +50,7 @@ class CategoriesActivity :
reorderHelper = ItemTouchHelper(ReorderHelperCallback()) reorderHelper = ItemTouchHelper(ReorderHelperCallback())
reorderHelper.attachToRecyclerView(binding.recyclerView) reorderHelper.attachToRecyclerView(binding.recyclerView)
viewModel.categories.observe(this, ::onCategoriesChanged) viewModel.allCategories.observe(this, ::onCategoriesChanged)
viewModel.onError.observe(this, ::onError) viewModel.onError.observe(this, ::onError)
} }
@@ -84,6 +85,10 @@ class CategoriesActivity :
return true return true
} }
override fun onAllCategoriesToggle(isVisible: Boolean) {
viewModel.setAllCategoriesVisible(isVisible)
}
override fun onWindowInsetsChanged(insets: Insets) { override fun onWindowInsetsChanged(insets: Insets) {
binding.fabAdd.updateLayoutParams<ViewGroup.MarginLayoutParams> { binding.fabAdd.updateLayoutParams<ViewGroup.MarginLayoutParams> {
rightMargin = topMargin + insets.right rightMargin = topMargin + insets.right
@@ -97,7 +102,7 @@ class CategoriesActivity :
) )
} }
private fun onCategoriesChanged(categories: List<FavouriteCategory>) { private fun onCategoriesChanged(categories: List<CategoryListModel>) {
adapter.items = categories adapter.items = categories
binding.textViewHolder.isVisible = categories.isEmpty() binding.textViewHolder.isVisible = categories.isEmpty()
} }
@@ -138,13 +143,19 @@ class CategoriesActivity :
ItemTouchHelper.DOWN or ItemTouchHelper.UP, 0 ItemTouchHelper.DOWN or ItemTouchHelper.UP, 0
) { ) {
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) = Unit
override fun onMove( override fun onMove(
recyclerView: RecyclerView, recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder, viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder,
): Boolean = true ): Boolean = viewHolder.itemViewType == target.itemViewType
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) = Unit override fun canDropOver(
recyclerView: RecyclerView,
current: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder,
): Boolean = current.itemViewType == target.itemViewType
override fun onMoved( override fun onMoved(
recyclerView: RecyclerView, recyclerView: RecyclerView,
@@ -158,6 +169,8 @@ class CategoriesActivity :
super.onMoved(recyclerView, viewHolder, fromPos, target, toPos, x, y) super.onMoved(recyclerView, viewHolder, fromPos, target, toPos, x, y)
viewModel.reorderCategories(fromPos, toPos) viewModel.reorderCategories(fromPos, toPos)
} }
override fun isLongPressDragEnabled(): Boolean = false
} }
companion object { companion object {

View File

@@ -4,13 +4,18 @@ import androidx.recyclerview.widget.DiffUtil
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.model.FavouriteCategory import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.favourites.ui.categories.adapter.CategoryListModel
import org.koitharu.kotatsu.favourites.ui.categories.adapter.allCategoriesAD
import org.koitharu.kotatsu.favourites.ui.categories.adapter.categoryAD
class CategoriesAdapter( class CategoriesAdapter(
onItemClickListener: OnListItemClickListener<FavouriteCategory>, onItemClickListener: OnListItemClickListener<FavouriteCategory>,
) : AsyncListDifferDelegationAdapter<FavouriteCategory>(DiffCallback()) { allCategoriesToggleListener: AllCategoriesToggleListener,
) : AsyncListDifferDelegationAdapter<CategoryListModel>(DiffCallback()) {
init { init {
delegatesManager.addDelegate(categoryAD(onItemClickListener)) delegatesManager.addDelegate(categoryAD(onItemClickListener))
.addDelegate(allCategoriesAD(allCategoriesToggleListener))
setHasStableIds(true) setHasStableIds(true)
} }
@@ -18,28 +23,23 @@ class CategoriesAdapter(
return items[position].id return items[position].id
} }
private class DiffCallback : DiffUtil.ItemCallback<FavouriteCategory>() { private class DiffCallback : DiffUtil.ItemCallback<CategoryListModel>() {
override fun areItemsTheSame( override fun areItemsTheSame(
oldItem: FavouriteCategory, oldItem: CategoryListModel,
newItem: FavouriteCategory, newItem: CategoryListModel,
): Boolean { ): Boolean = oldItem.id == newItem.id
return oldItem.id == newItem.id
}
override fun areContentsTheSame( override fun areContentsTheSame(
oldItem: FavouriteCategory, oldItem: CategoryListModel,
newItem: FavouriteCategory, newItem: CategoryListModel,
): Boolean { ): Boolean = oldItem == newItem
return oldItem.id == newItem.id && oldItem.title == newItem.title
&& oldItem.order == newItem.order
}
override fun getChangePayload( override fun getChangePayload(
oldItem: FavouriteCategory, oldItem: CategoryListModel,
newItem: FavouriteCategory, newItem: CategoryListModel,
): Any? = when { ): Any? = when {
oldItem.title == newItem.title && oldItem.order != newItem.order -> newItem.order oldItem is CategoryListModel.All && newItem is CategoryListModel.All -> Unit
else -> super.getChangePayload(oldItem, newItem) else -> super.getChangePayload(oldItem, newItem)
} }
} }

View File

@@ -3,20 +3,36 @@ package org.koitharu.kotatsu.favourites.ui.categories
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.*
import org.koitharu.kotatsu.base.ui.BaseViewModel import org.koitharu.kotatsu.base.ui.BaseViewModel
import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
import org.koitharu.kotatsu.favourites.ui.categories.adapter.CategoryListModel
import org.koitharu.kotatsu.parsers.model.SortOrder import org.koitharu.kotatsu.parsers.model.SortOrder
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
import java.util.* import java.util.*
class FavouritesCategoriesViewModel( class FavouritesCategoriesViewModel(
private val repository: FavouritesRepository private val repository: FavouritesRepository,
private val settings: AppSettings,
) : BaseViewModel() { ) : BaseViewModel() {
private var reorderJob: Job? = null private var reorderJob: Job? = null
val categories = repository.observeCategories() val allCategories = combine(
.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default) repository.observeCategories(),
observeAllCategoriesVisible(),
) { list, showAll ->
mapCategories(list, showAll, true)
}.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default)
val visibleCategories = combine(
repository.observeCategories(),
observeAllCategoriesVisible(),
) { list, showAll ->
mapCategories(list, showAll, showAll && list.isNotEmpty())
}.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default)
fun createCategory(name: String) { fun createCategory(name: String) {
launchJob { launchJob {
@@ -42,14 +58,40 @@ class FavouritesCategoriesViewModel(
} }
} }
fun setAllCategoriesVisible(isVisible: Boolean) {
settings.isAllFavouritesVisible = isVisible
}
fun reorderCategories(oldPos: Int, newPos: Int) { fun reorderCategories(oldPos: Int, newPos: Int) {
val prevJob = reorderJob val prevJob = reorderJob
reorderJob = launchJob(Dispatchers.Default) { reorderJob = launchJob(Dispatchers.Default) {
prevJob?.join() prevJob?.join()
val items = categories.value ?: error("This should not happen") val items = allCategories.value ?: error("This should not happen")
val ids = items.mapTo(ArrayList(items.size)) { it.id } val ids = items.mapTo(ArrayList(items.size)) { it.id }
Collections.swap(ids, oldPos, newPos) Collections.swap(ids, oldPos, newPos)
ids.remove(0L)
repository.reorderCategories(ids) repository.reorderCategories(ids)
} }
} }
private fun mapCategories(
categories: List<FavouriteCategory>,
isAllCategoriesVisible: Boolean,
withAllCategoriesItem: Boolean,
): List<CategoryListModel> {
val result = ArrayList<CategoryListModel>(categories.size + 1)
if (withAllCategoriesItem) {
result.add(CategoryListModel.All(isAllCategoriesVisible))
}
categories.mapTo(result) {
CategoryListModel.CategoryItem(it)
}
return result
}
private fun observeAllCategoriesVisible() = settings.observe()
.filter { it == AppSettings.KEY_ALL_FAVOURITES_VISIBLE }
.map { settings.isAllFavouritesVisible }
.onStart { emit(settings.isAllFavouritesVisible) }
.distinctUntilChanged()
} }

View File

@@ -0,0 +1,20 @@
package org.koitharu.kotatsu.favourites.ui.categories.adapter
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.databinding.ItemCategoriesAllBinding
import org.koitharu.kotatsu.favourites.ui.categories.AllCategoriesToggleListener
fun allCategoriesAD(
allCategoriesToggleListener: AllCategoriesToggleListener,
) = adapterDelegateViewBinding<CategoryListModel.All, CategoryListModel, ItemCategoriesAllBinding>(
{ inflater, parent -> ItemCategoriesAllBinding.inflate(inflater, parent, false) }
) {
binding.imageViewToggle.setOnClickListener {
allCategoriesToggleListener.onAllCategoriesToggle(!item.isVisible)
}
bind {
binding.imageViewToggle.isChecked = item.isVisible
}
}

View File

@@ -1,4 +1,4 @@
package org.koitharu.kotatsu.favourites.ui.categories package org.koitharu.kotatsu.favourites.ui.categories.adapter
import android.view.MotionEvent import android.view.MotionEvent
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
@@ -8,23 +8,23 @@ import org.koitharu.kotatsu.databinding.ItemCategoryBinding
fun categoryAD( fun categoryAD(
clickListener: OnListItemClickListener<FavouriteCategory> clickListener: OnListItemClickListener<FavouriteCategory>
) = adapterDelegateViewBinding<FavouriteCategory, FavouriteCategory, ItemCategoryBinding>( ) = adapterDelegateViewBinding<CategoryListModel.CategoryItem, CategoryListModel, ItemCategoryBinding>(
{ inflater, parent -> ItemCategoryBinding.inflate(inflater, parent, false) } { inflater, parent -> ItemCategoryBinding.inflate(inflater, parent, false) }
) { ) {
binding.imageViewMore.setOnClickListener { binding.imageViewMore.setOnClickListener {
clickListener.onItemClick(item, it) clickListener.onItemClick(item.category, it)
} }
@Suppress("ClickableViewAccessibility") @Suppress("ClickableViewAccessibility")
binding.imageViewHandle.setOnTouchListener { v, event -> binding.imageViewHandle.setOnTouchListener { v, event ->
if (event.actionMasked == MotionEvent.ACTION_DOWN) { if (event.actionMasked == MotionEvent.ACTION_DOWN) {
clickListener.onItemLongClick(item, itemView) clickListener.onItemLongClick(item.category, itemView)
} else { } else {
false false
} }
} }
bind { bind {
binding.textViewTitle.text = item.title binding.textViewTitle.text = item.category.title
} }
} }

View File

@@ -0,0 +1,59 @@
package org.koitharu.kotatsu.favourites.ui.categories.adapter
import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.list.ui.model.ListModel
sealed interface CategoryListModel : ListModel {
val id: Long
class All(
val isVisible: Boolean,
) : CategoryListModel {
override val id: Long = 0L
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as All
if (isVisible != other.isVisible) return false
return true
}
override fun hashCode(): Int {
return isVisible.hashCode()
}
}
class CategoryItem(
val category: FavouriteCategory,
) : CategoryListModel {
override val id: Long
get() = category.id
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as CategoryItem
if (category.id != other.category.id) return false
if (category.title != other.category.title) return false
if (category.order != other.category.order) return false
return true
}
override fun hashCode(): Int {
var result = category.id.hashCode()
result = 31 * result + category.title.hashCode()
result = 31 * result + category.order.hashCode()
return result
}
}
}

View File

@@ -1,9 +1,13 @@
package org.koitharu.kotatsu.favourites.ui.list package org.koitharu.kotatsu.favourites.ui.list
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import androidx.recyclerview.widget.RecyclerView.NO_ID
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
@@ -24,6 +28,14 @@ class FavouritesListViewModel(
settings: AppSettings, settings: AppSettings,
) : MangaListViewModel(settings), CountersProvider { ) : MangaListViewModel(settings), CountersProvider {
var sortOrder: LiveData<SortOrder?> = if (categoryId == NO_ID) {
MutableLiveData(null)
} else {
repository.observeCategory(categoryId)
.map { it?.order }
.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default)
}
override val content = combine( override val content = combine(
if (categoryId == 0L) { if (categoryId == 0L) {
repository.observeAll(SortOrder.NEWEST) repository.observeAll(SortOrder.NEWEST)

View File

@@ -3,6 +3,7 @@ package org.koitharu.kotatsu.list.ui.adapter
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import coil.ImageLoader import coil.ImageLoader
import coil.request.Disposable import coil.request.Disposable
import coil.size.Scale
import coil.util.CoilUtils import coil.util.CoilUtils
import com.google.android.material.badge.BadgeDrawable import com.google.android.material.badge.BadgeDrawable
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
@@ -43,6 +44,7 @@ fun mangaGridItemAD(
.fallback(R.drawable.ic_placeholder) .fallback(R.drawable.ic_placeholder)
.error(R.drawable.ic_placeholder) .error(R.drawable.ic_placeholder)
.allowRgb565(true) .allowRgb565(true)
.scale(Scale.FILL)
.lifecycle(lifecycleOwner) .lifecycle(lifecycleOwner)
.enqueueWith(coil) .enqueueWith(coil)
badge = itemView.bindBadge(badge, item.counter) badge = itemView.bindBadge(badge, item.counter)

View File

@@ -3,6 +3,7 @@ package org.koitharu.kotatsu.list.ui.adapter
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import coil.ImageLoader import coil.ImageLoader
import coil.request.Disposable import coil.request.Disposable
import coil.size.Scale
import coil.util.CoilUtils import coil.util.CoilUtils
import com.google.android.material.badge.BadgeDrawable import com.google.android.material.badge.BadgeDrawable
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
@@ -44,6 +45,7 @@ fun mangaListDetailedItemAD(
.placeholder(R.drawable.ic_placeholder) .placeholder(R.drawable.ic_placeholder)
.fallback(R.drawable.ic_placeholder) .fallback(R.drawable.ic_placeholder)
.error(R.drawable.ic_placeholder) .error(R.drawable.ic_placeholder)
.scale(Scale.FILL)
.allowRgb565(true) .allowRgb565(true)
.lifecycle(lifecycleOwner) .lifecycle(lifecycleOwner)
.enqueueWith(coil) .enqueueWith(coil)

View File

@@ -3,6 +3,7 @@ package org.koitharu.kotatsu.list.ui.adapter
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import coil.ImageLoader import coil.ImageLoader
import coil.request.Disposable import coil.request.Disposable
import coil.size.Scale
import coil.util.CoilUtils import coil.util.CoilUtils
import com.google.android.material.badge.BadgeDrawable import com.google.android.material.badge.BadgeDrawable
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
@@ -44,6 +45,7 @@ fun mangaListItemAD(
.placeholder(R.drawable.ic_placeholder) .placeholder(R.drawable.ic_placeholder)
.fallback(R.drawable.ic_placeholder) .fallback(R.drawable.ic_placeholder)
.error(R.drawable.ic_placeholder) .error(R.drawable.ic_placeholder)
.scale(Scale.FILL)
.allowRgb565(true) .allowRgb565(true)
.lifecycle(lifecycleOwner) .lifecycle(lifecycleOwner)
.enqueueWith(coil) .enqueueWith(coil)

View File

@@ -7,7 +7,6 @@ import org.koitharu.kotatsu.download.domain.DownloadManager
import org.koitharu.kotatsu.local.data.LocalStorageManager import org.koitharu.kotatsu.local.data.LocalStorageManager
import org.koitharu.kotatsu.local.domain.LocalMangaRepository import org.koitharu.kotatsu.local.domain.LocalMangaRepository
import org.koitharu.kotatsu.local.ui.LocalListViewModel import org.koitharu.kotatsu.local.ui.LocalListViewModel
import org.koitharu.kotatsu.utils.ExternalStorageHelper
val localModule val localModule
get() = module { get() = module {
@@ -15,8 +14,6 @@ val localModule
single { LocalStorageManager(androidContext(), get()) } single { LocalStorageManager(androidContext(), get()) }
single { LocalMangaRepository(get()) } single { LocalMangaRepository(get()) }
factory { ExternalStorageHelper(androidContext()) }
factory { DownloadManager.Factory(androidContext(), get(), get(), get(), get(), get()) } factory { DownloadManager.Factory(androidContext(), get(), get(), get(), get(), get()) }
viewModel { LocalListViewModel(get(), get(), get(), get()) } viewModel { LocalListViewModel(get(), get(), get(), get()) }

View File

@@ -4,6 +4,7 @@ import android.content.ContentResolver
import android.content.Context import android.content.Context
import android.os.StatFs import android.os.StatFs
import androidx.annotation.WorkerThread import androidx.annotation.WorkerThread
import java.io.File
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runInterruptible import kotlinx.coroutines.runInterruptible
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@@ -11,7 +12,6 @@ import okhttp3.Cache
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.utils.ext.computeSize import org.koitharu.kotatsu.utils.ext.computeSize
import org.koitharu.kotatsu.utils.ext.getStorageName import org.koitharu.kotatsu.utils.ext.getStorageName
import java.io.File
private const val DIR_NAME = "manga" private const val DIR_NAME = "manga"
private const val CACHE_DISK_PERCENTAGE = 0.02 private const val CACHE_DISK_PERCENTAGE = 0.02
@@ -71,7 +71,7 @@ class LocalStorageManager(
private fun getAvailableStorageDirs(): MutableSet<File> { private fun getAvailableStorageDirs(): MutableSet<File> {
val result = LinkedHashSet<File>() val result = LinkedHashSet<File>()
result += File(context.filesDir, DIR_NAME) result += File(context.filesDir, DIR_NAME)
result += context.getExternalFilesDirs(DIR_NAME) context.getExternalFilesDirs(DIR_NAME).filterNotNullTo(result)
result.retainAll { it.exists() || it.mkdirs() } result.retainAll { it.exists() || it.mkdirs() }
return result return result
} }
@@ -87,8 +87,8 @@ class LocalStorageManager(
private fun getCacheDirs(subDir: String): MutableSet<File> { private fun getCacheDirs(subDir: String): MutableSet<File> {
val result = LinkedHashSet<File>() val result = LinkedHashSet<File>()
result += File(context.cacheDir, subDir) result += File(context.cacheDir, subDir)
context.externalCacheDirs.mapTo(result) { context.externalCacheDirs.mapNotNullTo(result) {
File(it, subDir) File(it ?: return@mapNotNullTo null, subDir)
} }
return result return result
} }
@@ -110,4 +110,4 @@ class LocalStorageManager(
private fun File.isWriteable() = runCatching { private fun File.isWriteable() = runCatching {
canWrite() canWrite()
}.getOrDefault(false) }.getOrDefault(false)
} }

View File

@@ -18,6 +18,7 @@ import org.koitharu.kotatsu.parsers.model.*
import org.koitharu.kotatsu.parsers.util.longHashCode import org.koitharu.kotatsu.parsers.util.longHashCode
import org.koitharu.kotatsu.parsers.util.toCamelCase import org.koitharu.kotatsu.parsers.util.toCamelCase
import org.koitharu.kotatsu.utils.AlphanumComparator import org.koitharu.kotatsu.utils.AlphanumComparator
import org.koitharu.kotatsu.utils.CompositeMutex
import org.koitharu.kotatsu.utils.ext.deleteAwait import org.koitharu.kotatsu.utils.ext.deleteAwait
import org.koitharu.kotatsu.utils.ext.readText import org.koitharu.kotatsu.utils.ext.readText
import org.koitharu.kotatsu.utils.ext.resolveName import org.koitharu.kotatsu.utils.ext.resolveName
@@ -34,6 +35,7 @@ class LocalMangaRepository(private val storageManager: LocalStorageManager) : Ma
override val source = MangaSource.LOCAL override val source = MangaSource.LOCAL
private val filenameFilter = CbzFilter() private val filenameFilter = CbzFilter()
private val locks = CompositeMutex<Long>()
override suspend fun getList( override suspend fun getList(
offset: Int, offset: Int,
@@ -112,11 +114,18 @@ class LocalMangaRepository(private val storageManager: LocalStorageManager) : Ma
return file.deleteAwait() return file.deleteAwait()
} }
suspend fun deleteChapters(manga: Manga, ids: Set<Long>) = runInterruptible(Dispatchers.IO) { suspend fun deleteChapters(manga: Manga, ids: Set<Long>) {
val uri = Uri.parse(manga.url) lockManga(manga.id)
val file = uri.toFile() try {
val cbz = CbzMangaOutput(file, manga) runInterruptible(Dispatchers.IO) {
CbzMangaOutput.filterChapters(cbz, ids) val uri = Uri.parse(manga.url)
val file = uri.toFile()
val cbz = CbzMangaOutput(file, manga)
CbzMangaOutput.filterChapters(cbz, ids)
}
} finally {
unlockManga(manga.id)
}
} }
@WorkerThread @WorkerThread
@@ -278,6 +287,14 @@ class LocalMangaRepository(private val storageManager: LocalStorageManager) : Ma
} }
} }
suspend fun lockManga(id: Long) {
locks.lock(id)
}
suspend fun unlockManga(id: Long) {
locks.unlock(id)
}
private suspend fun getAllFiles() = storageManager.getReadableDirs().flatMap { dir -> private suspend fun getAllFiles() = storageManager.getReadableDirs().flatMap { dir ->
dir.listFiles(filenameFilter)?.toList().orEmpty() dir.listFiles(filenameFilter)?.toList().orEmpty()
} }

View File

@@ -22,7 +22,6 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.navigation.NavigationView import com.google.android.material.navigation.NavigationView
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.koin.android.ext.android.get import org.koin.android.ext.android.get
import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.androidx.viewmodel.ext.android.viewModel
@@ -49,6 +48,7 @@ import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionListener
import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionViewModel import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionViewModel
import org.koitharu.kotatsu.settings.AppUpdateChecker import org.koitharu.kotatsu.settings.AppUpdateChecker
import org.koitharu.kotatsu.settings.SettingsActivity import org.koitharu.kotatsu.settings.SettingsActivity
import org.koitharu.kotatsu.settings.newsources.NewSourcesDialogFragment
import org.koitharu.kotatsu.settings.onboard.OnboardDialogFragment import org.koitharu.kotatsu.settings.onboard.OnboardDialogFragment
import org.koitharu.kotatsu.suggestions.ui.SuggestionsFragment import org.koitharu.kotatsu.suggestions.ui.SuggestionsFragment
import org.koitharu.kotatsu.suggestions.ui.SuggestionsWorker import org.koitharu.kotatsu.suggestions.ui.SuggestionsWorker
@@ -384,16 +384,19 @@ class MainActivity :
} }
private fun onFirstStart() { private fun onFirstStart() {
lifecycleScope.launch(Dispatchers.Default) { lifecycleScope.launchWhenResumed {
TrackWorker.setup(applicationContext) val isUpdateSupported = withContext(Dispatchers.Default) {
SuggestionsWorker.setup(applicationContext) TrackWorker.setup(applicationContext)
if (AppUpdateChecker.isUpdateSupported(this@MainActivity)) { SuggestionsWorker.setup(applicationContext)
AppUpdateChecker.isUpdateSupported(this@MainActivity)
}
if (isUpdateSupported) {
AppUpdateChecker(this@MainActivity).checkIfNeeded() AppUpdateChecker(this@MainActivity).checkIfNeeded()
} }
if (!get<AppSettings>().isSourcesSelected) { val settings = get<AppSettings>()
withContext(Dispatchers.Main) { when {
OnboardDialogFragment.showWelcome(supportFragmentManager) !settings.isSourcesSelected -> OnboardDialogFragment.showWelcome(supportFragmentManager)
} settings.newSources.isNotEmpty() -> NewSourcesDialogFragment.show(supportFragmentManager)
} }
} }
} }

View File

@@ -1,9 +1,11 @@
package org.koitharu.kotatsu.reader package org.koitharu.kotatsu.reader
import org.koin.android.ext.koin.androidContext
import org.koin.androidx.viewmodel.dsl.viewModel import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.dsl.module import org.koin.dsl.module
import org.koitharu.kotatsu.base.domain.MangaDataRepository import org.koitharu.kotatsu.base.domain.MangaDataRepository
import org.koitharu.kotatsu.local.data.PagesCache import org.koitharu.kotatsu.local.data.PagesCache
import org.koitharu.kotatsu.reader.ui.PageSaveHelper
import org.koitharu.kotatsu.reader.ui.ReaderViewModel import org.koitharu.kotatsu.reader.ui.ReaderViewModel
val readerModule val readerModule
@@ -12,6 +14,8 @@ val readerModule
single { MangaDataRepository(get()) } single { MangaDataRepository(get()) }
single { PagesCache(get()) } single { PagesCache(get()) }
factory { PageSaveHelper(get(), androidContext()) }
viewModel { params -> viewModel { params ->
ReaderViewModel( ReaderViewModel(
intent = params[0], intent = params[0],
@@ -21,7 +25,7 @@ val readerModule
historyRepository = get(), historyRepository = get(),
shortcutsRepository = get(), shortcutsRepository = get(),
settings = get(), settings = get(),
externalStorageHelper = get(), pageSaveHelper = get(),
) )
} }
} }

View File

@@ -113,6 +113,10 @@ class PageLoader : KoinComponent, Closeable {
} }
} }
suspend fun getPageUrl(page: MangaPage): String {
return getRepository(page.source).getPageUrl(page)
}
private fun onIdle() { private fun onIdle() {
synchronized(prefetchQueue) { synchronized(prefetchQueue) {
while (prefetchQueue.isNotEmpty()) { while (prefetchQueue.isNotEmpty()) {
@@ -151,7 +155,7 @@ class PageLoader : KoinComponent, Closeable {
} }
private suspend fun loadPageImpl(page: MangaPage, progress: MutableStateFlow<Float>): File { private suspend fun loadPageImpl(page: MangaPage, progress: MutableStateFlow<Float>): File {
val pageUrl = getRepository(page.source).getPageUrl(page) val pageUrl = getPageUrl(page)
check(pageUrl.isNotBlank()) { "Cannot obtain full image url" } check(pageUrl.isNotBlank()) { "Cannot obtain full image url" }
val uri = Uri.parse(pageUrl) val uri = Uri.parse(pageUrl)
return if (uri.scheme == "cbz") { return if (uri.scheme == "cbz") {

View File

@@ -0,0 +1,77 @@
package org.koitharu.kotatsu.reader.ui
import android.content.Context
import android.net.Uri
import android.webkit.MimeTypeMap
import androidx.activity.result.ActivityResultLauncher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runInterruptible
import kotlinx.coroutines.suspendCancellableCoroutine
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,
) {
private var continuation: Continuation<Uri>? = null
private val contentResolver = context.contentResolver
suspend fun savePage(
pageLoader: PageLoader,
page: MangaPage,
saveLauncher: ActivityResultLauncher<String>,
): Uri {
val pageUrl = pageLoader.getPageUrl(page)
val pageFile = pageLoader.loadPage(page, force = false)
val proposedName = getProposedFileName(pageUrl, pageFile)
val destination = withContext(Dispatchers.Main) {
suspendCancellableCoroutine<Uri> { cont ->
continuation = cont
saveLauncher.launch(proposedName)
}.also {
continuation = null
}
}
runInterruptible(Dispatchers.IO) {
contentResolver.openOutputStream(destination)?.use { output ->
pageFile.inputStream().use { input ->
input.copyTo(output)
}
} ?: throw IOException("Output stream is null")
}
return destination
}
fun onActivityResult(uri: Uri): Boolean = continuation?.apply {
resume(uri)
} != null
private suspend fun getProposedFileName(url: String, file: File): String {
var name = url.toHttpUrl().pathSegments.last()
var extension = name.substringAfterLast('.', "")
name = name.substringBeforeLast('.')
if (extension.length !in 2..4) {
val mimeType = MangaUtils.getImageMimeType(file)
extension = if (mimeType != null) {
MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType) ?: EXTENSION_FALLBACK
} else {
EXTENSION_FALLBACK
}
}
return name.toFileNameSafe().take(MAX_FILENAME_LENGTH) + "." + extension
}
}

View File

@@ -9,7 +9,6 @@ import android.view.*
import android.widget.Toast import android.widget.Toast
import androidx.activity.result.ActivityResultCallback import androidx.activity.result.ActivityResultCallback
import androidx.core.graphics.Insets import androidx.core.graphics.Insets
import androidx.core.net.toUri
import androidx.core.view.* import androidx.core.view.*
import androidx.fragment.app.commit import androidx.fragment.app.commit
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
@@ -187,10 +186,7 @@ class ReaderActivity :
R.id.action_save_page -> { R.id.action_save_page -> {
viewModel.getCurrentPage()?.also { page -> viewModel.getCurrentPage()?.also { page ->
viewModel.saveCurrentState(reader?.getCurrentState()) viewModel.saveCurrentState(reader?.getCurrentState())
val name = page.url.toUri().run { viewModel.saveCurrentPage(page, savePageRequest)
fragment ?: lastPathSegment ?: ""
}
savePageRequest.launch(name)
} ?: showWaitWhileLoading() } ?: showWaitWhileLoading()
} }
else -> return super.onOptionsItemSelected(item) else -> return super.onOptionsItemSelected(item)
@@ -199,9 +195,7 @@ class ReaderActivity :
} }
override fun onActivityResult(uri: Uri?) { override fun onActivityResult(uri: Uri?) {
if (uri != null) { viewModel.onActivityResult(uri)
viewModel.saveCurrentPage(uri)
}
} }
private fun onLoadingStateChanged(isLoading: Boolean) { private fun onLoadingStateChanged(isLoading: Boolean) {

View File

@@ -2,6 +2,7 @@ package org.koitharu.kotatsu.reader.ui
import android.net.Uri import android.net.Uri
import android.util.LongSparseArray import android.util.LongSparseArray
import androidx.activity.result.ActivityResultLauncher
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.* import kotlinx.coroutines.*
@@ -26,7 +27,6 @@ import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.reader.domain.PageLoader import org.koitharu.kotatsu.reader.domain.PageLoader
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
import org.koitharu.kotatsu.reader.ui.pager.ReaderUiState import org.koitharu.kotatsu.reader.ui.pager.ReaderUiState
import org.koitharu.kotatsu.utils.ExternalStorageHelper
import org.koitharu.kotatsu.utils.SingleLiveEvent import org.koitharu.kotatsu.utils.SingleLiveEvent
import org.koitharu.kotatsu.utils.ext.IgnoreErrors import org.koitharu.kotatsu.utils.ext.IgnoreErrors
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
@@ -40,10 +40,11 @@ class ReaderViewModel(
private val historyRepository: HistoryRepository, private val historyRepository: HistoryRepository,
private val shortcutsRepository: ShortcutsRepository, private val shortcutsRepository: ShortcutsRepository,
private val settings: AppSettings, private val settings: AppSettings,
private val externalStorageHelper: ExternalStorageHelper, private val pageSaveHelper: PageSaveHelper,
) : BaseViewModel() { ) : BaseViewModel() {
private var loadingJob: Job? = null private var loadingJob: Job? = null
private var pageSaveJob: Job? = null
private val currentState = MutableStateFlow(initialState) private val currentState = MutableStateFlow(initialState)
private val mangaData = MutableStateFlow(intent.manga) private val mangaData = MutableStateFlow(intent.manga)
private val chapters = LongSparseArray<MangaChapter>() private val chapters = LongSparseArray<MangaChapter>()
@@ -54,7 +55,7 @@ class ReaderViewModel(
val onPageSaved = SingleLiveEvent<Uri?>() val onPageSaved = SingleLiveEvent<Uri?>()
val uiState = combine( val uiState = combine(
mangaData, mangaData,
currentState currentState,
) { manga, state -> ) { manga, state ->
val chapter = state?.chapterId?.let(chapters::get) val chapter = state?.chapterId?.let(chapters::get)
ReaderUiState( ReaderUiState(
@@ -137,12 +138,16 @@ class ReaderViewModel(
return pages.filter { it.chapterId == chapterId }.map { it.toMangaPage() } return pages.filter { it.chapterId == chapterId }.map { it.toMangaPage() }
} }
fun saveCurrentPage(destination: Uri) { fun saveCurrentPage(
launchJob(Dispatchers.Default) { page: MangaPage,
saveLauncher: ActivityResultLauncher<String>,
) {
val prevJob = pageSaveJob
pageSaveJob = launchLoadingJob(Dispatchers.Default) {
prevJob?.cancelAndJoin()
try { try {
val page = getCurrentPage() ?: error("Page not found") val dest = pageSaveHelper.savePage(pageLoader, page, saveLauncher)
externalStorageHelper.savePage(page, destination) onPageSaved.postCall(dest)
onPageSaved.postCall(destination)
} catch (e: CancellationException) { } catch (e: CancellationException) {
throw e throw e
} catch (e: Exception) { } catch (e: Exception) {
@@ -154,6 +159,15 @@ class ReaderViewModel(
} }
} }
fun onActivityResult(uri: Uri?) {
if (uri != null) {
pageSaveHelper.onActivityResult(uri)
} else {
pageSaveJob?.cancel()
pageSaveJob = null
}
}
fun getCurrentPage(): MangaPage? { fun getCurrentPage(): MangaPage? {
val state = currentState.value ?: return null val state = currentState.value ?: return null
return content.value?.pages?.find { return content.value?.pages?.find {

View File

@@ -9,6 +9,7 @@ import androidx.fragment.app.FragmentManager
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetBehavior
import org.koin.android.ext.android.get import org.koin.android.ext.android.get
import org.koin.androidx.viewmodel.ext.android.getViewModel
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseBottomSheet import org.koitharu.kotatsu.base.ui.BaseBottomSheet
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
@@ -20,6 +21,8 @@ import org.koitharu.kotatsu.databinding.SheetPagesBinding
import org.koitharu.kotatsu.list.ui.MangaListSpanResolver import org.koitharu.kotatsu.list.ui.MangaListSpanResolver
import org.koitharu.kotatsu.parsers.model.MangaPage import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.reader.domain.PageLoader import org.koitharu.kotatsu.reader.domain.PageLoader
import org.koitharu.kotatsu.reader.ui.ReaderActivity
import org.koitharu.kotatsu.reader.ui.ReaderViewModel
import org.koitharu.kotatsu.reader.ui.thumbnails.adapter.PageThumbnailAdapter import org.koitharu.kotatsu.reader.ui.thumbnails.adapter.PageThumbnailAdapter
import org.koitharu.kotatsu.utils.BottomSheetToolbarController import org.koitharu.kotatsu.utils.BottomSheetToolbarController
import org.koitharu.kotatsu.utils.ext.viewLifecycleScope import org.koitharu.kotatsu.utils.ext.viewLifecycleScope
@@ -81,7 +84,7 @@ class PagesThumbnailsSheet :
dataSet = thumbnails, dataSet = thumbnails,
coil = get(), coil = get(),
scope = viewLifecycleScope, scope = viewLifecycleScope,
loader = PageLoader().also { pageLoader = it }, loader = getPageLoader(),
clickListener = this@PagesThumbnailsSheet clickListener = this@PagesThumbnailsSheet
) )
addOnLayoutChangeListener(spanResolver) addOnLayoutChangeListener(spanResolver)
@@ -109,6 +112,11 @@ class PagesThumbnailsSheet :
} }
} }
private fun getPageLoader(): PageLoader {
val viewModel = (activity as? ReaderActivity)?.getViewModel<ReaderViewModel>()
return viewModel?.pageLoader ?: PageLoader().also { pageLoader = it }
}
private inner class ToolbarController(toolbar: Toolbar) : BottomSheetToolbarController(toolbar) { private inner class ToolbarController(toolbar: Toolbar) : BottomSheetToolbarController(toolbar) {
override fun onStateChanged(bottomSheet: View, newState: Int) { override fun onStateChanged(bottomSheet: View, newState: Int) {
super.onStateChanged(bottomSheet, newState) super.onStateChanged(bottomSheet, newState)

View File

@@ -8,6 +8,15 @@ import android.net.Uri
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.annotation.MainThread import androidx.annotation.MainThread
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import java.io.ByteArrayInputStream
import java.io.InputStream
import java.security.MessageDigest
import java.security.NoSuchAlgorithmException
import java.security.cert.CertificateEncodingException
import java.security.cert.CertificateException
import java.security.cert.CertificateFactory
import java.security.cert.X509Certificate
import java.util.concurrent.TimeUnit
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.koin.android.ext.android.get import org.koin.android.ext.android.get
@@ -19,15 +28,6 @@ import org.koitharu.kotatsu.core.github.VersionId
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.parsers.util.byte2HexFormatted import org.koitharu.kotatsu.parsers.util.byte2HexFormatted
import org.koitharu.kotatsu.utils.FileSize import org.koitharu.kotatsu.utils.FileSize
import java.io.ByteArrayInputStream
import java.io.InputStream
import java.security.MessageDigest
import java.security.NoSuchAlgorithmException
import java.security.cert.CertificateEncodingException
import java.security.cert.CertificateException
import java.security.cert.CertificateFactory
import java.security.cert.X509Certificate
import java.util.concurrent.TimeUnit
class AppUpdateChecker(private val activity: ComponentActivity) { class AppUpdateChecker(private val activity: ComponentActivity) {
@@ -61,25 +61,22 @@ class AppUpdateChecker(private val activity: ComponentActivity) {
@MainThread @MainThread
private fun showUpdateDialog(version: AppVersion) { private fun showUpdateDialog(version: AppVersion) {
val message = buildString {
append(activity.getString(R.string.new_version_s, version.name))
appendLine()
append(activity.getString(R.string.size_s, FileSize.BYTES.format(activity, version.apkSize)))
appendLine()
appendLine()
append(version.description)
}
MaterialAlertDialogBuilder(activity) MaterialAlertDialogBuilder(activity)
.setTitle(R.string.app_update_available) .setTitle(R.string.app_update_available)
.setMessage(buildString { .setMessage(message)
append(activity.getString(R.string.new_version_s, version.name))
appendLine()
append(
activity.getString(
R.string.size_s,
FileSize.BYTES.format(activity, version.apkSize),
)
)
appendLine()
appendLine()
append(version.description)
})
.setPositiveButton(R.string.download) { _, _ -> .setPositiveButton(R.string.download) { _, _ ->
activity.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(version.apkUrl))) activity.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(version.apkUrl)))
} }
.setNegativeButton(R.string.close, null) .setNegativeButton(R.string.close, null)
.setCancelable(false)
.create() .create()
.show() .show()
} }
@@ -128,4 +125,4 @@ class AppUpdateChecker(private val activity: ComponentActivity) {
} }
} }
} }
} }

View File

@@ -9,6 +9,7 @@ import org.koitharu.kotatsu.core.backup.RestoreRepository
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.settings.backup.BackupViewModel import org.koitharu.kotatsu.settings.backup.BackupViewModel
import org.koitharu.kotatsu.settings.backup.RestoreViewModel import org.koitharu.kotatsu.settings.backup.RestoreViewModel
import org.koitharu.kotatsu.settings.newsources.NewSourcesViewModel
import org.koitharu.kotatsu.settings.onboard.OnboardViewModel import org.koitharu.kotatsu.settings.onboard.OnboardViewModel
import org.koitharu.kotatsu.settings.protect.ProtectSetupViewModel import org.koitharu.kotatsu.settings.protect.ProtectSetupViewModel
import org.koitharu.kotatsu.settings.sources.SourcesSettingsViewModel import org.koitharu.kotatsu.settings.sources.SourcesSettingsViewModel
@@ -27,4 +28,5 @@ val settingsModule
viewModel { ProtectSetupViewModel(get()) } viewModel { ProtectSetupViewModel(get()) }
viewModel { OnboardViewModel(get()) } viewModel { OnboardViewModel(get()) }
viewModel { SourcesSettingsViewModel(get()) } viewModel { SourcesSettingsViewModel(get()) }
viewModel { NewSourcesViewModel(get()) }
} }

View File

@@ -0,0 +1,68 @@
package org.koitharu.kotatsu.settings.newsources
import android.content.DialogInterface
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.FragmentManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.koin.android.ext.android.get
import org.koin.androidx.viewmodel.ext.android.viewModel
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.AlertDialogFragment
import org.koitharu.kotatsu.databinding.DialogOnboardBinding
import org.koitharu.kotatsu.settings.sources.adapter.SourceConfigAdapter
import org.koitharu.kotatsu.settings.sources.adapter.SourceConfigListener
import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem
class NewSourcesDialogFragment :
AlertDialogFragment<DialogOnboardBinding>(),
SourceConfigListener,
DialogInterface.OnClickListener {
private val viewModel by viewModel<NewSourcesViewModel>()
override fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): DialogOnboardBinding {
return DialogOnboardBinding.inflate(inflater, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val adapter = SourceConfigAdapter(this, get(), viewLifecycleOwner)
binding.recyclerView.adapter = adapter
binding.textViewTitle.setText(R.string.new_sources_text)
viewModel.sources.observe(viewLifecycleOwner) { adapter.items = it }
}
override fun onBuildDialog(builder: MaterialAlertDialogBuilder) {
builder
.setPositiveButton(R.string.done, this)
.setCancelable(true)
.setTitle(R.string.remote_sources)
}
override fun onClick(dialog: DialogInterface, which: Int) {
viewModel.apply()
dialog.dismiss()
}
override fun onItemSettingsClick(item: SourceConfigItem.SourceItem) = Unit
override fun onItemEnabledChanged(item: SourceConfigItem.SourceItem, isEnabled: Boolean) {
viewModel.onItemEnabledChanged(item, isEnabled)
}
override fun onDragHandleTouch(holder: RecyclerView.ViewHolder) = Unit
override fun onHeaderClick(header: SourceConfigItem.LocaleGroup) = Unit
companion object {
private const val TAG = "NewSources"
fun show(fm: FragmentManager) = NewSourcesDialogFragment().show(fm, TAG)
}
}

View File

@@ -0,0 +1,42 @@
package org.koitharu.kotatsu.settings.newsources
import androidx.lifecycle.MutableLiveData
import org.koitharu.kotatsu.base.ui.BaseViewModel
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem
class NewSourcesViewModel(
private val settings: AppSettings,
) : BaseViewModel() {
val sources = MutableLiveData<List<SourceConfigItem>>()
private val initialList = settings.newSources
init {
buildList()
}
fun onItemEnabledChanged(item: SourceConfigItem.SourceItem, isEnabled: Boolean) {
if (isEnabled) {
settings.hiddenSources -= item.source.name
} else {
settings.hiddenSources += item.source.name
}
}
fun apply() {
settings.markKnownSources(initialList)
}
private fun buildList() {
val hidden = settings.hiddenSources
sources.value = initialList.map {
SourceConfigItem.SourceItem(
source = it,
summary = null,
isEnabled = it.name !in hidden,
isDraggable = false,
)
}
}
}

View File

@@ -18,8 +18,10 @@ import org.koitharu.kotatsu.utils.ext.observeNotNull
import org.koitharu.kotatsu.utils.ext.showAllowStateLoss import org.koitharu.kotatsu.utils.ext.showAllowStateLoss
import org.koitharu.kotatsu.utils.ext.withArgs import org.koitharu.kotatsu.utils.ext.withArgs
class OnboardDialogFragment : AlertDialogFragment<DialogOnboardBinding>(), class OnboardDialogFragment :
OnListItemClickListener<SourceLocale>, DialogInterface.OnClickListener { AlertDialogFragment<DialogOnboardBinding>(),
OnListItemClickListener<SourceLocale>,
DialogInterface.OnClickListener {
private val viewModel by viewModel<OnboardViewModel>() private val viewModel by viewModel<OnboardViewModel>()
private var isWelcome: Boolean = false private var isWelcome: Boolean = false
@@ -53,6 +55,7 @@ class OnboardDialogFragment : AlertDialogFragment<DialogOnboardBinding>(),
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
val adapter = SourceLocalesAdapter(this) val adapter = SourceLocalesAdapter(this)
binding.recyclerView.adapter = adapter binding.recyclerView.adapter = adapter
binding.textViewTitle.setText(R.string.onboard_text)
viewModel.list.observeNotNull(viewLifecycleOwner) { viewModel.list.observeNotNull(viewLifecycleOwner) {
adapter.items = it adapter.items = it
} }

View File

@@ -3,7 +3,6 @@ package org.koitharu.kotatsu.settings.onboard
import androidx.collection.ArraySet import androidx.collection.ArraySet
import androidx.core.os.LocaleListCompat import androidx.core.os.LocaleListCompat
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import java.util.*
import org.koitharu.kotatsu.base.ui.BaseViewModel import org.koitharu.kotatsu.base.ui.BaseViewModel
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
@@ -12,6 +11,7 @@ import org.koitharu.kotatsu.parsers.util.toTitleCase
import org.koitharu.kotatsu.settings.onboard.model.SourceLocale import org.koitharu.kotatsu.settings.onboard.model.SourceLocale
import org.koitharu.kotatsu.utils.ext.map import org.koitharu.kotatsu.utils.ext.map
import org.koitharu.kotatsu.utils.ext.mapToSet import org.koitharu.kotatsu.utils.ext.mapToSet
import java.util.*
class OnboardViewModel( class OnboardViewModel(
private val settings: AppSettings, private val settings: AppSettings,
@@ -55,6 +55,7 @@ class OnboardViewModel(
settings.hiddenSources = allSources.filterNot { x -> settings.hiddenSources = allSources.filterNot { x ->
x.locale in selectedLocales x.locale in selectedLocales
}.mapToSet { x -> x.name } }.mapToSet { x -> x.name }
settings.markKnownSources(settings.newSources)
} }
private fun rebuildList() { private fun rebuildList() {

View File

@@ -83,7 +83,9 @@ fun sourceConfigDraggableItemDelegate(
on = { item, _, _ -> item is SourceConfigItem.SourceItem && item.isDraggable } on = { item, _, _ -> item is SourceConfigItem.SourceItem && item.isDraggable }
) { ) {
val eventListener = object : View.OnClickListener, View.OnTouchListener, val eventListener = object :
View.OnClickListener,
View.OnTouchListener,
CompoundButton.OnCheckedChangeListener { CompoundButton.OnCheckedChangeListener {
override fun onClick(v: View?) = listener.onItemSettingsClick(item) override fun onClick(v: View?) = listener.onItemSettingsClick(item)

View File

@@ -3,6 +3,7 @@ package org.koitharu.kotatsu.tracker.ui.adapter
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import coil.ImageLoader import coil.ImageLoader
import coil.request.Disposable import coil.request.Disposable
import coil.size.Scale
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
@@ -34,6 +35,7 @@ fun feedItemAD(
.fallback(R.drawable.ic_placeholder) .fallback(R.drawable.ic_placeholder)
.error(R.drawable.ic_placeholder) .error(R.drawable.ic_placeholder)
.allowRgb565(true) .allowRgb565(true)
.scale(Scale.FILL)
.lifecycle(lifecycleOwner) .lifecycle(lifecycleOwner)
.enqueueWith(coil) .enqueueWith(coil)
binding.textViewTitle.text = item.title binding.textViewTitle.text = item.title

View File

@@ -2,15 +2,16 @@ package org.koitharu.kotatsu.utils
import android.view.View import android.view.View
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
import com.google.android.material.R as materialR
import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.R as materialR
open class BottomSheetToolbarController( open class BottomSheetToolbarController(
protected val toolbar: Toolbar, protected val toolbar: Toolbar,
) : BottomSheetBehavior.BottomSheetCallback() { ) : BottomSheetBehavior.BottomSheetCallback() {
override fun onStateChanged(bottomSheet: View, newState: Int) { override fun onStateChanged(bottomSheet: View, newState: Int) {
if (newState == BottomSheetBehavior.STATE_EXPANDED) { val isExpanded = newState == BottomSheetBehavior.STATE_EXPANDED && bottomSheet.top <= 0
if (isExpanded) {
toolbar.setNavigationIcon(materialR.drawable.abc_ic_clear_material) toolbar.setNavigationIcon(materialR.drawable.abc_ic_clear_material)
} else { } else {
toolbar.navigationIcon = null toolbar.navigationIcon = null

View File

@@ -0,0 +1,66 @@
package org.koitharu.kotatsu.utils
import kotlinx.coroutines.CancellableContinuation
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import java.util.*
import kotlin.coroutines.resume
class CompositeMutex<T : Any> : Set<T> {
private val data = HashMap<T, MutableList<CancellableContinuation<Unit>>>()
private val mutex = Mutex()
override val size: Int
get() = data.size
override fun contains(element: T): Boolean {
return data.containsKey(element)
}
override fun containsAll(elements: Collection<T>): Boolean {
return elements.all { x -> data.containsKey(x) }
}
override fun isEmpty(): Boolean {
return data.isEmpty()
}
override fun iterator(): Iterator<T> {
return data.keys.iterator()
}
suspend fun lock(element: T) {
waitForRemoval(element)
mutex.withLock {
val lastValue = data.put(element, LinkedList<CancellableContinuation<Unit>>())
check(lastValue == null) {
"CompositeMutex is double-locked for $element"
}
}
}
suspend fun unlock(element: T) {
val continuations = mutex.withLock {
checkNotNull(data.remove(element)) {
"CompositeMutex is not locked for $element"
}
}
continuations.forEach { c ->
if (c.isActive) {
c.resume(Unit)
}
}
}
private suspend fun waitForRemoval(element: T) {
val list = data[element] ?: return
suspendCancellableCoroutine<Unit> { continuation ->
list.add(continuation)
continuation.invokeOnCancellation {
list.remove(continuation)
}
}
}
}

View File

@@ -1,26 +0,0 @@
package org.koitharu.kotatsu.utils
import android.content.Context
import android.net.Uri
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runInterruptible
import okio.IOException
import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.reader.domain.PageLoader
class ExternalStorageHelper(context: Context) {
private val contentResolver = context.contentResolver
suspend fun savePage(page: MangaPage, destination: Uri) {
val pageLoader = PageLoader()
val pageFile = pageLoader.loadPage(page, force = false)
runInterruptible(Dispatchers.IO) {
contentResolver.openOutputStream(destination)?.use { output ->
pageFile.inputStream().use { input ->
input.copyTo(output)
}
} ?: throw IOException("Output stream is null")
}
}
}

View File

@@ -1,6 +1,7 @@
package org.koitharu.kotatsu.utils.progress package org.koitharu.kotatsu.utils.progress
import android.os.SystemClock import android.os.SystemClock
import java.util.concurrent.TimeUnit
import kotlin.math.roundToInt import kotlin.math.roundToInt
import kotlin.math.roundToLong import kotlin.math.roundToLong
@@ -11,6 +12,7 @@ class TimeLeftEstimator {
private var times = ArrayList<Int>() private var times = ArrayList<Int>()
private var lastTick: Tick? = null private var lastTick: Tick? = null
private val tooLargeTime = TimeUnit.DAYS.toMillis(1)
fun tick(value: Int, total: Int) { fun tick(value: Int, total: Int) {
if (total < 0) { if (total < 0) {
@@ -36,7 +38,8 @@ class TimeLeftEstimator {
} }
val timePerTick = times.average() val timePerTick = times.average()
val ticksLeft = progress.total - progress.value val ticksLeft = progress.total - progress.value
return (ticksLeft * timePerTick).roundToLong() val eta = (ticksLeft * timePerTick).roundToLong()
return if (eta < tooLargeTime) eta else NO_TIME
} }
private class Tick( private class Tick(

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="?colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000"
android:pathData="M2,5.27L3.28,4L20,20.72L18.73,22L15.65,18.92C14.5,19.3 13.28,19.5 12,19.5C7,19.5 2.73,16.39 1,12C1.69,10.24 2.79,8.69 4.19,7.46L2,5.27M12,9A3,3 0 0,1 15,12C15,12.35 14.94,12.69 14.83,13L11,9.17C11.31,9.06 11.65,9 12,9M12,4.5C17,4.5 21.27,7.61 23,12C22.18,14.08 20.79,15.88 19,17.19L17.58,15.76C18.94,14.82 20.06,13.54 20.82,12C19.17,8.64 15.76,6.5 12,6.5C10.91,6.5 9.84,6.68 8.84,7L7.3,5.47C8.74,4.85 10.33,4.5 12,4.5M3.18,12C4.83,15.36 8.24,17.5 12,17.5C12.69,17.5 13.37,17.43 14,17.29L11.72,15C10.29,14.85 9.15,13.71 9,12.28L5.6,8.87C4.61,9.72 3.78,10.78 3.18,12Z" />
</vector>

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<group
android:scaleX="0.12950581"
android:scaleY="0.12950581"
android:translateX="20.846512"
android:translateY="20.846512">
<path
android:fillColor="#ffffff"
android:pathData="m256,206c-50.54,0 -91.67,44.86 -91.67,100 0,55.14 41.13,100 91.67,100 50.54,0 91.67,-44.86 91.67,-100 0,-55.14 -41.13,-100 -91.67,-100zM221.79,284.73c-1.46,2.91 -4.41,4.61 -7.45,4.61 -1.25,0 -2.52,-0.28 -3.73,-0.88l-12.94,-6.48 -12.94,6.48c-4.13,2.07 -9.11,0.37 -11.18,-3.73 -2.05,-4.12 -0.39,-9.11 3.73,-11.18l16.67,-8.33c2.34,-1.17 5.11,-1.17 7.45,0l16.67,8.33c4.12,2.07 5.78,7.06 3.73,11.18zM280.12,259.73c-1.46,2.91 -4.41,4.61 -7.45,4.61 -1.25,0 -2.52,-0.28 -3.73,-0.88l-12.94,-6.48 -12.94,6.48c-4.13,2.07 -9.11,0.37 -11.18,-3.73 -2.05,-4.12 -0.39,-9.11 3.73,-11.18l16.67,-8.33c2.34,-1.17 5.11,-1.17 7.45,0l16.67,8.33c4.12,2.07 5.78,7.06 3.73,11.18zM334.73,273.54c4.12,2.07 5.78,7.06 3.73,11.18 -1.46,2.91 -4.41,4.61 -7.45,4.61 -1.25,0 -2.52,-0.28 -3.73,-0.88l-12.94,-6.48 -12.94,6.48c-4.12,2.07 -9.11,0.37 -11.18,-3.73 -2.05,-4.12 -0.39,-9.11 3.73,-11.18l16.67,-8.33c2.34,-1.17 5.11,-1.17 7.45,0z"
android:strokeWidth="0.781247" />
<path
android:fillColor="#ffffff"
android:pathData="m364.24,169.25c-3.48,-6.91 -6.92,-13.42 -10.21,-19.37l-8.27,-14.48c-6.8,-11.52 -12.26,-19.9 -14.78,-23.67 -0.72,-43.33 -19.12,-53.79 -21.26,-54.87 -3.21,-1.58 -7.1,-0.98 -9.62,1.56 -12.26,12.26 -20.23,24.46 -24.01,30.89h-40.2c-3.78,-6.43 -11.75,-18.64 -24.01,-30.89 -2.52,-2.54 -6.4,-3.14 -9.62,-1.56 -2.13,1.07 -20.54,11.54 -21.26,54.87 -2.53,3.76 -7.99,12.15 -14.78,23.66l-8.27,14.49c-3.29,5.96 -6.73,12.47 -10.21,19.38l-7.44,15.33c-17.73,38.05 -34.3,85.43 -34.3,129.73 0,69.32 58.27,128.42 60.76,130.89 0.91,0.91 2.03,1.61 3.26,2.02 1.07,0.36 26.79,8.76 69.32,8.76 2.21,0 4.33,-0.88 5.89,-2.44l5.89,-5.89h9.77l5.89,5.89c1.56,1.56 3.68,2.44 5.89,2.44 42.53,0 68.25,-8.4 69.32,-8.76 1.22,-0.41 2.34,-1.11 3.26,-2.02 2.49,-2.47 60.76,-61.57 60.76,-130.89 0,-44.3 -16.57,-91.67 -34.3,-129.73zM297.67,122.67c4.61,0 4.41,7.35 4.41,11.96 4.61,0 12.25,0.1 12.25,4.71 0,9.2 -7.47,16.67 -16.67,16.67 -9.2,0 -16.67,-7.47 -16.67,-16.67 0,-9.2 7.47,-16.67 16.67,-16.67zM239.97,152.81c1.29,-3.11 4.33,-5.14 7.7,-5.14h16.67c3.37,0 6.41,2.03 7.7,5.14 1.29,3.11 0.57,6.71 -1.81,9.08l-8.33,8.33c-1.63,1.63 -3.76,2.44 -5.89,2.44 -2.13,0 -4.26,-0.81 -5.89,-2.44l-8.33,-8.33c-2.37,-2.38 -3.09,-5.97 -1.8,-9.08zM214.33,122.67c4.61,0 4.41,7.35 4.41,11.96 4.61,0 12.25,0.1 12.25,4.71 0,9.2 -7.47,16.67 -16.67,16.67 -9.2,0 -16.67,-7.47 -16.67,-16.67 -0,-9.2 7.47,-16.67 16.67,-16.67zM256,422.67c-59.73,0 -108.33,-52.34 -108.33,-116.67 0,-64.32 48.6,-116.67 108.33,-116.67 59.73,0 108.33,52.34 108.33,116.67 0,64.32 -48.6,116.67 -108.33,116.67z"
android:strokeWidth="0.781247" />
</group>
</vector>

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="?colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000"
android:pathData="M12,9A3,3 0 0,1 15,12A3,3 0 0,1 12,15A3,3 0 0,1 9,12A3,3 0 0,1 12,9M12,4.5C17,4.5 21.27,7.61 23,12C21.27,16.39 17,19.5 12,19.5C7,19.5 2.73,16.39 1,12C2.73,7.61 7,4.5 12,4.5M3.18,12C4.83,15.36 8.24,17.5 12,17.5C15.76,17.5 19.17,15.36 20.82,12C19.17,8.64 15.76,6.5 12,6.5C8.24,6.5 4.83,8.64 3.18,12Z" />
</vector>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/ic_shown" android:state_checked="true" />
<item android:drawable="@drawable/ic_hidden" android:state_checked="false" />
</selector>

View File

@@ -10,7 +10,7 @@
<com.google.android.material.appbar.MaterialToolbar <com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar" android:id="@+id/toolbar"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="?attr/actionBarSize"
app:menu="@menu/opt_favourites_bs" app:menu="@menu/opt_favourites_bs"
app:title="@string/add_to_favourites" /> app:title="@string/add_to_favourites" />

View File

@@ -8,14 +8,15 @@
android:orientation="vertical"> android:orientation="vertical">
<TextView <TextView
android:id="@+id/textView_title"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:paddingStart="?listPreferredItemPaddingStart" android:paddingStart="?listPreferredItemPaddingStart"
android:paddingTop="6dp" android:paddingTop="6dp"
android:paddingEnd="?listPreferredItemPaddingEnd" android:paddingEnd="?listPreferredItemPaddingEnd"
android:paddingBottom="6dp" android:paddingBottom="6dp"
android:text="@string/onboard_text" android:textAppearance="?attr/textAppearanceBodyMedium"
android:textAppearance="?attr/textAppearanceBodyMedium" /> tools:text="@string/onboard_text" />
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view" android:id="@+id/recycler_view"

View File

@@ -17,4 +17,10 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" /> android:layout_height="match_parent" />
<ViewStub
android:id="@+id/stub_empty_state"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout="@layout/item_empty_state" />
</LinearLayout> </LinearLayout>

View File

@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="?android:listPreferredItemHeightSmall"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingStart="?listPreferredItemPaddingStart"
tools:ignore="Overdraw">
<TextView
android:id="@+id/textView_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:ellipsize="marquee"
android:fadingEdge="horizontal"
android:singleLine="true"
android:text="@string/all_favourites"
android:textAppearance="?attr/textAppearanceBodyLarge" />
<org.koitharu.kotatsu.base.ui.widgets.CheckableImageView
android:id="@+id/imageView_toggle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?selectableItemBackgroundBorderless"
android:padding="?listPreferredItemPaddingEnd"
android:scaleType="center"
app:srcCompat="@drawable/ic_shown_hidden" />
</LinearLayout>

View File

@@ -2,6 +2,10 @@
<menu <menu
xmlns:android="http://schemas.android.com/apk/res/android"> xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/action_hide"
android:title="@string/hide" />
<item <item
android:id="@+id/action_create" android:id="@+id/action_create"
android:title="@string/create_category" /> android:title="@string/create_category" />

View File

@@ -3,5 +3,5 @@
xmlns:android="http://schemas.android.com/apk/res/android"> xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/blue_primary" /> <background android:drawable="@color/blue_primary" />
<foreground android:drawable="@mipmap/ic_launcher_foreground" /> <foreground android:drawable="@mipmap/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_totoro" /> <monochrome android:drawable="@drawable/ic_launcher_monochrome" />
</adaptive-icon> </adaptive-icon>

View File

@@ -2,5 +2,5 @@
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/blue_primary"/> <background android:drawable="@color/blue_primary"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground" /> <foreground android:drawable="@mipmap/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_totoro" /> <monochrome android:drawable="@drawable/ic_launcher_monochrome" />
</adaptive-icon> </adaptive-icon>

View File

@@ -272,4 +272,10 @@
<string name="text_delete_local_manga_batch">Ausgewählte Elemente dauerhaft vom Gerät löschen\?</string> <string name="text_delete_local_manga_batch">Ausgewählte Elemente dauerhaft vom Gerät löschen\?</string>
<string name="batch_manga_save_confirm">Sind Sie sicher, dass Sie alle ausgewählten Mangas mit allen Kapiteln herunterladen möchten\? Diese Aktion kann eine Menge Datenverkehr und Speicherplatz verbrauchen</string> <string name="batch_manga_save_confirm">Sind Sie sicher, dass Sie alle ausgewählten Mangas mit allen Kapiteln herunterladen möchten\? Diese Aktion kann eine Menge Datenverkehr und Speicherplatz verbrauchen</string>
<string name="removal_completed">Entfernung abgeschlossen</string> <string name="removal_completed">Entfernung abgeschlossen</string>
<string name="download_slowdown">Download-Verzögerung</string>
<string name="parallel_downloads">Parallele Downloads</string>
<string name="local_manga_processing">Gespeicherte Manga-Verarbeitung</string>
<string name="download_slowdown_summary">Hilft, das Blockieren Ihrer IP-Adresse zu vermeiden</string>
<string name="chapters_will_removed_background">Die Kapitel werden im Hintergrund entfernt. Das kann einige Zeit dauern</string>
<string name="hide">Ausblenden</string>
</resources> </resources>

View File

@@ -271,4 +271,11 @@
<string name="suggestions_excluded_genres">Sulje pois genrejä</string> <string name="suggestions_excluded_genres">Sulje pois genrejä</string>
<string name="text_delete_local_manga_batch">Poista valitut kohteet laitteesta pysyvästi\?</string> <string name="text_delete_local_manga_batch">Poista valitut kohteet laitteesta pysyvästi\?</string>
<string name="removal_completed">Poisto valmis</string> <string name="removal_completed">Poisto valmis</string>
<string name="parallel_downloads">Rinnakkaislataukset</string>
<string name="download_slowdown">Latauksen hidastuminen</string>
<string name="download_slowdown_summary">Auttaa välttämään IP-osoitteesi estämisen</string>
<string name="chapters_will_removed_background">Luvut poistetaan taustalla. Se voi kestää jonkin aikaa</string>
<string name="batch_manga_save_confirm">Oletko varma, että haluat ladata kaikki valitut mangat kaikkine lukuineen\? Tämä toiminto voi kuluttaa paljon liikennettä ja tallennustilaa</string>
<string name="local_manga_processing">Tallennettujen mangojen käsittely</string>
<string name="hide">Piilota</string>
</resources> </resources>

View File

@@ -272,4 +272,10 @@
<string name="text_delete_local_manga_batch">Supprimer définitivement les éléments sélectionnés de l\'appareil \?</string> <string name="text_delete_local_manga_batch">Supprimer définitivement les éléments sélectionnés de l\'appareil \?</string>
<string name="removal_completed">Suppression terminée</string> <string name="removal_completed">Suppression terminée</string>
<string name="batch_manga_save_confirm">Voulez-vous vraiment télécharger tous les mangas sélectionnés avec tous leurs chapitres \? Cette action peut consommer beaucoup de trafic et de stockage</string> <string name="batch_manga_save_confirm">Voulez-vous vraiment télécharger tous les mangas sélectionnés avec tous leurs chapitres \? Cette action peut consommer beaucoup de trafic et de stockage</string>
<string name="parallel_downloads">Téléchargements parallèles</string>
<string name="download_slowdown">Ralentissement du téléchargement</string>
<string name="download_slowdown_summary">Permet d\'éviter le blocage de votre adresse IP</string>
<string name="chapters_will_removed_background">Les chapitres seront supprimés en arrière-plan. Cela peut prendre un certain temps</string>
<string name="local_manga_processing">Traitement des mangas sauvegardés</string>
<string name="hide">Masquer</string>
</resources> </resources>

View File

@@ -272,4 +272,10 @@
<string name="removal_completed">Rimozione completata</string> <string name="removal_completed">Rimozione completata</string>
<string name="text_delete_local_manga_batch">Eliminare gli elementi selezionati dal dispositivo in modo permanente\?</string> <string name="text_delete_local_manga_batch">Eliminare gli elementi selezionati dal dispositivo in modo permanente\?</string>
<string name="batch_manga_save_confirm">Vuoi davvero scaricare tutti i manga selezionati con tutti i loro capitoli\? Questa azione può consumare molto traffico e memoria</string> <string name="batch_manga_save_confirm">Vuoi davvero scaricare tutti i manga selezionati con tutti i loro capitoli\? Questa azione può consumare molto traffico e memoria</string>
<string name="parallel_downloads">Scaricamenti paralleli</string>
<string name="download_slowdown">Rallentamento dello scaricamento</string>
<string name="local_manga_processing">Elaborazione dei manga salvati</string>
<string name="chapters_will_removed_background">I capitoli saranno rimossi in sfondo. Può richiedere un po\' di tempo</string>
<string name="download_slowdown_summary">Aiuta ad evitare il blocco del tuo indirizzo IP</string>
<string name="hide">Nascondi</string>
</resources> </resources>

View File

@@ -272,4 +272,10 @@
<string name="text_delete_local_manga_batch">選択した項目をデバイスから完全に削除しますか?</string> <string name="text_delete_local_manga_batch">選択した項目をデバイスから完全に削除しますか?</string>
<string name="removal_completed">削除が完了しました</string> <string name="removal_completed">削除が完了しました</string>
<string name="batch_manga_save_confirm">本当に選択したマンガを全編ダウンロードしますか?この動作は多くのトラフィックとストレージを消費する可能性があります</string> <string name="batch_manga_save_confirm">本当に選択したマンガを全編ダウンロードしますか?この動作は多くのトラフィックとストレージを消費する可能性があります</string>
<string name="download_slowdown_summary">IPアドレスのブロックを回避することができます</string>
<string name="local_manga_processing">保存されたマンガの処理</string>
<string name="download_slowdown">ダウンロードの速度低下</string>
<string name="parallel_downloads">並列ダウンロード</string>
<string name="chapters_will_removed_background">チャプターはバックグラウンドで削除されます。時間がかかる場合があります</string>
<string name="hide">隠す</string>
</resources> </resources>

View File

@@ -272,4 +272,10 @@
<string name="removal_completed">Remoção concluída</string> <string name="removal_completed">Remoção concluída</string>
<string name="text_delete_local_manga_batch">Excluir itens selecionados do dispositivo permanentemente\?</string> <string name="text_delete_local_manga_batch">Excluir itens selecionados do dispositivo permanentemente\?</string>
<string name="batch_manga_save_confirm">Tem certeza de que deseja baixar todos os mangás selecionados com todos os seus capítulos\? Essa ação pode consumir muito tráfego e armazenamento</string> <string name="batch_manga_save_confirm">Tem certeza de que deseja baixar todos os mangás selecionados com todos os seus capítulos\? Essa ação pode consumir muito tráfego e armazenamento</string>
<string name="hide">Esconder</string>
<string name="download_slowdown">Baixar lentidão</string>
<string name="download_slowdown_summary">Ajuda a evitar o bloqueio do seu endereço IP</string>
<string name="local_manga_processing">Processamento de mangá salvo</string>
<string name="chapters_will_removed_background">Os capítulos serão removidos em segundo plano. Pode levar algum tempo</string>
<string name="parallel_downloads">Downloads paralelos</string>
</resources> </resources>

View File

@@ -276,4 +276,7 @@
<string name="download_slowdown_summary">Помогает избежать блокировки IP-адреса</string> <string name="download_slowdown_summary">Помогает избежать блокировки IP-адреса</string>
<string name="local_manga_processing">Обработка сохранённой манги</string> <string name="local_manga_processing">Обработка сохранённой манги</string>
<string name="chapters_will_removed_background">Главы будут удалены в фоновом режиме. Это может занять какое-то время</string> <string name="chapters_will_removed_background">Главы будут удалены в фоновом режиме. Это может занять какое-то время</string>
<string name="hide">Скрыть</string>
<string name="new_sources_text">Доступны новые источники манги</string>
<string name="empty_favourite_categories">Нет категорий избранного</string>
</resources> </resources>

View File

@@ -272,4 +272,5 @@
<string name="text_delete_local_manga_batch">Vill du ta bort markerade objekt från enheten permanent\?</string> <string name="text_delete_local_manga_batch">Vill du ta bort markerade objekt från enheten permanent\?</string>
<string name="percent_string_pattern">%1$s%%</string> <string name="percent_string_pattern">%1$s%%</string>
<string name="batch_manga_save_confirm">Är du säker på att du vill ladda ner alla utvalda manga med alla kapitel\? Den här åtgärden kan kräva mycket nätverkstrafik och lagringsutrymme</string> <string name="batch_manga_save_confirm">Är du säker på att du vill ladda ner alla utvalda manga med alla kapitel\? Den här åtgärden kan kräva mycket nätverkstrafik och lagringsutrymme</string>
<string name="parallel_downloads">Parallella nedladdningar</string>
</resources> </resources>

View File

@@ -272,4 +272,10 @@
<string name="text_delete_local_manga_batch">Seçilen ögeler aygıttan kalıcı olarak silinsin mi\?</string> <string name="text_delete_local_manga_batch">Seçilen ögeler aygıttan kalıcı olarak silinsin mi\?</string>
<string name="batch_manga_save_confirm">Seçilen tüm mangaları tüm bölümleriyle birlikte indirmek istediğinizden emin misiniz\? Bu işlem çok fazla trafik ve depolama alanı tüketebilir</string> <string name="batch_manga_save_confirm">Seçilen tüm mangaları tüm bölümleriyle birlikte indirmek istediğinizden emin misiniz\? Bu işlem çok fazla trafik ve depolama alanı tüketebilir</string>
<string name="removal_completed">Kaldırma tamamlandı</string> <string name="removal_completed">Kaldırma tamamlandı</string>
<string name="chapters_will_removed_background">Bölümler arka planda kaldırılacaktır. Bu biraz zaman alabilir</string>
<string name="parallel_downloads">Paralel indirmeler</string>
<string name="download_slowdown">İndirmeyi yavaşlat</string>
<string name="download_slowdown_summary">IP adresinizin engellenmesinden kaçınmanıza yardımcı olur</string>
<string name="local_manga_processing">Kaydedilen manga işleme</string>
<string name="hide">Gizle</string>
</resources> </resources>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="ThemeOverlay.Kotatsu.BottomSheetDialog" parent="ThemeOverlay.Material3.DayNight.BottomSheetDialog">
<item name="android:navigationBarColor">@color/navigation_bar_scrim</item>
<item name="android:windowLightNavigationBar">@bool/light_navigation_bar</item>
</style>
</resources>

View File

@@ -279,4 +279,7 @@
<string name="download_slowdown_summary">Helps avoid blocking your IP address</string> <string name="download_slowdown_summary">Helps avoid blocking your IP address</string>
<string name="local_manga_processing">Saved manga processing</string> <string name="local_manga_processing">Saved manga processing</string>
<string name="chapters_will_removed_background">Chapters will be removed in the background. It can take some time</string> <string name="chapters_will_removed_background">Chapters will be removed in the background. It can take some time</string>
<string name="hide">Hide</string>
<string name="new_sources_text">New manga sources are available</string>
<string name="empty_favourite_categories">No favourite categories</string>
</resources> </resources>

View File

@@ -22,8 +22,8 @@
<!-- Bottom sheet --> <!-- Bottom sheet -->
<style name="ThemeOverlay.Kotatsu.BottomSheetDialog" parent="ThemeOverlay.Material3.BottomSheetDialog"> <style name="ThemeOverlay.Kotatsu.BottomSheetDialog" parent="ThemeOverlay.Material3.DayNight.BottomSheetDialog">
<item name="android:navigationBarColor">?colorSurfaceVariant</item> <item name="android:statusBarColor">@color/dim</item>
</style> </style>
<!-- Widget styles --> <!-- Widget styles -->