UI improvements

This commit is contained in:
Koitharu
2024-04-06 19:58:54 +03:00
parent 1bf01ca240
commit 7c4b254f08
17 changed files with 158 additions and 33 deletions

View File

@@ -14,6 +14,7 @@ import android.widget.TextView
import androidx.annotation.StringRes
import androidx.appcompat.widget.LinearLayoutCompat
import androidx.core.content.withStyledAttributes
import androidx.core.view.children
import androidx.core.widget.TextViewCompat
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.util.ext.getAnimationDuration
@@ -100,6 +101,11 @@ class ProgressButton @JvmOverloads constructor(
}
}
override fun setEnabled(enabled: Boolean) {
super.setEnabled(enabled)
children.forEach { it.isEnabled = enabled }
}
override fun onAnimationUpdate(animation: ValueAnimator) {
progress = animation.animatedValue as Float
invalidate()

View File

@@ -54,7 +54,8 @@ class DetailsLoadUseCase @Inject constructor(
} catch (e: IOException) {
local?.await()?.manga?.also { localManga ->
send(MangaDetails(localManga, null, localManga.description?.parseAsHtml(withImages = false), true))
} ?: throw e
}
throw e
}
}

View File

@@ -16,7 +16,6 @@ import android.view.ViewTreeObserver
import android.widget.Toast
import androidx.activity.viewModels
import androidx.appcompat.widget.PopupMenu
import androidx.core.content.ContextCompat
import androidx.core.graphics.Insets
import androidx.core.text.buildSpannedString
import androidx.core.text.inSpans
@@ -57,7 +56,6 @@ import org.koitharu.kotatsu.core.ui.widgets.ChipsView
import org.koitharu.kotatsu.core.util.FileSize
import org.koitharu.kotatsu.core.util.ViewBadge
import org.koitharu.kotatsu.core.util.ext.crossfade
import org.koitharu.kotatsu.core.util.ext.drawableStart
import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.getThemeColor
import org.koitharu.kotatsu.core.util.ext.ifNullOrEmpty
@@ -67,7 +65,6 @@ import org.koitharu.kotatsu.core.util.ext.observeEvent
import org.koitharu.kotatsu.core.util.ext.parentView
import org.koitharu.kotatsu.core.util.ext.scaleUpActivityOptionsOf
import org.koitharu.kotatsu.core.util.ext.setOnContextClickListenerCompat
import org.koitharu.kotatsu.core.util.ext.showDistinct
import org.koitharu.kotatsu.core.util.ext.source
import org.koitharu.kotatsu.core.util.ext.textAndVisible
import org.koitharu.kotatsu.databinding.ActivityDetailsNewBinding
@@ -486,14 +483,14 @@ class DetailsActivity2 :
ratingBar.isVisible = false
}
textViewState.apply {
manga.state?.let { state ->
textAndVisible = resources.getString(state.titleResId)
drawableStart = ContextCompat.getDrawable(context, state.iconResId)
} ?: run {
isVisible = false
}
manga.state?.let { state ->
textViewState.textAndVisible = resources.getString(state.titleResId)
imageViewState.setImageResource(state.iconResId)
} ?: run {
textViewState.isVisible = false
imageViewState.isVisible = false
}
if (manga.source == MangaSource.LOCAL || manga.source == MangaSource.DUMMY) {
infoLayout.chipSource.isVisible = false
} else {

View File

@@ -10,8 +10,8 @@ import android.view.ViewGroup
import androidx.core.graphics.Insets
import androidx.core.graphics.drawable.toBitmap
import androidx.core.view.isVisible
import androidx.core.view.marginBottom
import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding
import coil.ImageLoader
import coil.request.CachePolicy
import coil.request.ErrorResult
@@ -21,6 +21,7 @@ import coil.target.ViewTarget
import com.davemorrissey.labs.subscaleview.ImageSource
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.BaseActivity
import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.getDisplayIcon
@@ -42,27 +43,25 @@ class ImageActivity : BaseActivity<ActivityImageBinding>(), ImageRequest.Listene
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(ActivityImageBinding.inflate(layoutInflater))
supportActionBar?.run {
setDisplayHomeAsUpEnabled(true)
setDisplayShowTitleEnabled(false)
}
viewBinding.buttonBack.setOnClickListener(this)
loadImage(intent.data)
}
override fun onWindowInsetsChanged(insets: Insets) {
with(viewBinding.toolbar) {
updatePadding(
left = insets.left,
right = insets.right,
)
with(viewBinding.buttonBack) {
updateLayoutParams<ViewGroup.MarginLayoutParams> {
topMargin = insets.top
topMargin = insets.top + marginBottom
leftMargin = insets.left + marginBottom
rightMargin = insets.right + marginBottom
}
}
}
override fun onClick(v: View?) {
loadImage(intent.data)
override fun onClick(v: View) {
when (v.id) {
R.id.button_back -> dispatchNavigateUp()
else -> loadImage(intent.data)
}
}
override fun onError(request: ImageRequest, result: ErrorResult) {

View File

@@ -31,6 +31,7 @@ import org.koitharu.kotatsu.core.ui.list.ListSelectionController
import org.koitharu.kotatsu.core.ui.list.PaginationScrollListener
import org.koitharu.kotatsu.core.ui.list.fastscroll.FastScroller
import org.koitharu.kotatsu.core.ui.util.ReversibleActionObserver
import org.koitharu.kotatsu.core.ui.widgets.TipView
import org.koitharu.kotatsu.core.util.ShareHelper
import org.koitharu.kotatsu.core.util.ext.addMenuProvider
import org.koitharu.kotatsu.core.util.ext.findAppCompatDelegate
@@ -231,6 +232,10 @@ abstract class MangaListFragment :
override fun onListHeaderClick(item: ListHeader, view: View) = Unit
override fun onPrimaryButtonClick(tipView: TipView) = Unit
override fun onSecondaryButtonClick(tipView: TipView) = Unit
override fun onRetryClick(error: Throwable) {
resolveException(error)
}

View File

@@ -24,5 +24,6 @@ open class MangaListAdapter(
addDelegate(ListItemType.STATE_EMPTY, emptyStateListAD(coil, lifecycleOwner, listener))
addDelegate(ListItemType.HINT_EMPTY, emptyHintAD(coil, lifecycleOwner, listener))
addDelegate(ListItemType.HEADER, listHeaderAD(listener))
addDelegate(ListItemType.TIP, tipAD(listener))
}
}

View File

@@ -1,9 +1,11 @@
package org.koitharu.kotatsu.list.ui.adapter
import android.view.View
import org.koitharu.kotatsu.core.ui.widgets.TipView
import org.koitharu.kotatsu.parsers.model.MangaTag
interface MangaListListener : MangaDetailsClickListener, ListStateHolderListener, ListHeaderClickListener {
interface MangaListListener : MangaDetailsClickListener, ListStateHolderListener, ListHeaderClickListener,
TipView.OnButtonClickListener {
fun onUpdateFilter(tags: Set<MangaTag>)

View File

@@ -1,11 +1,16 @@
package org.koitharu.kotatsu.local.data
import android.Manifest
import android.content.ContentResolver
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.os.Environment
import android.os.StatFs
import androidx.annotation.WorkerThread
import androidx.core.content.ContextCompat
import androidx.core.net.toFile
import dagger.Reusable
import dagger.hilt.android.qualifiers.ApplicationContext
@@ -101,6 +106,23 @@ class LocalStorageManager @Inject constructor(
contentResolver.takePersistableUriPermission(uri, flags)
}
fun isOnExternalStorage(file: File): Boolean {
return !file.absolutePath.contains(context.packageName)
}
fun hasExternalStoragePermission(isReadOnly: Boolean): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
Environment.isExternalStorageManager()
} else {
val permission = if (isReadOnly) {
Manifest.permission.READ_EXTERNAL_STORAGE
} else {
Manifest.permission.WRITE_EXTERNAL_STORAGE
}
ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED
}
}
suspend fun getDirectoryDisplayName(dir: File, isFullPath: Boolean): String = runInterruptible(Dispatchers.IO) {
val packageName = context.packageName
if (dir.absolutePath.contains(packageName)) {

View File

@@ -1,9 +1,12 @@
package org.koitharu.kotatsu.local.ui
import android.Manifest
import android.os.Build
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import android.view.View
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.view.ActionMode
import androidx.core.net.toFile
import androidx.core.net.toUri
@@ -12,9 +15,11 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.list.ListSelectionController
import org.koitharu.kotatsu.core.ui.widgets.TipView
import org.koitharu.kotatsu.core.util.ShareHelper
import org.koitharu.kotatsu.core.util.ext.addMenuProvider
import org.koitharu.kotatsu.core.util.ext.observeEvent
import org.koitharu.kotatsu.core.util.ext.tryLaunch
import org.koitharu.kotatsu.core.util.ext.withArgs
import org.koitharu.kotatsu.databinding.FragmentListBinding
import org.koitharu.kotatsu.filter.ui.FilterOwner
@@ -23,9 +28,23 @@ import org.koitharu.kotatsu.filter.ui.sheet.FilterSheetFragment
import org.koitharu.kotatsu.list.ui.MangaListFragment
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.remotelist.ui.RemoteListFragment
import org.koitharu.kotatsu.settings.storage.RequestStorageManagerPermissionContract
import org.koitharu.kotatsu.settings.storage.directories.MangaDirectoriesActivity
class LocalListFragment : MangaListFragment(), FilterOwner {
private val permissionRequestLauncher = registerForActivityResult(
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
RequestStorageManagerPermissionContract()
} else {
ActivityResultContracts.RequestPermission()
},
) {
if (it) {
viewModel.onRefresh()
}
}
init {
withArgs(1) {
putSerializable(
@@ -54,6 +73,16 @@ class LocalListFragment : MangaListFragment(), FilterOwner {
FilterSheetFragment.show(childFragmentManager)
}
override fun onPrimaryButtonClick(tipView: TipView) {
if (!permissionRequestLauncher.tryLaunch(Manifest.permission.READ_EXTERNAL_STORAGE)) {
Snackbar.make(tipView, R.string.operation_not_supported, Snackbar.LENGTH_SHORT).show()
}
}
override fun onSecondaryButtonClick(tipView: TipView) {
startActivity(MangaDirectoriesActivity.newIntent(tipView.context))
}
override fun onScrolledToEnd() = viewModel.loadNextPage()
override fun onCreateActionMode(

View File

@@ -10,12 +10,18 @@ import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
import org.koitharu.kotatsu.core.util.ext.call
import org.koitharu.kotatsu.core.util.ext.toFileOrNull
import org.koitharu.kotatsu.core.util.ext.toUriOrNull
import org.koitharu.kotatsu.download.ui.worker.DownloadWorker
import org.koitharu.kotatsu.explore.domain.ExploreRepository
import org.koitharu.kotatsu.filter.ui.FilterCoordinator
import org.koitharu.kotatsu.list.domain.ListExtraProvider
import org.koitharu.kotatsu.list.ui.model.EmptyState
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.list.ui.model.MangaItemModel
import org.koitharu.kotatsu.list.ui.model.TipModel
import org.koitharu.kotatsu.local.data.LocalStorageChanges
import org.koitharu.kotatsu.local.data.LocalStorageManager
import org.koitharu.kotatsu.local.domain.DeleteLocalMangaUseCase
import org.koitharu.kotatsu.local.domain.model.LocalManga
import org.koitharu.kotatsu.remotelist.ui.RemoteListViewModel
@@ -32,6 +38,7 @@ class LocalListViewModel @Inject constructor(
private val deleteLocalMangaUseCase: DeleteLocalMangaUseCase,
exploreRepository: ExploreRepository,
@LocalStorageChanges private val localStorageChanges: SharedFlow<LocalManga?>,
private val localStorageManager: LocalStorageManager,
) : RemoteListViewModel(
savedStateHandle,
mangaRepositoryFactory,
@@ -54,6 +61,31 @@ class LocalListViewModel @Inject constructor(
settings.subscribe(this)
}
override suspend fun onBuildList(list: MutableList<ListModel>) {
super.onBuildList(list)
if (localStorageManager.hasExternalStoragePermission(isReadOnly = true)) {
return
}
for (item in list) {
if (item !is MangaItemModel) {
continue
}
val file = item.manga.url.toUriOrNull()?.toFileOrNull() ?: continue
if (localStorageManager.isOnExternalStorage(file)) {
val tip = TipModel(
key = "permission",
title = R.string.external_storage,
text = R.string.missing_storage_permission,
icon = R.drawable.ic_storage,
primaryButtonText = R.string.fix,
secondaryButtonText = R.string.settings,
)
list.add(0, tip)
return
}
}
}
override fun onCleared() {
settings.unsubscribe(this)
super.onCleared()

View File

@@ -32,6 +32,7 @@ import org.koitharu.kotatsu.filter.ui.MangaFilter
import org.koitharu.kotatsu.list.domain.ListExtraProvider
import org.koitharu.kotatsu.list.ui.MangaListViewModel
import org.koitharu.kotatsu.list.ui.model.EmptyState
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.list.ui.model.LoadingFooter
import org.koitharu.kotatsu.list.ui.model.LoadingState
import org.koitharu.kotatsu.list.ui.model.toErrorFooter
@@ -89,6 +90,7 @@ open class RemoteListViewModel @Inject constructor(
}
}
}
onBuildList(this)
}
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, listOf(LoadingState))
@@ -166,6 +168,8 @@ open class RemoteListViewModel @Inject constructor(
actionStringRes = if (canResetFilter) R.string.reset_filter else 0,
)
protected open suspend fun onBuildList(list: MutableList<ListModel>) = Unit
fun openRandom() {
if (randomJob?.isActive == true) {
return

View File

@@ -18,6 +18,7 @@ import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.ui.BaseActivity
import org.koitharu.kotatsu.core.ui.list.ListSelectionController
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.ui.widgets.TipView
import org.koitharu.kotatsu.core.util.ShareHelper
import org.koitharu.kotatsu.core.util.ext.invalidateNestedItemDecorations
import org.koitharu.kotatsu.core.util.ext.observe
@@ -140,6 +141,10 @@ class MultiSearchActivity :
override fun onListHeaderClick(item: ListHeader, view: View) = Unit
override fun onPrimaryButtonClick(tipView: TipView) = Unit
override fun onSecondaryButtonClick(tipView: TipView) = Unit
override fun onSelectionChanged(controller: ListSelectionController, count: Int) {
viewBinding.recyclerView.invalidateNestedItemDecorations()
}

View File

@@ -15,6 +15,7 @@ import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver
import org.koitharu.kotatsu.core.ui.BaseFragment
import org.koitharu.kotatsu.core.ui.list.PaginationScrollListener
import org.koitharu.kotatsu.core.ui.widgets.TipView
import org.koitharu.kotatsu.core.util.ext.addMenuProvider
import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.observeEvent
@@ -100,6 +101,10 @@ class FeedFragment :
override fun onEmptyActionClick() = Unit
override fun onPrimaryButtonClick(tipView: TipView) = Unit
override fun onSecondaryButtonClick(tipView: TipView) = Unit
override fun onListHeaderClick(item: ListHeader, view: View) {
val context = view.context
context.startActivity(UpdatesActivity.newIntent(context))

View File

@@ -1,10 +1,12 @@
package org.koitharu.kotatsu.tracker.ui.feed.adapter
import androidx.core.content.ContextCompat
import androidx.lifecycle.LifecycleOwner
import coil.ImageLoader
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.util.ext.drawableStart
import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.core.util.ext.source
@@ -20,14 +22,13 @@ fun feedItemAD(
) = adapterDelegateViewBinding<FeedItem, ListModel, ItemFeedBinding>(
{ inflater, parent -> ItemFeedBinding.inflate(inflater, parent, false) },
) {
val indicatorNew = ContextCompat.getDrawable(context, R.drawable.ic_new)
itemView.setOnClickListener {
clickListener.onItemClick(item.manga, it)
}
bind {
val alpha = if (item.isNew) 1f else 0.5f
binding.textViewTitle.alpha = alpha
binding.textViewSummary.alpha = alpha
binding.imageViewCover.newImageRequest(lifecycleOwner, item.imageUrl)?.run {
placeholder(R.drawable.ic_placeholder)
fallback(R.drawable.ic_placeholder)
@@ -42,5 +43,10 @@ fun feedItemAD(
item.count,
item.count,
)
binding.textViewSummary.drawableStart = if (item.isNew) {
indicatorNew
} else {
null
}
}
}

View File

@@ -2,6 +2,7 @@
<FrameLayout
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="match_parent">
@@ -9,12 +10,19 @@
android:id="@+id/ssiv"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:restoreStrategy="deferred" />
app:restoreStrategy="deferred"
tools:background="@tools:sample/backgrounds/scenic" />
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<ImageButton
android:id="@+id/button_back"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_margin="@dimen/screen_padding"
android:background="@drawable/bg_circle_button"
android:contentDescription="@string/back"
android:elevation="4dp"
android:scaleType="center"
android:src="?homeAsUpIndicator" />
<com.google.android.material.progressindicator.CircularProgressIndicator
android:id="@+id/progressBar"

View File

@@ -43,6 +43,7 @@
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="8dp"
android:drawablePadding="8dp"
android:ellipsize="end"
android:maxLines="1"
android:textAppearance="?attr/textAppearanceBodySmall"

View File

@@ -649,4 +649,6 @@
<string name="minutes_short">%d m</string>
<!-- Short hours and minutes format pattern -->
<string name="hours_minutes_short">%1$d h %2$d m</string>
<string name="fix">Fix</string>
<string name="missing_storage_permission">There is no permission to access manga on external storage</string>
</resources>