Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
10c03ff01a | ||
|
|
e85b9db118 | ||
|
|
f6b0a7c780 | ||
|
|
fa150e45ff | ||
|
|
de9c1017b3 | ||
|
|
2709d40fc0 | ||
|
|
45b42ad5bd | ||
|
|
b759f8d0a0 | ||
|
|
23e7aa2aaa | ||
|
|
fdd4f5abca | ||
|
|
c695468aec | ||
|
|
9166716f2a | ||
|
|
3407e74e99 | ||
|
|
4c5314fe59 | ||
|
|
96be49aa83 | ||
|
|
28b556121b | ||
|
|
558c19e526 | ||
|
|
59c2d20311 | ||
|
|
fa1f2cbf51 | ||
|
|
de8739f143 | ||
|
|
9aa28f6fd2 | ||
|
|
a2b1699047 | ||
|
|
2dce65a448 | ||
|
|
3d68d7c818 | ||
|
|
4987d43042 | ||
|
|
684b494edb | ||
|
|
714b708fa9 | ||
|
|
c462c19a8b |
4
.github/ISSUE_TEMPLATE/report_issue.yml
vendored
4
.github/ISSUE_TEMPLATE/report_issue.yml
vendored
@@ -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
|
||||||
2
.github/ISSUE_TEMPLATE/request_feature.yml
vendored
2
.github/ISSUE_TEMPLATE/request_feature.yml
vendored
@@ -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
1
.gitignore
vendored
@@ -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
|
||||||
|
|||||||
17
.idea/deploymentTargetDropDown.xml
generated
17
.idea/deploymentTargetDropDown.xml
generated
@@ -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>
|
|
||||||
@@ -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'
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package org.koitharu.kotatsu.favourites.ui.categories
|
||||||
|
|
||||||
|
interface AllCategoriesToggleListener {
|
||||||
|
|
||||||
|
fun onAllCategoriesToggle(isVisible: Boolean)
|
||||||
|
}
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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()) }
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
@@ -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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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") {
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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()) }
|
||||||
}
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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(
|
||||||
|
|||||||
12
app/src/main/res/drawable/ic_hidden.xml
Normal file
12
app/src/main/res/drawable/ic_hidden.xml
Normal 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>
|
||||||
22
app/src/main/res/drawable/ic_launcher_monochrome.xml
Normal file
22
app/src/main/res/drawable/ic_launcher_monochrome.xml
Normal 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>
|
||||||
12
app/src/main/res/drawable/ic_shown.xml
Normal file
12
app/src/main/res/drawable/ic_shown.xml
Normal 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>
|
||||||
5
app/src/main/res/drawable/ic_shown_hidden.xml
Normal file
5
app/src/main/res/drawable/ic_shown_hidden.xml
Normal 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>
|
||||||
@@ -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" />
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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>
|
||||||
33
app/src/main/res/layout/item_categories_all.xml
Normal file
33
app/src/main/res/layout/item_categories_all.xml
Normal 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>
|
||||||
@@ -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" />
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
7
app/src/main/res/values-v27/styles.xml
Normal file
7
app/src/main/res/values-v27/styles.xml
Normal 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>
|
||||||
@@ -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>
|
||||||
@@ -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 -->
|
||||||
|
|||||||
Reference in New Issue
Block a user