From 3cf2c580580c68949a8f8d32108c8730b4323fe0 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Sat, 24 Feb 2024 12:13:32 +0200 Subject: [PATCH] Local manga info dialog --- .../kotatsu/core/prefs/AppSettings.kt | 2 +- .../core/ui/dialog/RecyclerViewAlertDialog.kt | 8 ++ .../kotatsu/core/ui/image/FaviconDrawable.kt | 8 +- .../org/koitharu/kotatsu/core/util/Colors.kt | 41 +++++++ .../kotatsu/details/ui/DetailsFragment.kt | 6 ++ .../details/ui/DownloadDialogHelper.kt | 4 + .../kotatsu/local/ui/info/LocalInfoDialog.kt | 100 ++++++++++++++++++ .../local/ui/info/LocalInfoViewModel.kt | 41 +++++++ .../userdata/StorageUsagePreference.kt | 30 +----- app/src/main/res/layout/dialog_local_info.xml | 54 ++++++++++ .../main/res/layout/layout_details_info.xml | 1 + app/src/main/res/values/strings.xml | 3 +- 12 files changed, 264 insertions(+), 34 deletions(-) create mode 100644 app/src/main/kotlin/org/koitharu/kotatsu/core/util/Colors.kt create mode 100644 app/src/main/kotlin/org/koitharu/kotatsu/local/ui/info/LocalInfoDialog.kt create mode 100644 app/src/main/kotlin/org/koitharu/kotatsu/local/ui/info/LocalInfoViewModel.kt create mode 100644 app/src/main/res/layout/dialog_local_info.xml diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/AppSettings.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/AppSettings.kt index 931211d43..c1df00a17 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/AppSettings.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/AppSettings.kt @@ -430,7 +430,7 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) { fun getPagesSaveDir(context: Context): DocumentFile? = prefs.getString(KEY_PAGES_SAVE_DIR, null)?.toUriOrNull()?.let { - DocumentFile.fromTreeUri(context, it) + DocumentFile.fromTreeUri(context, it)?.takeIf { it.canWrite() } } fun setPagesSaveDir(uri: Uri?) { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/dialog/RecyclerViewAlertDialog.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/dialog/RecyclerViewAlertDialog.kt index 3199138e4..f5acf5d36 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/dialog/RecyclerViewAlertDialog.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/dialog/RecyclerViewAlertDialog.kt @@ -68,6 +68,14 @@ class RecyclerViewAlertDialog private constructor( return this } + fun setNeutralButton( + @StringRes textId: Int, + listener: DialogInterface.OnClickListener, + ): Builder { + delegate.setNeutralButton(textId, listener) + return this + } + fun setCancelable(isCancelable: Boolean): Builder { delegate.setCancelable(isCancelable) return this diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/image/FaviconDrawable.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/image/FaviconDrawable.kt index 492ee9f97..24c4b9461 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/image/FaviconDrawable.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/image/FaviconDrawable.kt @@ -16,6 +16,7 @@ import androidx.core.graphics.ColorUtils import androidx.core.graphics.withClip import com.google.android.material.color.MaterialColors import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.core.util.Colors import kotlin.math.absoluteValue class FaviconDrawable( @@ -44,7 +45,7 @@ class FaviconDrawable( } paint.textAlign = Paint.Align.CENTER paint.isFakeBoldText = true - colorForeground = MaterialColors.harmonize(colorOfString(name), colorBackground) + colorForeground = MaterialColors.harmonize(Colors.random(name), colorBackground) } override fun draw(canvas: Canvas) { @@ -104,9 +105,4 @@ class FaviconDrawable( paint.getTextBounds(text, 0, text.length, tempRect) return testTextSize * width / tempRect.width() } - - private fun colorOfString(str: String): Int { - val hue = (str.hashCode() % 360).absoluteValue.toFloat() - return ColorUtils.HSLToColor(floatArrayOf(hue, 0.5f, 0.5f)) - } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/Colors.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/Colors.kt new file mode 100644 index 000000000..3ad46e935 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/Colors.kt @@ -0,0 +1,41 @@ +package org.koitharu.kotatsu.core.util + +import android.content.Context +import androidx.annotation.AttrRes +import androidx.annotation.ColorInt +import androidx.core.graphics.ColorUtils +import com.google.android.material.R +import com.google.android.material.color.MaterialColors +import org.koitharu.kotatsu.core.util.ext.getThemeColor +import kotlin.math.absoluteValue + +object Colors { + + @ColorInt + fun segmentColor(context: Context, @AttrRes resId: Int): Int { + val colorHex = String.format("%06x", context.getThemeColor(resId)) + val hue = getHue(colorHex) + val color = ColorUtils.HSLToColor(floatArrayOf(hue, 0.5f, 0.5f)) + val backgroundColor = context.getThemeColor(R.attr.colorSurfaceContainerHigh) + return MaterialColors.harmonize(color, backgroundColor) + } + + fun random(seed: Any): Int { + val hue = (seed.hashCode() % 360).absoluteValue.toFloat() + return ColorUtils.HSLToColor(floatArrayOf(hue, 0.5f, 0.5f)) + } + + private fun getHue(hex: String): Float { + val r = (hex.substring(0, 2).toInt(16)).toFloat() + val g = (hex.substring(2, 4).toInt(16)).toFloat() + val b = (hex.substring(4, 6).toInt(16)).toFloat() + + var hue = 0F + if ((r >= g) && (g >= b)) { + hue = 60 * (g - b) / (r - b) + } else if ((g > r) && (r >= b)) { + hue = 60 * (2 - (r - b) / (g - b)) + } + return hue + } +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsFragment.kt index 2533b0874..d1aa92a2b 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsFragment.kt @@ -62,6 +62,7 @@ import org.koitharu.kotatsu.list.ui.adapter.mangaGridItemAD import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.MangaItemModel import org.koitharu.kotatsu.list.ui.size.StaticItemSizeResolver +import org.koitharu.kotatsu.local.ui.info.LocalInfoDialog import org.koitharu.kotatsu.main.ui.owners.NoModalBottomSheetOwner import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaSource @@ -102,6 +103,7 @@ class DetailsFragment : binding.buttonScrobblingMore.setOnClickListener(this) binding.buttonRelatedMore.setOnClickListener(this) binding.infoLayout.textViewSource.setOnClickListener(this) + binding.infoLayout.textViewSize.setOnClickListener(this) binding.textViewDescription.addOnLayoutChangeListener(this) binding.textViewDescription.viewTreeObserver.addOnDrawListener(this) binding.textViewDescription.movementMethod = LinkMovementMethodCompat.getInstance() @@ -324,6 +326,10 @@ class DetailsFragment : ) } + R.id.textView_size -> { + LocalInfoDialog.show(parentFragmentManager, manga) + } + R.id.imageView_cover -> { startActivity( ImageActivity.newIntent( diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DownloadDialogHelper.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DownloadDialogHelper.kt index df456252f..112382d2f 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DownloadDialogHelper.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DownloadDialogHelper.kt @@ -8,6 +8,7 @@ import org.koitharu.kotatsu.core.ui.dialog.RecyclerViewAlertDialog import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener import org.koitharu.kotatsu.download.ui.dialog.DownloadOption import org.koitharu.kotatsu.download.ui.dialog.downloadOptionAD +import org.koitharu.kotatsu.settings.SettingsActivity class DownloadDialogHelper( private val host: View, @@ -57,6 +58,9 @@ class DownloadDialogHelper( .setCancelable(true) .setTitle(R.string.download) .setNegativeButton(android.R.string.cancel) + .setNeutralButton(R.string.settings) { _, _ -> + host.context.startActivity(SettingsActivity.newDownloadsSettingsIntent(host.context)) + } .setItems(options) .create() .also { it.show() } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/local/ui/info/LocalInfoDialog.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/ui/info/LocalInfoDialog.kt new file mode 100644 index 000000000..95ae313f6 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/local/ui/info/LocalInfoDialog.kt @@ -0,0 +1,100 @@ +package org.koitharu.kotatsu.local.ui.info + +import android.content.res.ColorStateList +import android.os.Bundle +import android.os.Parcelable +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.annotation.AttrRes +import androidx.annotation.ColorInt +import androidx.annotation.StringRes +import androidx.core.graphics.ColorUtils +import androidx.core.widget.TextViewCompat +import androidx.fragment.app.FragmentManager +import androidx.fragment.app.viewModels +import com.google.android.material.color.MaterialColors +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.flow.combine +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga +import org.koitharu.kotatsu.core.ui.AlertDialogFragment +import org.koitharu.kotatsu.core.ui.widgets.SegmentedBarView +import org.koitharu.kotatsu.core.util.Colors +import org.koitharu.kotatsu.core.util.FileSize +import org.koitharu.kotatsu.core.util.ext.combine +import org.koitharu.kotatsu.core.util.ext.getThemeColor +import org.koitharu.kotatsu.core.util.ext.observe +import org.koitharu.kotatsu.core.util.ext.showDistinct +import org.koitharu.kotatsu.core.util.ext.withArgs +import org.koitharu.kotatsu.databinding.DialogLocalInfoBinding +import org.koitharu.kotatsu.parsers.model.Manga +import org.koitharu.kotatsu.settings.userdata.StorageUsage +import com.google.android.material.R as materialR + +@AndroidEntryPoint +class LocalInfoDialog : AlertDialogFragment() { + + private val viewModel: LocalInfoViewModel by viewModels() + + override fun onBuildDialog(builder: MaterialAlertDialogBuilder): MaterialAlertDialogBuilder { + return super.onBuildDialog(builder) + .setTitle(R.string.saved_manga) + .setNegativeButton(R.string.close, null) + } + + override fun onCreateViewBinding(inflater: LayoutInflater, container: ViewGroup?): DialogLocalInfoBinding { + return DialogLocalInfoBinding.inflate(inflater, container, false) + } + + override fun onViewBindingCreated(binding: DialogLocalInfoBinding, savedInstanceState: Bundle?) { + super.onViewBindingCreated(binding, savedInstanceState) + viewModel.path.observe(this) { + binding.textViewPath.text = it + } + combine(viewModel.size, viewModel.availableSize, ::Pair).observe(this) { + if (it.first >= 0 && it.second >= 0) { + setSegments(it.first, it.second) + } else { + binding.barView.animateSegments(emptyList()) + } + } + } + + private fun setSegments(size: Long, available: Long) { + val view = viewBinding?.barView ?: return + val total = size + available + val segment = SegmentedBarView.Segment( + percent = (size.toDouble() / total.toDouble()).toFloat(), + color = Colors.segmentColor(view.context, materialR.attr.colorPrimary), + ) + requireViewBinding().labelUsed.text = view.context.getString( + R.string.memory_usage_pattern, + getString(R.string.this_manga), + FileSize.BYTES.format(view.context, size), + ) + requireViewBinding().labelAvailable.text = view.context.getString( + R.string.memory_usage_pattern, + getString(R.string.available), + FileSize.BYTES.format(view.context, available), + ) + TextViewCompat.setCompoundDrawableTintList( + requireViewBinding().labelUsed, + ColorStateList.valueOf(segment.color), + ) + view.animateSegments(listOf(segment)) + } + + companion object { + + const val ARG_MANGA = "manga" + private const val TAG = "LocalInfoDialog" + + fun show(fm: FragmentManager, manga: Manga) { + LocalInfoDialog().withArgs(1) { + putParcelable(ARG_MANGA, ParcelableManga(manga)) + }.showDistinct(fm, TAG) + } + } +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/local/ui/info/LocalInfoViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/ui/info/LocalInfoViewModel.kt new file mode 100644 index 000000000..81436b7ed --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/local/ui/info/LocalInfoViewModel.kt @@ -0,0 +1,41 @@ +package org.koitharu.kotatsu.local.ui.info + +import androidx.core.net.toFile +import androidx.core.net.toUri +import androidx.lifecycle.SavedStateHandle +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga +import org.koitharu.kotatsu.core.ui.BaseViewModel +import org.koitharu.kotatsu.core.ui.widgets.SegmentedBarView +import org.koitharu.kotatsu.core.util.ext.computeSize +import org.koitharu.kotatsu.core.util.ext.require +import org.koitharu.kotatsu.core.util.ext.toFileOrNull +import org.koitharu.kotatsu.local.data.LocalMangaRepository +import org.koitharu.kotatsu.local.data.LocalStorageManager +import javax.inject.Inject + +@HiltViewModel +class LocalInfoViewModel @Inject constructor( + savedStateHandle: SavedStateHandle, + private val localMangaRepository: LocalMangaRepository, + private val storageManager: LocalStorageManager, +) : BaseViewModel() { + + private val manga = savedStateHandle.require(LocalInfoDialog.ARG_MANGA).manga + + val path = MutableStateFlow(null) + val size = MutableStateFlow(-1L) + val availableSize = MutableStateFlow(-1L) + + init { + launchLoadingJob(Dispatchers.Default) { + val file = manga.url.toUri().toFileOrNull() ?: localMangaRepository.findSavedManga(manga)?.file + requireNotNull(file) + path.value = file.path + size.value = file.computeSize() + availableSize.value = storageManager.computeAvailableSize() + } + } +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/userdata/StorageUsagePreference.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/userdata/StorageUsagePreference.kt index 20f114981..bb072537a 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/settings/userdata/StorageUsagePreference.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/userdata/StorageUsagePreference.kt @@ -14,6 +14,7 @@ import com.google.android.material.color.MaterialColors import kotlinx.coroutines.flow.FlowCollector import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.ui.widgets.SegmentedBarView +import org.koitharu.kotatsu.core.util.Colors import org.koitharu.kotatsu.core.util.FileSize import org.koitharu.kotatsu.core.util.ext.getThemeColor import org.koitharu.kotatsu.databinding.PreferenceMemoryUsageBinding @@ -38,15 +39,15 @@ class StorageUsagePreference @JvmOverloads constructor( val binding = PreferenceMemoryUsageBinding.bind(holder.itemView) val storageSegment = SegmentedBarView.Segment( usage?.savedManga?.percent ?: 0f, - segmentColor(materialR.attr.colorPrimary), + Colors.segmentColor(context, materialR.attr.colorPrimary), ) val pagesSegment = SegmentedBarView.Segment( usage?.pagesCache?.percent ?: 0f, - segmentColor(materialR.attr.colorSecondary), + Colors.segmentColor(context, materialR.attr.colorSecondary), ) val otherSegment = SegmentedBarView.Segment( usage?.otherCache?.percent ?: 0f, - segmentColor(materialR.attr.colorTertiary), + Colors.segmentColor(context, materialR.attr.colorTertiary), ) with(binding) { @@ -81,27 +82,4 @@ class StorageUsagePreference @JvmOverloads constructor( context.getString(emptyResId) } } - - private fun getHue(hex: String): Float { - val r = (hex.substring(0, 2).toInt(16)).toFloat() - val g = (hex.substring(2, 4).toInt(16)).toFloat() - val b = (hex.substring(4, 6).toInt(16)).toFloat() - - var hue = 0F - if ((r >= g) && (g >= b)) { - hue = 60 * (g - b) / (r - b) - } else if ((g > r) && (r >= b)) { - hue = 60 * (2 - (r - b) / (g - b)) - } - return hue - } - - @ColorInt - private fun segmentColor(@AttrRes resId: Int): Int { - val colorHex = String.format("%06x", context.getThemeColor(resId)) - val hue = getHue(colorHex) - val color = ColorUtils.HSLToColor(floatArrayOf(hue, 0.5f, 0.5f)) - val backgroundColor = context.getThemeColor(materialR.attr.colorSurfaceContainerHigh) - return MaterialColors.harmonize(color, backgroundColor) - } } diff --git a/app/src/main/res/layout/dialog_local_info.xml b/app/src/main/res/layout/dialog_local_info.xml new file mode 100644 index 000000000..eecb46c07 --- /dev/null +++ b/app/src/main/res/layout/dialog_local_info.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/layout_details_info.xml b/app/src/main/res/layout/layout_details_info.xml index d32880195..9a6931d7b 100644 --- a/app/src/main/res/layout/layout_details_info.xml +++ b/app/src/main/res/layout/layout_details_info.xml @@ -66,6 +66,7 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" + android:background="@drawable/custom_selectable_item_background" android:visibility="gone" app:drawableTopCompat="@drawable/ic_storage" tools:text="1.8 GiB" diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 57fe834d9..bc7f8e99b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -598,4 +598,5 @@ Ask for the destination dir every time Default page save directory Remove from history - \ No newline at end of file + Location +