diff --git a/app/src/main/java/dev/arkbuilders/navigator/di/AppComponent.kt b/app/src/main/java/dev/arkbuilders/navigator/di/AppComponent.kt index 4765d138..8f696c33 100644 --- a/app/src/main/java/dev/arkbuilders/navigator/di/AppComponent.kt +++ b/app/src/main/java/dev/arkbuilders/navigator/di/AppComponent.kt @@ -3,10 +3,13 @@ package dev.arkbuilders.navigator.di import android.content.Context import dagger.BindsInstance import dagger.Component +import dev.arkbuilders.arkfilepicker.folders.FoldersRepo +import dev.arkbuilders.navigator.analytics.AnalyticsModule import dev.arkbuilders.navigator.data.StorageBackup import dev.arkbuilders.navigator.data.preferences.Preferences import dev.arkbuilders.navigator.di.modules.AppModule import dev.arkbuilders.navigator.di.modules.CiceroneModule +import dev.arkbuilders.navigator.di.modules.DispatcherModule import dev.arkbuilders.navigator.di.modules.RepoModule import dev.arkbuilders.navigator.presentation.App import dev.arkbuilders.navigator.presentation.dialog.ExplainPermsDialog @@ -17,17 +20,12 @@ import dev.arkbuilders.navigator.presentation.dialog.sort.SortDialogPresenter import dev.arkbuilders.navigator.presentation.dialog.tagssort.TagsSortDialogFragment import dev.arkbuilders.navigator.presentation.screen.folders.FoldersFragment import dev.arkbuilders.navigator.presentation.screen.gallery.GalleryFragment -import dev.arkbuilders.navigator.presentation.screen.gallery.GalleryPresenter -import dev.arkbuilders.navigator.presentation.screen.gallery.previewpager.PreviewImageViewHolder import dev.arkbuilders.navigator.presentation.screen.main.MainActivity import dev.arkbuilders.navigator.presentation.screen.resources.ResourcesFragment import dev.arkbuilders.navigator.presentation.screen.resources.ResourcesPresenter import dev.arkbuilders.navigator.presentation.screen.resources.adapter.FileItemViewHolder import dev.arkbuilders.navigator.presentation.screen.resources.adapter.ResourcesGridPresenter import dev.arkbuilders.navigator.presentation.screen.settings.SettingsFragment -import dev.arkbuilders.arkfilepicker.folders.FoldersRepo -import dev.arkbuilders.navigator.analytics.AnalyticsModule -import dev.arkbuilders.navigator.di.modules.DispatcherModule import javax.inject.Singleton @Singleton @@ -48,13 +46,12 @@ interface AppComponent { fun inject(foldersFragment: FoldersFragment) fun inject(resourcesPresenter: ResourcesPresenter) fun inject(resourcesFragment: ResourcesFragment) - fun inject(galleryPresenter: GalleryPresenter) fun inject(galleryFragment: GalleryFragment) + fun inject(settingsFragment: SettingsFragment) fun inject(resourcesGridPresenter: ResourcesGridPresenter) fun inject(editTagsDialogPresenter: EditTagsDialogPresenter) fun inject(fileItemViewHolder: FileItemViewHolder) - fun inject(previewImageViewHolder: PreviewImageViewHolder) fun inject(sortDialogPresenter: SortDialogPresenter) fun inject(tagsSortDialogFragment: TagsSortDialogFragment) fun inject(rootPickerDialogFragment: RootPickerDialogFragment) diff --git a/app/src/main/java/dev/arkbuilders/navigator/domain/HandleGalleryExternalChangesUseCase.kt b/app/src/main/java/dev/arkbuilders/navigator/domain/HandleGalleryExternalChangesUseCase.kt deleted file mode 100644 index 9dce8cc0..00000000 --- a/app/src/main/java/dev/arkbuilders/navigator/domain/HandleGalleryExternalChangesUseCase.kt +++ /dev/null @@ -1,58 +0,0 @@ -package dev.arkbuilders.navigator.domain - -import androidx.recyclerview.widget.DiffUtil -import dev.arkbuilders.navigator.presentation.screen.gallery.GalleryPresenter -import dev.arkbuilders.navigator.presentation.screen.resources.adapter.ResourceDiffUtilCallback -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.cancel -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import moxy.presenterScope -import javax.inject.Inject - -class HandleGalleryExternalChangesUseCase @Inject constructor() { - operator fun invoke( - presenter: GalleryPresenter - ) = with(presenter) { - presenterScope.launch { - withContext(Dispatchers.Main) { - viewState.setProgressVisibility(true, "Changes detected, indexing") - } - - index.updateAll() - - withContext(Dispatchers.Main) { - viewState.notifyResourcesChanged() - } - - presenterScope.launch { - metadataStorage.busy.collect { busy -> - if (!busy) { - cancel() - } - } - }.join() - - val newItems = provideGalleryItems() - if (newItems.isEmpty()) { - onBackClick() - return@launch - } - - diffResult = DiffUtil.calculateDiff( - ResourceDiffUtilCallback( - galleryItems.map { it.resource.id }, - newItems.map { it.resource.id } - ) - ) - - galleryItems = newItems.toMutableList() - - withContext(Dispatchers.Main) { - viewState.updatePagerAdapterWithDiff() - viewState.notifyCurrentItemChanged() - viewState.setProgressVisibility(false) - } - } - } -} diff --git a/app/src/main/java/dev/arkbuilders/navigator/presentation/App.kt b/app/src/main/java/dev/arkbuilders/navigator/presentation/App.kt index 32cbf108..ee9da216 100644 --- a/app/src/main/java/dev/arkbuilders/navigator/presentation/App.kt +++ b/app/src/main/java/dev/arkbuilders/navigator/presentation/App.kt @@ -1,7 +1,6 @@ package dev.arkbuilders.navigator.presentation import android.app.Application -import android.os.StrictMode import dev.arkbuilders.arkfilepicker.folders.FoldersRepo import dev.arkbuilders.arklib.initArkLib import dev.arkbuilders.arklib.initRustLogger @@ -30,9 +29,9 @@ class App : Application() { private set init { - if (BuildConfig.DEBUG) { - StrictMode.enableDefaults() - } +// if (BuildConfig.DEBUG) { +// StrictMode.enableDefaults() +// } } override fun onCreate() { diff --git a/app/src/main/java/dev/arkbuilders/navigator/presentation/navigation/Screens.kt b/app/src/main/java/dev/arkbuilders/navigator/presentation/navigation/Screens.kt index 8b7ec03d..290864d6 100644 --- a/app/src/main/java/dev/arkbuilders/navigator/presentation/navigation/Screens.kt +++ b/app/src/main/java/dev/arkbuilders/navigator/presentation/navigation/Screens.kt @@ -1,13 +1,13 @@ package dev.arkbuilders.navigator.presentation.navigation -import ru.terrakok.cicerone.android.support.SupportAppScreen import dev.arkbuilders.arkfilepicker.folders.RootAndFav import dev.arkbuilders.arklib.ResourceId +import dev.arkbuilders.arklib.user.tags.Tag import dev.arkbuilders.navigator.presentation.screen.folders.FoldersFragment import dev.arkbuilders.navigator.presentation.screen.gallery.GalleryFragment import dev.arkbuilders.navigator.presentation.screen.resources.ResourcesFragment import dev.arkbuilders.navigator.presentation.screen.settings.SettingsFragment -import dev.arkbuilders.arklib.user.tags.Tag +import ru.terrakok.cicerone.android.support.SupportAppScreen class Screens { class FoldersScreen : SupportAppScreen() { diff --git a/app/src/main/java/dev/arkbuilders/navigator/presentation/screen/gallery/GalleryFragment.kt b/app/src/main/java/dev/arkbuilders/navigator/presentation/screen/gallery/GalleryFragment.kt index c9fa5e5f..69ad7996 100644 --- a/app/src/main/java/dev/arkbuilders/navigator/presentation/screen/gallery/GalleryFragment.kt +++ b/app/src/main/java/dev/arkbuilders/navigator/presentation/screen/gallery/GalleryFragment.kt @@ -6,25 +6,38 @@ import android.content.Intent import android.net.Uri import android.os.Bundle import android.util.TypedValue +import android.view.LayoutInflater import android.view.View +import android.view.ViewGroup import android.widget.Toast import androidx.activity.addCallback import androidx.core.content.FileProvider import androidx.core.os.bundleOf -import androidx.core.view.doOnNextLayout import androidx.core.view.isVisible +import androidx.fragment.app.Fragment import androidx.fragment.app.setFragmentResult +import androidx.fragment.app.viewModels import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.SimpleItemAnimator import androidx.viewpager2.widget.ViewPager2 import by.kirich1409.viewbindingdelegate.viewBinding import com.google.android.material.chip.Chip +import dev.arkbuilders.arkfilepicker.folders.RootAndFav +import dev.arkbuilders.arklib.ResourceId +import dev.arkbuilders.arklib.data.index.Resource +import dev.arkbuilders.arklib.data.index.ResourceIndex +import dev.arkbuilders.arklib.data.meta.Metadata +import dev.arkbuilders.arklib.user.tags.Tag +import dev.arkbuilders.arklib.user.tags.TagStorage +import dev.arkbuilders.arklib.user.tags.Tags +import dev.arkbuilders.arklib.utils.extension import dev.arkbuilders.components.databinding.ScoreWidgetBinding import dev.arkbuilders.components.scorewidget.ScoreWidget import dev.arkbuilders.navigator.BuildConfig import dev.arkbuilders.navigator.R -import dev.arkbuilders.navigator.data.utils.LogTags.GALLERY_SCREEN +import dev.arkbuilders.navigator.data.stats.StatsStorage +import dev.arkbuilders.navigator.data.utils.LogTags import dev.arkbuilders.navigator.databinding.FragmentGalleryBinding import dev.arkbuilders.navigator.databinding.PopupGalleryTagMenuBinding import dev.arkbuilders.navigator.presentation.App @@ -32,7 +45,10 @@ import dev.arkbuilders.navigator.presentation.dialog.DetailsAlertDialog import dev.arkbuilders.navigator.presentation.dialog.StorageExceptionDialogFragment import dev.arkbuilders.navigator.presentation.dialog.edittags.EditTagsDialogFragment import dev.arkbuilders.navigator.presentation.navigation.Screens -import dev.arkbuilders.navigator.presentation.screen.gallery.previewpager.PreviewsPager +import dev.arkbuilders.navigator.presentation.screen.gallery.domain.GallerySideEffect +import dev.arkbuilders.navigator.presentation.screen.gallery.domain.GalleryState +import dev.arkbuilders.navigator.presentation.screen.gallery.domain.ProgressState +import dev.arkbuilders.navigator.presentation.screen.gallery.pager.PreviewsPager import dev.arkbuilders.navigator.presentation.screen.main.MainActivity import dev.arkbuilders.navigator.presentation.utils.FullscreenHelper import dev.arkbuilders.navigator.presentation.utils.extra.ExtraLoader @@ -41,54 +57,48 @@ import dev.arkbuilders.navigator.presentation.utils.makeVisible import dev.arkbuilders.navigator.presentation.view.DefaultPopup import dev.arkbuilders.navigator.presentation.view.DepthPageTransformer import dev.arkbuilders.navigator.presentation.view.StackedToasts -import kotlinx.coroutines.launch -import moxy.MvpAppCompatFragment -import moxy.ktx.moxyPresenter -import dev.arkbuilders.arkfilepicker.folders.RootAndFav -import dev.arkbuilders.arklib.ResourceId -import dev.arkbuilders.arklib.data.index.Resource -import dev.arkbuilders.arklib.data.meta.Metadata -import dev.arkbuilders.arklib.user.tags.Tag -import dev.arkbuilders.arklib.user.tags.Tags -import dev.arkbuilders.arklib.utils.extension +import org.orbitmvi.orbit.viewmodel.observe import timber.log.Timber import java.nio.file.Path +import javax.inject.Inject import kotlin.system.measureTimeMillis -class GalleryFragment : - MvpAppCompatFragment(R.layout.fragment_gallery), GalleryView { - +class GalleryFragment : Fragment() { private val binding by viewBinding(FragmentGalleryBinding::bind) - private val presenter by moxyPresenter { - GalleryPresenter( - requireArguments()[ROOT_AND_FAV_KEY] as RootAndFav, - requireArguments().getParcelableArray(RESOURCES_KEY)!!.toList() - as List, - requireArguments().getInt(START_AT_KEY), - requireArguments().getBoolean(SELECTING_ENABLED_KEY), - ( - requireArguments().getParcelableArray(SELECTED_RESOURCES_KEY)!! - .toList() as List - ) - .toMutableList() + @Inject + lateinit var factory: GalleryViewModelFactory.Factory + private val viewModel: GalleryViewModel by viewModels { + factory.create( + startPos = requireArguments().getInt(START_AT_KEY), + selectingEnabled = requireArguments().getBoolean(SELECTING_ENABLED_KEY), + selectedResources = requireArguments() + .getParcelableArray(SELECTED_RESOURCES_KEY)!! + .toList() as List, + rootAndFav = requireArguments()[ROOT_AND_FAV_KEY] as RootAndFav, + resourcesIds = requireArguments().getParcelableArray(RESOURCES_KEY)!! + .toList() as List ).apply { - - Timber.d(GALLERY_SCREEN, "creating GalleryPresenter") - - App.instance.appComponent.inject(this) + App.instance.appComponent.inject(this@GalleryFragment) } } - private lateinit var stackedToasts: StackedToasts - private lateinit var pagerAdapter: PreviewsPager + private val scoreWidget by lazy { - ScoreWidget(presenter.scoreWidgetController, viewLifecycleOwner) + ScoreWidget(viewModel.scoreWidgetController, viewLifecycleOwner) } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - Timber.d(GALLERY_SCREEN, "view created in GalleryFragment") - super.onViewCreated(view, savedInstanceState) - App.instance.appComponent.inject(this) + private lateinit var stackedToasts: StackedToasts + private lateinit var pagerAdapter: PreviewsPager + + // avoid pointless update on render if there are no changes + private var cacheState: GalleryState? = null + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + return inflater.inflate(R.layout.fragment_gallery, container, false) } override fun onDestroyView() { @@ -96,9 +106,19 @@ class GalleryFragment : scoreWidget.onDestroyView() } - override fun init() { - Timber.d(GALLERY_SCREEN, "currentItem = ${binding.viewPager.currentItem}") + override fun onResume() { + super.onResume() + viewModel.onResume() + } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + Timber.d(LogTags.GALLERY_SCREEN, "view created in GalleryFragment") + App.instance.appComponent.inject(this) + super.onViewCreated(view, savedInstanceState) + Timber.d( + LogTags.GALLERY_SCREEN, + "currentItem = ${binding.viewPager.currentItem}" + ) animatePagerAppearance() initResultListener() stackedToasts = StackedToasts(binding.rvToasts, lifecycleScope) @@ -107,20 +127,18 @@ class GalleryFragment : (requireActivity() as MainActivity).setBottomNavigationVisibility(false) requireActivity().onBackPressedDispatcher.addCallback(this) { - presenter.onBackClick() + onBackClick() } - - pagerAdapter = PreviewsPager(requireContext(), presenter) + pagerAdapter = + PreviewsPager(lifecycleScope, requireContext(), viewModel) initViewPager() scoreWidget.init(ScoreWidgetBinding.bind(binding.scoreWidget)) - binding.apply { val selectingEnabled = requireArguments().getBoolean(SELECTING_ENABLED_KEY) layoutSelected.isVisible = selectingEnabled fabStartSelect.isVisible = !selectingEnabled - removeResourceFab.setOnClickListener { Toast.makeText( requireContext(), @@ -128,87 +146,151 @@ class GalleryFragment : Toast.LENGTH_SHORT ).show() } - removeResourceFab.setOnLongClickListener { val time = measureTimeMillis { - presenter.onRemoveFabClick() + viewModel.onRemoveFabClick() } - Timber.tag(GALLERY_SCREEN).d("${time / 1000L}s") + Timber.tag(LogTags.GALLERY_SCREEN).d("${time / 1000L}s") true } - infoResourceFab.setOnClickListener { - presenter.onInfoFabClick() + viewModel.onInfoFabClick() } shareResourceFab.setOnClickListener { - presenter.onShareFabClick() + viewModel.onShareFabClick() } fabStartSelect.setOnClickListener { - presenter.onSelectingChanged() + viewModel.onSelectingChanged(true) } openResourceFab.setOnClickListener { - presenter.onOpenFabClick() + viewModel.onOpenFabClick() } editResourceFab.setOnClickListener { - presenter.onEditFabClick() + viewModel.onEditFabClick() } layoutSelected.setOnClickListener { - presenter.onSelectBtnClick() + viewModel.onSelectBtnClick() } layoutSelected.setOnLongClickListener { - presenter.onSelectingChanged() + viewModel.onSelectingChanged(false) return@setOnLongClickListener true } } + viewModel.observe( + lifecycleOwner = this, + state = ::render, + sideEffect = ::handleSideEffect + ) } - override fun updatePagerAdapter() { - pagerAdapter.notifyDataSetChanged() - binding.viewPager.adapter?.itemCount?.let { count -> - val startAt = requireArguments().getInt(START_AT_KEY) - if (startAt < count) { - binding.viewPager.setCurrentItem( - startAt, - false + private fun handleSideEffect(sideEffect: GallerySideEffect) { + with(sideEffect) { + when (this) { + is GallerySideEffect.ScrollToPage -> { + binding.viewPager.adapter?.itemCount?.let { count -> + if (this.pos < count) { + binding.viewPager.setCurrentItem( + this.pos, + false + ) + } + } + } + + is GallerySideEffect.DisplayStorageException -> + displayStorageException( + label = label, + msg = messenger + ) + + is GallerySideEffect.EditResource -> editResource(path) + GallerySideEffect.NavigateBack -> onBackClick() + GallerySideEffect.NotifyCurrentItemChange -> + notifyCurrentItemChanged() + + GallerySideEffect.NotifyResourceChange -> notifyResourcesChanged() + GallerySideEffect.NotifyResourceScoresChanged -> + notifyResourceScoresChanged() + + GallerySideEffect.NotifyTagsChanged -> notifyTagsChanged() + is GallerySideEffect.OpenLink -> openLink(url) + + is GallerySideEffect.ShareLink -> shareLink(url) + is GallerySideEffect.ShareResource -> shareResource(path) + is GallerySideEffect.ShowEditTagsDialog -> showEditTagsDialog( + resource = resource, + resources = resources, + statsStorage = statsStorage, + rootAndFav = rootAndFav, + index = index, + storage = storage + ) + + is GallerySideEffect.ShowInfoAlert -> showInfoAlert( + path = path, + resource = resource, + metadata = metadata ) + + is GallerySideEffect.ToastIndexFailedPath -> toastIndexFailedPath( + path + ) + + is GallerySideEffect.ViewInExternalApp -> viewInExternalApp(path) + is GallerySideEffect.AbortSelectAnimation -> + binding.cbSelected.jumpDrawablesToCurrentState() } } } - override fun updatePagerAdapterWithDiff() { - presenter.diffResult?.dispatchUpdatesTo(pagerAdapter) + private fun render(state: GalleryState) { + pagerAdapter.dispatchUpdates(state.galleryItems) + binding.previewControls.isVisible = state.controlsVisible + toggleSelecting(state.selectingEnabled) + displaySelected( + state.currentItemSelected, + state.selectedResources.size, + state.galleryItems.size + ) + handleProgressState(state.progressState) + if (state.tags != cacheState?.tags) { + displayPreviewTags(state.currentItem.id(), state.tags) + } + if (state.currentPos != cacheState?.currentPos || + state.currentItem.metadata != cacheState?.currentItem?.metadata + ) { + setupPreview(state.currentPos, state.currentItem.metadata) + } + cacheState = state } - override fun setupPreview( + private fun onBackClick() { + Timber.d(LogTags.GALLERY_SCREEN, "quitting from GalleryPresenter") + notifySelectedChanged(viewModel.container.stateFlow.value.selectedResources) + exitFullscreen() + viewModel.router.exit() + } + + private fun setupPreview( pos: Int, meta: Metadata ) { - lifecycleScope.launch { - with(binding) { - setupOpenEditFABs(meta) - ExtraLoader.load( - meta, - listOf(primaryExtra, secondaryExtra), - verbose = true - ) - requireArguments().putInt(START_AT_KEY, pos) - } + with(binding) { + setupOpenEditFABs(meta) + ExtraLoader.load( + meta, + listOf(primaryExtra, secondaryExtra), + verbose = true + ) + requireArguments().putInt(START_AT_KEY, pos) } } - override fun setPreviewsScrollingEnabled(enabled: Boolean) { - binding.viewPager.isUserInputEnabled = enabled - } - - override fun setControlsVisibility(visible: Boolean) { - binding.previewControls.isVisible = visible - } - - override fun editResource(resourcePath: Path) { + private fun editResource(resourcePath: Path) { val intent = getExternalAppIntent( resourcePath, Intent.ACTION_EDIT, @@ -229,65 +311,55 @@ class GalleryFragment : } } - override fun shareResource(resourcePath: Path) = + private fun shareResource(resourcePath: Path) = openIntentChooser( resourcePath, Intent.ACTION_SEND, detachProcess = false ) - override fun openLink(link: String) { + private fun openLink(link: String) { val intent = Intent(Intent.ACTION_VIEW) val uri = Uri.parse(link) intent.data = uri startActivity(Intent.createChooser(intent, "View the link with:")) } - override fun shareLink(link: String) { + private fun shareLink(link: String) { val intent = Intent(Intent.ACTION_SEND) intent.putExtra(Intent.EXTRA_TEXT, link) intent.type = "text/plain" startActivity(Intent.createChooser(intent, "Share the link with:")) } - override fun showInfoAlert(path: Path, resource: Resource, metadata: Metadata) { + private fun showInfoAlert(path: Path, resource: Resource, metadata: Metadata) { DetailsAlertDialog(path, resource, metadata, requireContext()).show() } - override fun viewInExternalApp(resourcePath: Path) { + private fun viewInExternalApp(resourcePath: Path) { openIntentChooser(resourcePath, Intent.ACTION_VIEW, true) } - override fun deleteResource(pos: Int) { - binding.viewPager.apply { - setPageTransformer(null) - pagerAdapter.notifyItemRemoved(pos) - doOnNextLayout { - setPageTransformer(DepthPageTransformer()) - } - } - } - - override fun displayStorageException(label: String, msg: String) { + private fun displayStorageException(label: String, msg: String) { StorageExceptionDialogFragment.newInstance(label, msg).show( childFragmentManager, StorageExceptionDialogFragment.TAG ) } - override fun notifyResourcesChanged() { + private fun notifyResourcesChanged() { setFragmentResult(REQUEST_RESOURCES_CHANGED_KEY, bundleOf()) } - override fun notifyTagsChanged() { + private fun notifyTagsChanged() { setFragmentResult(REQUEST_TAGS_CHANGED_KEY, bundleOf()) } - override fun notifyResourceScoresChanged() { + private fun notifyResourceScoresChanged() { setFragmentResult(SCORES_CHANGED_KEY, bundleOf()) } - override fun notifySelectedChanged( + private fun notifySelectedChanged( selected: List ) { setFragmentResult( @@ -297,56 +369,96 @@ class GalleryFragment : SELECTING_ENABLED_KEY, requireArguments().getBoolean(SELECTING_ENABLED_KEY) ) - putParcelableArray(SELECTED_RESOURCES_KEY, selected.toTypedArray()) + putParcelableArray( + SELECTED_RESOURCES_KEY, + selected.toTypedArray() + ) } ) } - override fun toastIndexFailedPath(path: Path) { + private fun toastIndexFailedPath(path: Path) { stackedToasts.toast(path) } - override fun displayPreviewTags(resource: ResourceId, tags: Tags) { - lifecycleScope.launch { - Timber.d( - GALLERY_SCREEN, - "displaying tags of resource $resource for preview" - ) - binding.tagsCg.removeAllViews() + private fun displayPreviewTags(resource: ResourceId, tags: Tags) { + Timber.d( + LogTags.GALLERY_SCREEN, + "displaying tags of resource $resource for preview" + ) + binding.tagsCg.removeAllViews() - tags.forEach { tag -> - val chip = Chip(context) - chip.text = tag + tags.forEach { tag -> + val chip = Chip(context) + chip.text = tag - chip.setOnClickListener { - showTagMenuPopup(tag, chip) - } - binding.tagsCg.addView(chip) + chip.setOnClickListener { + showTagMenuPopup(tag, chip) } - - binding.tagsCg.addView(createEditChip()) + binding.tagsCg.addView(chip) } + + binding.tagsCg.addView(createEditChip()) } - override fun showEditTagsDialog( - resource: ResourceId + private fun showEditTagsDialog( + resource: ResourceId, + rootAndFav: RootAndFav, + resources: List, + index: ResourceIndex, + storage: TagStorage, + statsStorage: StatsStorage ) { Timber.d( - GALLERY_SCREEN, + LogTags.GALLERY_SCREEN, "showing [edit-tags] dialog for resource $resource" ) val dialog = EditTagsDialogFragment.newInstance( - requireArguments()[ROOT_AND_FAV_KEY] as RootAndFav, - listOf(resource), - presenter.index, - presenter.tagsStorage, - presenter.statsStorage + rootAndFav = rootAndFav, + resources = resources, + index = index, + storage = storage, + statsStorage = statsStorage ) dialog.show(childFragmentManager, EditTagsDialogFragment.FRAGMENT_TAG) } + private fun handleProgressState(state: ProgressState) { + when (state) { + ProgressState.HideProgress -> setProgressVisibility( + false, + "" + ) + + ProgressState.Indexing -> setProgressVisibility( + true, + getString(R.string.progress_text_changes_detected_indexing) + ) + + ProgressState.ProvidingDataStorage -> setProgressVisibility( + true, + getString(R.string.progress_text_providing_data_storage) + ) + + ProgressState.ProvidingMetaDataStorage -> setProgressVisibility( + true, + getString(R.string.progress_text_providing_metadata_storage) + ) + + ProgressState.ProvidingPreviewStorage -> setProgressVisibility( + true, + getString(R.string.progress_text_providing_previews_storage) + ) + + ProgressState.ProvidingRootIndex -> setProgressVisibility( + true, + getString(R.string.progress_text_providing_root_index) + ) + } + } + @SuppressLint("ClickableViewAccessibility") - override fun setProgressVisibility(isVisible: Boolean, withText: String) { + private fun setProgressVisibility(isVisible: Boolean, withText: String) { binding.layoutProgress.apply { root.isVisible = isVisible @@ -367,84 +479,59 @@ class GalleryFragment : } } - override fun exitFullscreen() { + private fun exitFullscreen() { FullscreenHelper.setStatusBarVisibility(true, requireActivity().window) (requireActivity() as MainActivity).setBottomNavigationVisibility(true) } - override fun notifyCurrentItemChanged() { + private fun notifyCurrentItemChanged() { binding.viewPager.post { pagerAdapter.notifyItemChanged(binding.viewPager.currentItem) } } - override fun displaySelected( + private fun displaySelected( selected: Boolean, - showAnim: Boolean, selectedCount: Int, itemCount: Int ) = with(binding) { Timber.d("display ${System.currentTimeMillis()}") cbSelected.isChecked = selected - if (!showAnim) { - cbSelected.jumpDrawablesToCurrentState() - } tvSelectedOf.text = "$selectedCount/$itemCount" return@with } - override fun onResume() { - super.onResume() - presenter.onResume() - } - - override fun toggleSelecting(enabled: Boolean) { + private fun toggleSelecting(enabled: Boolean) { binding.layoutSelected.isVisible = enabled binding.fabStartSelect.isVisible = !enabled requireArguments().apply { putBoolean(SELECTING_ENABLED_KEY, enabled) } - if (enabled) { - presenter.onSelectBtnClick() - } else { - binding.cbSelected.isChecked = false - } } - private fun initViewPager() = with(binding.viewPager) { - adapter = pagerAdapter - offscreenPageLimit = 2 - val rv = (getChildAt(0) as RecyclerView) - (rv.itemAnimator as SimpleItemAnimator).removeDuration = 0 - setPageTransformer(DepthPageTransformer()) - - registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() { - override fun onPageSelected(position: Int) { - presenter.onPageChanged(position) + private fun setupOpenEditFABs(meta: Metadata?) = binding.apply { + openResourceFab.makeGone() + editResourceFab.makeGone() + when (meta) { + is Metadata.Video, is Metadata.Link, null -> { + // "open" capabilities only + openResourceFab.makeVisible() } - }) - } - private fun showTagMenuPopup(tag: Tag, tagView: View) { - val menuBinding = PopupGalleryTagMenuBinding - .inflate(requireActivity().layoutInflater) - val popup = DefaultPopup( - menuBinding, - R.style.BottomFadeScaleAnimation, - R.drawable.bg_rounded_16dp - ) - menuBinding.apply { - btnNewSelection.setOnClickListener { - presenter.onTagSelected(tag) - popup.popupWindow.dismiss() + is Metadata.Document, is Metadata.PlainText -> { + // both "open" and "edit" capabilities + editResourceFab.makeVisible() + openResourceFab.makeVisible() } - btnRemoveTag.setOnClickListener { - presenter.onTagRemove(tag) - popup.popupWindow.dismiss() + + is Metadata.Image -> { + // "edit" capabilities only + editResourceFab.makeVisible() } + + else -> {} } - popup.showAbove(tagView) } /** @@ -457,14 +544,14 @@ class GalleryFragment : this ) { _, _ -> setFragmentResult(REQUEST_TAGS_CHANGED_KEY, bundleOf()) - presenter.onTagsChanged() + viewModel.onTagsChanged() } childFragmentManager.setFragmentResultListener( StorageExceptionDialogFragment.STORAGE_CORRUPTION_DETECTED, this ) { _, _ -> - presenter.router.newRootScreen(Screens.FoldersScreen()) + viewModel.router.newRootScreen(Screens.FoldersScreen()) } } @@ -475,28 +562,39 @@ class GalleryFragment : } } - private fun setupOpenEditFABs(meta: Metadata?) = binding.apply { - openResourceFab.makeGone() - editResourceFab.makeGone() - when (meta) { - is Metadata.Video, is Metadata.Link, null -> { - // "open" capabilities only - openResourceFab.makeVisible() - } + private fun initViewPager() = with(binding.viewPager) { + adapter = pagerAdapter + offscreenPageLimit = 2 + val rv = (getChildAt(0) as RecyclerView) + (rv.itemAnimator as SimpleItemAnimator).removeDuration = 0 + setPageTransformer(DepthPageTransformer()) - is Metadata.Document, is Metadata.PlainText -> { - // both "open" and "edit" capabilities - editResourceFab.makeVisible() - openResourceFab.makeVisible() + registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() { + override fun onPageSelected(position: Int) { + viewModel.onPageChanged(position) } + }) + } - is Metadata.Image -> { - // "edit" capabilities only - editResourceFab.makeVisible() + private fun showTagMenuPopup(tag: Tag, tagView: View) { + val menuBinding = PopupGalleryTagMenuBinding + .inflate(requireActivity().layoutInflater) + val popup = DefaultPopup( + menuBinding, + R.style.BottomFadeScaleAnimation, + R.drawable.bg_rounded_16dp + ) + menuBinding.apply { + btnNewSelection.setOnClickListener { + viewModel.onTagSelected(tag) + popup.popupWindow.dismiss() + } + btnRemoveTag.setOnClickListener { + viewModel.onTagRemove(tag) + popup.popupWindow.dismiss() } - - else -> {} } + popup.showAbove(tagView) } private fun openIntentChooser( @@ -505,7 +603,7 @@ class GalleryFragment : detachProcess: Boolean ) { Timber.i( - GALLERY_SCREEN, + LogTags.GALLERY_SCREEN, "Opening resource in an external application " + "path: $resourcePath" + "action: $actionType" @@ -551,7 +649,7 @@ class GalleryFragment : } } Timber.d( - GALLERY_SCREEN, + LogTags.GALLERY_SCREEN, "URI: ${intent.data}" + "MIME: ${intent.type}" ) return intent @@ -579,10 +677,10 @@ class GalleryFragment : setOnClickListener { val position = binding.viewPager.currentItem Timber.d( - GALLERY_SCREEN, + LogTags.GALLERY_SCREEN, "[edit_tags] clicked at position $position" ) - presenter.onEditTagsDialogBtnClick() + viewModel.onEditTagsDialogBtnClick() } } } diff --git a/app/src/main/java/dev/arkbuilders/navigator/presentation/screen/gallery/GalleryPresenter.kt b/app/src/main/java/dev/arkbuilders/navigator/presentation/screen/gallery/GalleryPresenter.kt deleted file mode 100644 index 0569361f..00000000 --- a/app/src/main/java/dev/arkbuilders/navigator/presentation/screen/gallery/GalleryPresenter.kt +++ /dev/null @@ -1,456 +0,0 @@ -package dev.arkbuilders.navigator.presentation.screen.gallery - -import androidx.recyclerview.widget.DiffUtil -import dev.arkbuilders.arkfilepicker.folders.RootAndFav -import dev.arkbuilders.arklib.ResourceId -import dev.arkbuilders.arklib.data.Message -import dev.arkbuilders.arklib.data.index.Resource -import dev.arkbuilders.arklib.data.index.ResourceIndex -import dev.arkbuilders.arklib.data.index.ResourceIndexRepo -import dev.arkbuilders.arklib.data.meta.Metadata -import dev.arkbuilders.arklib.data.meta.MetadataProcessor -import dev.arkbuilders.arklib.data.meta.MetadataProcessorRepo -import dev.arkbuilders.arklib.data.preview.PreviewLocator -import dev.arkbuilders.arklib.data.preview.PreviewProcessor -import dev.arkbuilders.arklib.data.preview.PreviewProcessorRepo -import dev.arkbuilders.arklib.data.stats.StatsEvent -import dev.arkbuilders.arklib.data.storage.StorageException -import dev.arkbuilders.arklib.user.score.ScoreStorage -import dev.arkbuilders.arklib.user.score.ScoreStorageRepo -import dev.arkbuilders.arklib.user.tags.Tag -import dev.arkbuilders.arklib.user.tags.TagStorage -import dev.arkbuilders.arklib.user.tags.Tags -import dev.arkbuilders.arklib.user.tags.TagsStorageRepo -import dev.arkbuilders.arklib.utils.ImageUtils -import dev.arkbuilders.arklib.utils.extension -import dev.arkbuilders.components.scorewidget.ScoreWidgetController -import dev.arkbuilders.navigator.analytics.gallery.GalleryAnalytics -import dev.arkbuilders.navigator.data.preferences.PreferenceKey -import dev.arkbuilders.navigator.data.preferences.Preferences -import dev.arkbuilders.navigator.data.stats.StatsStorage -import dev.arkbuilders.navigator.data.stats.StatsStorageRepo -import dev.arkbuilders.navigator.data.utils.LogTags.GALLERY_SCREEN -import dev.arkbuilders.navigator.domain.HandleGalleryExternalChangesUseCase -import dev.arkbuilders.navigator.presentation.navigation.AppRouter -import dev.arkbuilders.navigator.presentation.navigation.Screens -import dev.arkbuilders.navigator.presentation.screen.gallery.previewpager.PreviewImageViewHolder -import dev.arkbuilders.navigator.presentation.screen.gallery.previewpager.PreviewPlainTextViewHolder -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.NonCancellable -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import moxy.MvpPresenter -import moxy.presenterScope -import timber.log.Timber -import java.io.FileReader -import java.nio.file.Files -import java.nio.file.Path -import javax.inject.Inject -import kotlin.io.path.getLastModifiedTime -import kotlin.io.path.notExists - -class GalleryPresenter( - private val rootAndFav: RootAndFav, - private val resourcesIds: List, - startAt: Int, - private var selectingEnabled: Boolean, - private val selectedResources: MutableList, - private val defaultDispatcher: CoroutineDispatcher = Dispatchers.IO -) : MvpPresenter() { - - val scoreWidgetController = ScoreWidgetController( - presenterScope, - getCurrentId = { currentItem.id() }, - onScoreChanged = { viewState.notifyResourceScoresChanged() } - ) - - lateinit var index: ResourceIndex - private set - lateinit var tagsStorage: TagStorage - private set - private lateinit var previewStorage: PreviewProcessor - lateinit var metadataStorage: MetadataProcessor - private set - lateinit var statsStorage: StatsStorage - private set - private lateinit var scoreStorage: ScoreStorage - - data class GalleryItem( - val resource: Resource, - val preview: PreviewLocator, - val metadata: Metadata - ) { - fun id() = resource.id - } - - var galleryItems: MutableList = mutableListOf() - - var diffResult: DiffUtil.DiffResult? = null - - private var currentPos = startAt - - private val currentItem: GalleryItem - get() = galleryItems[currentPos] - - private var sortByScores = false - private var isControlsVisible = false - - @Inject - lateinit var preferences: Preferences - - @Inject - lateinit var router: AppRouter - - @Inject - lateinit var indexRepo: ResourceIndexRepo - - @Inject - lateinit var previewStorageRepo: PreviewProcessorRepo - - @Inject - lateinit var metadataStorageRepo: MetadataProcessorRepo - - @Inject - lateinit var tagsStorageRepo: TagsStorageRepo - - @Inject - lateinit var statsStorageRepo: StatsStorageRepo - - @Inject - lateinit var scoreStorageRepo: ScoreStorageRepo - - private val messageFlow: MutableSharedFlow = MutableSharedFlow() - - @Inject - lateinit var handleGalleryExternalChangesUseCase: - HandleGalleryExternalChangesUseCase - - @Inject - lateinit var analytics: GalleryAnalytics - - override fun onFirstViewAttach() { - analytics.trackScreen() - Timber.d(GALLERY_SCREEN, "first view attached in GalleryPresenter") - super.onFirstViewAttach() - presenterScope.launch { - viewState.init() - viewState.setProgressVisibility(true, "Providing root index") - - index = indexRepo.provide(rootAndFav) - messageFlow.onEach { message -> - when (message) { - is Message.KindDetectFailed -> viewState.toastIndexFailedPath( - message.path - ) - } - }.launchIn(presenterScope) - - viewState.setProgressVisibility(true, "Providing metadata storage") - metadataStorage = metadataStorageRepo.provide(index) - - viewState.setProgressVisibility(true, "Providing previews storage") - previewStorage = previewStorageRepo.provide(index) - - viewState.setProgressVisibility(true, "Proviging data storages") - - try { - tagsStorage = tagsStorageRepo.provide(index) - scoreStorage = scoreStorageRepo.provide(index) - } catch (e: StorageException) { - viewState.displayStorageException( - e.label, - e.msg - ) - } - - statsStorage = statsStorageRepo.provide(index) - scoreWidgetController.init(scoreStorage) - - galleryItems = provideGalleryItems().toMutableList() - - sortByScores = preferences.get(PreferenceKey.SortByScores) - - viewState.updatePagerAdapter() - viewState.setProgressVisibility(false) - scoreWidgetController.setVisible(sortByScores) - } - } - - fun onPageChanged(newPos: Int) = presenterScope.launch { - if (galleryItems.isEmpty()) { - return@launch - } - - checkResourceChanges(newPos) - - currentPos = newPos - - val id = currentItem.id() - val tags = tagsStorage.getTags(id) - displayPreview(id, currentItem.metadata, tags) - } - - fun onResume() { - checkResourceChanges(currentPos) - } - - fun getKind(pos: Int): Int = - galleryItems[pos].metadata.kind.ordinal - - fun bindView(view: PreviewImageViewHolder) = presenterScope.launch { - view.reset() - val item = galleryItems[view.pos] - - val path = index.getPath(item.id())!! - val placeholder = ImageUtils.iconForExtension(extension(path)) - view.setSource(placeholder, item.id(), item.metadata, item.preview) - } - - fun bindPlainTextView(view: PreviewPlainTextViewHolder) = presenterScope.launch { - view.reset() - val item = galleryItems[view.pos] - - val path = index.getPath(item.id())!! - val content = readText(path) - - content.onSuccess { - view.setContent(it) - } - } - - fun onTagsChanged() { - val tags = tagsStorage.getTags(currentItem.id()) - viewState.displayPreviewTags(currentItem.id(), tags) - } - - fun onOpenFabClick() = presenterScope.launch { - analytics.trackResOpen() - Timber.d(GALLERY_SCREEN, "[open_resource] clicked at position $currentPos") - - val id = currentItem.id() - val path = index.getPath(id)!! - - if (currentItem.metadata is Metadata.Link) { - val url = readText(path).getOrThrow() - viewState.openLink(url) - - return@launch - } - - viewState.viewInExternalApp(path) - } - - fun onInfoFabClick() = presenterScope.launch { - analytics.trackResInfo() - Timber.d(GALLERY_SCREEN, "[info_resource] clicked at position $currentPos") - - val path = index.getPath(currentItem.id())!! - viewState.showInfoAlert(path, currentItem.resource, currentItem.metadata) - } - - fun onShareFabClick() = presenterScope.launch { - analytics.trackResShare() - Timber.d(GALLERY_SCREEN, "[share_resource] clicked at position $currentPos") - val path = index.getPath(currentItem.id())!! - - if (currentItem.metadata is Metadata.Link) { - val url = readText(path).getOrThrow() - viewState.shareLink(url) - return@launch - } - - viewState.shareResource(path) - } - - fun onEditFabClick() = presenterScope.launch { - analytics.trackResEdit() - Timber.d(GALLERY_SCREEN, "[edit_resource] clicked at position $currentPos") - val path = index.getPath(currentItem.id())!! - viewState.editResource(path) - } - - fun onRemoveFabClick() = presenterScope.launch(NonCancellable) { - analytics.trackResRemove() - Timber.d(GALLERY_SCREEN, "[remove_resource] clicked at position $currentPos") - deleteResource(currentItem.id()) - galleryItems.removeAt(currentPos) - - if (galleryItems.isEmpty()) { - onBackClick() - return@launch - } - - onTagsChanged() - viewState.deleteResource(currentPos) - } - - fun onTagSelected(tag: Tag) { - analytics.trackTagSelect() - router.navigateTo( - Screens.ResourcesScreenWithSelectedTag( - rootAndFav, - tag - ) - ) - } - - fun onTagRemove(tag: Tag) = presenterScope.launch(NonCancellable) { - analytics.trackTagRemove() - val id = currentItem.id() - - val tags = tagsStorage.getTags(id) - val newTags = tags - tag - - viewState.displayPreviewTags(id, newTags) - statsStorage.handleEvent( - StatsEvent.TagsChanged( - id, - tags, - newTags - ) - ) - - Timber.d(GALLERY_SCREEN, "setting new tags $newTags to $currentItem") - - tagsStorage.setTags(id, newTags) - tagsStorage.persist() - viewState.notifyTagsChanged() - } - - fun onSelectBtnClick() { - val id = currentItem.id() - val wasSelected = id in selectedResources - - if (wasSelected) { - selectedResources.remove(id) - } else { - selectedResources.add(id) - } - - viewState.displaySelected( - !wasSelected, - showAnim = true, - selectedResources.size, - galleryItems.size - ) - } - - fun onEditTagsDialogBtnClick() { - analytics.trackTagsEdit() - viewState.showEditTagsDialog(currentItem.id()) - } - - private fun checkResourceChanges(pos: Int) = - presenterScope.launch(defaultDispatcher) { - if (galleryItems.isEmpty()) { - return@launch - } - - val item = galleryItems[pos] - - val path = index.getPath(item.id()) - ?: let { - Timber.d("Resource ${item.id()} can't be found in the index") - handleGalleryExternalChangesUseCase(this@GalleryPresenter) - return@launch - } - - if (path.notExists()) { - Timber.d("Resource ${item.id()} isn't stored by path $path") - handleGalleryExternalChangesUseCase(this@GalleryPresenter) - return@launch - } - - if (path.getLastModifiedTime() != item.resource.modified) { - Timber.d("Index is not up-to-date regarding path $path") - handleGalleryExternalChangesUseCase(this@GalleryPresenter) - return@launch - } - } - - private suspend fun deleteResource(resource: ResourceId) { - Timber.d(GALLERY_SCREEN, "deleting resource $resource") - - val path = index.getPath(resource) - - withContext(defaultDispatcher) { - Files.delete(path) - } - - index.updateAll() - viewState.notifyResourcesChanged() - } - - private fun displayPreview( - id: ResourceId, - meta: Metadata, - tags: Tags - ) { - viewState.setupPreview(currentPos, meta) - viewState.displayPreviewTags(id, tags) - scoreWidgetController.displayScore() - viewState.displaySelected( - id in selectedResources, - showAnim = false, - selectedResources.size, - galleryItems.size - ) - } - - fun onPreviewsItemClick() { - isControlsVisible = !isControlsVisible - viewState.setControlsVisibility(isControlsVisible) - } - - fun onSelectingChanged() { - selectingEnabled = !selectingEnabled - viewState.toggleSelecting(selectingEnabled) - selectedResources.clear() - if (selectingEnabled) { - selectedResources.add(currentItem.resource.id) - } - } - - fun onPlayButtonClick() = presenterScope.launch { - viewState.viewInExternalApp(index.getPath(currentItem.id())!!) - } - - fun onBackClick() { - Timber.d(GALLERY_SCREEN, "quitting from GalleryPresenter") - viewState.notifySelectedChanged(selectedResources) - viewState.exitFullscreen() - router.exit() - } - - fun provideGalleryItems(): List = - try { - val allResources = index.allResources() - resourcesIds - .filter { allResources.keys.contains(it) } - .map { id -> - val preview = previewStorage.retrieve(id).getOrThrow() - val metadata = metadataStorage.retrieve(id).getOrThrow() - val resource = allResources.getOrElse(id) { - throw NullPointerException("Resource not exist") - } - GalleryItem(resource, preview, metadata) - }.toMutableList() - } catch (e: Exception) { - Timber.d("Can't provide gallery items") - emptyList() - } - - private suspend fun readText(source: Path): Result = - withContext(defaultDispatcher) { - try { - val content = FileReader(source.toFile()).readText() - Result.success(content) - } catch (e: Exception) { - Result.failure(e) - } - } -} diff --git a/app/src/main/java/dev/arkbuilders/navigator/presentation/screen/gallery/GalleryView.kt b/app/src/main/java/dev/arkbuilders/navigator/presentation/screen/gallery/GalleryView.kt deleted file mode 100644 index 4d856a2d..00000000 --- a/app/src/main/java/dev/arkbuilders/navigator/presentation/screen/gallery/GalleryView.kt +++ /dev/null @@ -1,75 +0,0 @@ -package dev.arkbuilders.navigator.presentation.screen.gallery - -import dev.arkbuilders.navigator.presentation.common.CommonMvpView -import moxy.viewstate.strategy.AddToEndSingleStrategy -import moxy.viewstate.strategy.SkipStrategy -import moxy.viewstate.strategy.StateStrategyType -import dev.arkbuilders.arklib.ResourceId -import dev.arkbuilders.arklib.data.index.Resource -import dev.arkbuilders.arklib.data.meta.Metadata -import dev.arkbuilders.arklib.user.tags.Tags -import java.nio.file.Path - -@StateStrategyType(AddToEndSingleStrategy::class) -interface GalleryView : CommonMvpView { - fun init() - fun updatePagerAdapter() - fun updatePagerAdapterWithDiff() - fun setControlsVisibility(visible: Boolean) - fun exitFullscreen() - fun setPreviewsScrollingEnabled(enabled: Boolean) - fun setupPreview(pos: Int, meta: Metadata) - fun displayPreviewTags(resource: ResourceId, tags: Tags) - fun setProgressVisibility(isVisible: Boolean, withText: String = "") - fun displaySelected( - selected: Boolean, - showAnim: Boolean, - selectedCount: Int, - itemCount: Int - ) - - @StateStrategyType(SkipStrategy::class) - fun openLink(link: String) - - @StateStrategyType(SkipStrategy::class) - fun shareLink(link: String) - - @StateStrategyType(SkipStrategy::class) - fun showInfoAlert(path: Path, resource: Resource, metadata: Metadata) - - @StateStrategyType(SkipStrategy::class) - fun viewInExternalApp(resourcePath: Path) - - @StateStrategyType(SkipStrategy::class) - fun editResource(resourcePath: Path) - - @StateStrategyType(SkipStrategy::class) - fun shareResource(resourcePath: Path) - - @StateStrategyType(SkipStrategy::class) - fun showEditTagsDialog(resource: ResourceId) - - @StateStrategyType(SkipStrategy::class) - fun deleteResource(pos: Int) - - @StateStrategyType(SkipStrategy::class) - fun toggleSelecting(enabled: Boolean) - - @StateStrategyType(SkipStrategy::class) - fun notifyResourcesChanged() - - @StateStrategyType(SkipStrategy::class) - fun notifyTagsChanged() - - @StateStrategyType(SkipStrategy::class) - fun notifyCurrentItemChanged() - - @StateStrategyType(SkipStrategy::class) - fun notifyResourceScoresChanged() - - @StateStrategyType(SkipStrategy::class) - fun notifySelectedChanged(selected: List) - - @StateStrategyType(SkipStrategy::class) - fun displayStorageException(label: String, msg: String) -} diff --git a/app/src/main/java/dev/arkbuilders/navigator/presentation/screen/gallery/GalleryViewModel.kt b/app/src/main/java/dev/arkbuilders/navigator/presentation/screen/gallery/GalleryViewModel.kt new file mode 100644 index 00000000..5d5c90ee --- /dev/null +++ b/app/src/main/java/dev/arkbuilders/navigator/presentation/screen/gallery/GalleryViewModel.kt @@ -0,0 +1,486 @@ +package dev.arkbuilders.navigator.presentation.screen.gallery + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dev.arkbuilders.arkfilepicker.folders.RootAndFav +import dev.arkbuilders.arklib.ResourceId +import dev.arkbuilders.arklib.data.Message +import dev.arkbuilders.arklib.data.index.ResourceIndex +import dev.arkbuilders.arklib.data.index.ResourceIndexRepo +import dev.arkbuilders.arklib.data.meta.Metadata +import dev.arkbuilders.arklib.data.meta.MetadataProcessor +import dev.arkbuilders.arklib.data.meta.MetadataProcessorRepo +import dev.arkbuilders.arklib.data.preview.PreviewProcessor +import dev.arkbuilders.arklib.data.preview.PreviewProcessorRepo +import dev.arkbuilders.arklib.data.stats.StatsEvent +import dev.arkbuilders.arklib.data.storage.StorageException +import dev.arkbuilders.arklib.user.score.ScoreStorage +import dev.arkbuilders.arklib.user.score.ScoreStorageRepo +import dev.arkbuilders.arklib.user.tags.Tag +import dev.arkbuilders.arklib.user.tags.TagStorage +import dev.arkbuilders.arklib.user.tags.TagsStorageRepo +import dev.arkbuilders.components.scorewidget.ScoreWidgetController +import dev.arkbuilders.navigator.analytics.gallery.GalleryAnalytics +import dev.arkbuilders.navigator.data.preferences.PreferenceKey +import dev.arkbuilders.navigator.data.preferences.Preferences +import dev.arkbuilders.navigator.data.stats.StatsStorage +import dev.arkbuilders.navigator.data.stats.StatsStorageRepo +import dev.arkbuilders.navigator.data.utils.LogTags +import dev.arkbuilders.navigator.presentation.navigation.AppRouter +import dev.arkbuilders.navigator.presentation.navigation.Screens +import dev.arkbuilders.navigator.presentation.screen.gallery.domain.GalleryItem +import dev.arkbuilders.navigator.presentation.screen.gallery.domain.GallerySideEffect +import dev.arkbuilders.navigator.presentation.screen.gallery.domain.GalleryState +import dev.arkbuilders.navigator.presentation.screen.gallery.domain.ProgressState +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.NonCancellable +import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.orbitmvi.orbit.Container +import org.orbitmvi.orbit.ContainerHost +import org.orbitmvi.orbit.syntax.simple.blockingIntent +import org.orbitmvi.orbit.syntax.simple.intent +import org.orbitmvi.orbit.syntax.simple.postSideEffect +import org.orbitmvi.orbit.syntax.simple.reduce +import org.orbitmvi.orbit.viewmodel.container +import timber.log.Timber +import java.io.FileReader +import java.nio.file.Files +import java.nio.file.Path +import kotlin.io.path.getLastModifiedTime +import kotlin.io.path.notExists + +class GalleryViewModel( + startPos: Int, + selectingEnabled: Boolean, + selectedResources: List, + rootAndFav: RootAndFav, + resourcesIds: List, + val preferences: Preferences, + val router: AppRouter, + val indexRepo: ResourceIndexRepo, + val previewStorageRepo: PreviewProcessorRepo, + val metadataStorageRepo: MetadataProcessorRepo, + val tagsStorageRepo: TagsStorageRepo, + val statsStorageRepo: StatsStorageRepo, + val scoreStorageRepo: ScoreStorageRepo, + val analytics: GalleryAnalytics +) : ContainerHost, ViewModel() { + private lateinit var index: ResourceIndex + private lateinit var tagsStorage: TagStorage + private lateinit var previewStorage: PreviewProcessor + private lateinit var metadataStorage: MetadataProcessor + private lateinit var statsStorage: StatsStorage + private lateinit var scoreStorage: ScoreStorage + + override val container: Container = + container( + GalleryState( + rootAndFav = rootAndFav, + resourcesIds = resourcesIds, + currentPos = startPos, + selectingEnabled = selectingEnabled, + selectedResources = selectedResources + ) + ) + + val scoreWidgetController = ScoreWidgetController( + scope = viewModelScope, + getCurrentId = { container.stateFlow.value.currentItem.id() }, + onScoreChanged = { + intent { + postSideEffect(GallerySideEffect.NotifyResourceScoresChanged) + } + } + ) + + private val messageFlow: MutableSharedFlow = MutableSharedFlow() + + init { + initStorages() + intent { + postSideEffect(GallerySideEffect.ScrollToPage(state.currentPos)) + } + } + + fun onPreviewsItemClick() { + intent { + reduce { state.copy(controlsVisible = !state.controlsVisible) } + } + } + + fun onRemoveFabClick() = viewModelScope.launch(NonCancellable) { + intent { + analytics.trackResRemove() + Timber.d( + LogTags.GALLERY_SCREEN, + buildString { + append("[remove_resource] clicked at position ") + append("${state.currentPos}") + } + ) + deleteResource(state.currentItem.id()) + val newGalleryItems = state.galleryItems.toMutableList() + newGalleryItems.removeAt(state.currentPos) + if (newGalleryItems.isEmpty()) { + intent { + postSideEffect(GallerySideEffect.NavigateBack) + } + return@intent + } + onTagsChanged() + reduce { + state.copy(galleryItems = newGalleryItems) + } + } + } + + private suspend fun deleteResource(resource: ResourceId) { + intent { + Timber.d(LogTags.GALLERY_SCREEN, "deleting resource $resource") + withContext(Dispatchers.IO) { + val path = index.getPath(resource) + Files.delete(path) + } + index.updateAll() + + postSideEffect(GallerySideEffect.NotifyResourceChange) + } + } + + fun onPlayButtonClick() = intent { + postSideEffect( + GallerySideEffect.ViewInExternalApp( + index.getPath(state.currentItem.id())!! + ) + ) + } + + fun onInfoFabClick() = intent { + analytics.trackResInfo() + Timber.d( + LogTags.GALLERY_SCREEN, + "[info_resource] clicked at position" + + " ${container.stateFlow.value.currentPos}" + ) + val path = index.getPath(state.currentItem.id())!! + intent { + postSideEffect( + GallerySideEffect.ShowInfoAlert( + path = path, + resource = state.currentItem.resource, + metadata = state.currentItem.metadata + ) + ) + } + } + + fun onShareFabClick() = intent { + analytics.trackResShare() + Timber.d( + LogTags.GALLERY_SCREEN, + "[share_resource] clicked at position " + + "${container.stateFlow.value.currentPos}" + ) + val path = index.getPath(state.currentItem.id())!! + if (state.currentItem.metadata is Metadata.Link) { + val url = readText(path).getOrThrow() + postSideEffect(GallerySideEffect.ShareLink(url)) + return@intent + } + postSideEffect(GallerySideEffect.ShareResource(path)) + } + + fun onSelectingChanged(enabled: Boolean) = intent { + reduce { + state.copy(selectingEnabled = enabled) + } + + reduce { + if (enabled) { + state.copy( + selectedResources = state.selectedResources + + state.currentItem.id() + ) + } else { + state.copy(selectedResources = emptyList()) + } + } + } + + fun onOpenFabClick() = intent { + analytics.trackResOpen() + Timber.d( + LogTags.GALLERY_SCREEN, + "[open_resource] clicked at position " + + "${container.stateFlow.value.currentPos}" + ) + val id = state.currentItem.id() + val path = index.getPath(id)!! + if (state.currentItem.metadata is Metadata.Link) { + val url = readText(path).getOrThrow() + postSideEffect(GallerySideEffect.OpenLink(url)) + return@intent + } + postSideEffect( + GallerySideEffect.ViewInExternalApp( + index.getPath( + state.currentItem.id() + )!! + ) + ) + } + + fun onEditFabClick() = intent { + analytics.trackResEdit() + Timber.d( + LogTags.GALLERY_SCREEN, + "[edit_resource] clicked at position " + + "${container.stateFlow.value.currentPos}" + ) + val path = index.getPath(state.currentItem.id())!! + postSideEffect(GallerySideEffect.EditResource(path)) + } + + fun onSelectBtnClick() = intent { + val id = state.currentItem.id() + val newSelectedList = state.selectedResources.toMutableList() + val wasSelected = id in state.selectedResources + if (wasSelected) { + newSelectedList.remove(id) + } else { + newSelectedList.add(id) + } + + reduce { + state.copy(selectedResources = newSelectedList) + } + } + + fun onResume() = intent { + checkResourceChanges(state.currentPos) + } + + fun onTagsChanged() = intent { + val tags = tagsStorage.getTags(state.currentItem.id()) + reduce { + state.copy(tags = tags) + } + } + + fun onPageChanged(newPos: Int) = intent { + if (state.galleryItems.isEmpty()) { + return@intent + } + + reduce { + state.copy(currentPos = newPos) + } + postSideEffect(GallerySideEffect.AbortSelectAnimation) + + checkResourceChanges(newPos) + val id = state.currentItem.id() + val tags = tagsStorage.getTags(id) + reduce { + state.copy(tags = tags) + } + scoreWidgetController.displayScore() + } + + fun onTagSelected(tag: Tag) { + analytics.trackTagSelect() + router.navigateTo( + Screens.ResourcesScreenWithSelectedTag( + container.stateFlow.value.rootAndFav, + tag + ) + ) + } + + fun onTagRemove(tag: Tag) = intent { + intent { + analytics.trackTagRemove() + val id = state.currentItem.id() + val tags = tagsStorage.getTags(id) + val newTags = tags - tag + reduce { + state.copy(tags = newTags) + } + statsStorage.handleEvent( + StatsEvent.TagsChanged( + id, + tags, + newTags + ) + ) + Timber.d( + LogTags.GALLERY_SCREEN, + "setting new tags $newTags to $state.currentItem" + ) + tagsStorage.setTags(id, newTags) + tagsStorage.persist() + postSideEffect(GallerySideEffect.NotifyTagsChanged) + } + } + + fun onEditTagsDialogBtnClick() = intent { + analytics.trackTagsEdit() + postSideEffect( + GallerySideEffect.ShowEditTagsDialog( + resource = state.currentItem.id(), + resources = listOf(state.currentItem.id()), + statsStorage = statsStorage, + rootAndFav = state.rootAndFav, + index = index, + storage = tagsStorage + ) + ) + } + + private fun checkResourceChanges(pos: Int) = + intent { + if (state.galleryItems.isEmpty()) { + return@intent + } + val item = state.galleryItems[pos] + val path = index.getPath(item.id()) + ?: let { + Timber.d("Resource ${item.id()} can't be found in the index") + invokeHandleGalleryExternalChangesUseCase() + return@intent + } + if (path.notExists()) { + Timber.d("Resource ${item.id()} isn't stored by path $path") + invokeHandleGalleryExternalChangesUseCase() + return@intent + } + if (path.getLastModifiedTime() != item.resource.modified) { + Timber.d("Index is not up-to-date regarding path $path") + invokeHandleGalleryExternalChangesUseCase() + return@intent + } + } + + private fun provideGalleryItems( + resourcesIds: List + ): List = + try { + val allResources = index.allResources() + resourcesIds + .filter { allResources.keys.contains(it) } + .map { id -> + val path = index.getPath(id)!! + val preview = previewStorage.retrieve(id).getOrThrow() + val metadata = metadataStorage.retrieve(id).getOrThrow() + val resource = allResources.getOrElse(id) { + throw NullPointerException("Resource not exist") + } + GalleryItem(resource, preview, metadata, path) + }.toMutableList() + } catch (e: Exception) { + Timber.d("Can't provide gallery items") + emptyList() + } + + private fun invokeHandleGalleryExternalChangesUseCase() = intent { + reduce { + state.copy(progressState = ProgressState.Indexing) + } + index.updateAll() + postSideEffect(GallerySideEffect.NotifyResourceChange) + + viewModelScope.launch { + metadataStorage.busy.collect { busy -> + if (!busy) cancel() + } + }.join() + + val newItems = provideGalleryItems(state.resourcesIds) + if (newItems.isEmpty()) { + postSideEffect(GallerySideEffect.NavigateBack) + return@intent + } + + reduce { + state.copy(galleryItems = newItems) + } + reduce { + state.copy(progressState = ProgressState.HideProgress) + } + } + + private suspend fun readText(source: Path): Result = + withContext(Dispatchers.IO) { + try { + val content = FileReader(source.toFile()).readText() + Result.success(content) + } catch (e: Exception) { + Result.failure(e) + } + } + + private fun initStorages() = blockingIntent { + analytics.trackScreen() + Timber.d( + LogTags.GALLERY_SCREEN, + "first view attached in GalleryPresenter" + ) + reduce { + state.copy(progressState = ProgressState.ProvidingRootIndex) + } + index = indexRepo.provide(state.rootAndFav) + messageFlow.onEach { message -> + when (message) { + is Message.KindDetectFailed -> + intent { + postSideEffect( + GallerySideEffect.ToastIndexFailedPath( + message.path + ) + ) + } + } + }.launchIn(viewModelScope) + reduce { + state.copy(progressState = ProgressState.ProvidingMetaDataStorage) + } + metadataStorage = metadataStorageRepo.provide(index) + reduce { + state.copy(progressState = ProgressState.ProvidingPreviewStorage) + } + previewStorage = previewStorageRepo.provide(index) + reduce { + state.copy(progressState = ProgressState.ProvidingDataStorage) + } + try { + tagsStorage = tagsStorageRepo.provide(index) + scoreStorage = scoreStorageRepo.provide(index) + } catch (e: StorageException) { + postSideEffect( + GallerySideEffect.DisplayStorageException( + label = e.label, + messenger = e.msg + ) + ) + } + statsStorage = statsStorageRepo.provide(index) + scoreWidgetController.init(scoreStorage) + val galleryItems = provideGalleryItems(state.resourcesIds) + viewModelScope.launch { + val result = preferences.get( + PreferenceKey.SortByScores + ) + scoreWidgetController.setVisible(result) + } + reduce { + state.copy( + galleryItems = galleryItems, + progressState = ProgressState.HideProgress + ) + } + } +} diff --git a/app/src/main/java/dev/arkbuilders/navigator/presentation/screen/gallery/GalleryViewModelFactory.kt b/app/src/main/java/dev/arkbuilders/navigator/presentation/screen/gallery/GalleryViewModelFactory.kt new file mode 100644 index 00000000..68cc6ccd --- /dev/null +++ b/app/src/main/java/dev/arkbuilders/navigator/presentation/screen/gallery/GalleryViewModelFactory.kt @@ -0,0 +1,65 @@ +package dev.arkbuilders.navigator.presentation.screen.gallery + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import dev.arkbuilders.arkfilepicker.folders.RootAndFav +import dev.arkbuilders.arklib.ResourceId +import dev.arkbuilders.arklib.data.index.ResourceIndexRepo +import dev.arkbuilders.arklib.data.meta.MetadataProcessorRepo +import dev.arkbuilders.arklib.data.preview.PreviewProcessorRepo +import dev.arkbuilders.arklib.user.score.ScoreStorageRepo +import dev.arkbuilders.arklib.user.tags.TagsStorageRepo +import dev.arkbuilders.navigator.analytics.gallery.GalleryAnalytics +import dev.arkbuilders.navigator.data.preferences.Preferences +import dev.arkbuilders.navigator.data.stats.StatsStorageRepo +import dev.arkbuilders.navigator.presentation.navigation.AppRouter + +class GalleryViewModelFactory @AssistedInject constructor( + @Assisted val startPos: Int, + @Assisted val selectingEnabled: Boolean, + @Assisted private val rootAndFav: RootAndFav, + @Assisted("all") private val resourcesIds: List, + @Assisted("selected") private val selectedResources: List, + val preferences: Preferences, + val router: AppRouter, + val indexRepo: ResourceIndexRepo, + val previewStorageRepo: PreviewProcessorRepo, + val metadataStorageRepo: MetadataProcessorRepo, + val tagsStorageRepo: TagsStorageRepo, + val statsStorageRepo: StatsStorageRepo, + val scoreStorageRepo: ScoreStorageRepo, + val analytics: GalleryAnalytics +) : ViewModelProvider.Factory { + override fun create(modelClass: Class): T { + return GalleryViewModel( + startPos = startPos, + selectingEnabled = selectingEnabled, + selectedResources = selectedResources, + rootAndFav = rootAndFav, + resourcesIds = resourcesIds, + preferences = preferences, + router = router, + indexRepo = indexRepo, + previewStorageRepo = previewStorageRepo, + metadataStorageRepo = metadataStorageRepo, + tagsStorageRepo = tagsStorageRepo, + statsStorageRepo = statsStorageRepo, + scoreStorageRepo = scoreStorageRepo, + analytics = analytics + ) as T + } + + @AssistedFactory + interface Factory { + fun create( + @Assisted startPos: Int, + @Assisted selectingEnabled: Boolean, + @Assisted rootAndFav: RootAndFav, + @Assisted("all") resourcesIds: List, + @Assisted("selected") selectedResources: List + ): GalleryViewModelFactory + } +} diff --git a/app/src/main/java/dev/arkbuilders/navigator/presentation/screen/gallery/domain/GalleryItem.kt b/app/src/main/java/dev/arkbuilders/navigator/presentation/screen/gallery/domain/GalleryItem.kt new file mode 100644 index 00000000..55fa2337 --- /dev/null +++ b/app/src/main/java/dev/arkbuilders/navigator/presentation/screen/gallery/domain/GalleryItem.kt @@ -0,0 +1,15 @@ +package dev.arkbuilders.navigator.presentation.screen.gallery.domain + +import dev.arkbuilders.arklib.data.index.Resource +import dev.arkbuilders.arklib.data.meta.Metadata +import dev.arkbuilders.arklib.data.preview.PreviewLocator +import java.nio.file.Path + +data class GalleryItem( + val resource: Resource, + val preview: PreviewLocator, + val metadata: Metadata, + val path: Path +) { + fun id() = resource.id +} diff --git a/app/src/main/java/dev/arkbuilders/navigator/presentation/screen/gallery/domain/GalleryState.kt b/app/src/main/java/dev/arkbuilders/navigator/presentation/screen/gallery/domain/GalleryState.kt new file mode 100644 index 00000000..92e95e16 --- /dev/null +++ b/app/src/main/java/dev/arkbuilders/navigator/presentation/screen/gallery/domain/GalleryState.kt @@ -0,0 +1,77 @@ +package dev.arkbuilders.navigator.presentation.screen.gallery.domain + +import dev.arkbuilders.arkfilepicker.folders.RootAndFav +import dev.arkbuilders.arklib.ResourceId +import dev.arkbuilders.arklib.data.index.Resource +import dev.arkbuilders.arklib.data.index.ResourceIndex +import dev.arkbuilders.arklib.data.meta.Metadata +import dev.arkbuilders.arklib.user.tags.TagStorage +import dev.arkbuilders.arklib.user.tags.Tags +import dev.arkbuilders.navigator.data.stats.StatsStorage +import java.nio.file.Path + +data class GalleryState( + val rootAndFav: RootAndFav, + val currentPos: Int = 0, + val resourcesIds: List = emptyList(), + val galleryItems: List = emptyList(), + val selectingEnabled: Boolean = false, + val selectedResources: List = emptyList(), + val controlsVisible: Boolean = true, + val progressState: ProgressState = ProgressState.HideProgress, + val tags: Tags = emptySet() +) { + val currentItem: GalleryItem + get() = galleryItems[currentPos] + + val currentItemSelected: Boolean + get() = currentItem.id() in selectedResources +} + +sealed interface ProgressState { + data object ProvidingRootIndex : ProgressState + data object ProvidingMetaDataStorage : ProgressState + data object ProvidingPreviewStorage : ProgressState + data object ProvidingDataStorage : ProgressState + data object Indexing : ProgressState + data object HideProgress : ProgressState +} + +sealed class GallerySideEffect { + data class ScrollToPage(val pos: Int) : GallerySideEffect() + data object NotifyResourceScoresChanged : GallerySideEffect() + data object NavigateBack : GallerySideEffect() + data class ToastIndexFailedPath(val path: Path) : GallerySideEffect() + data class ShowInfoAlert( + val path: Path, + val resource: Resource, + val metadata: Metadata + ) : GallerySideEffect() + + data class DisplayStorageException( + val label: String, + val messenger: String + ) : GallerySideEffect() + + data class ShareLink(val url: String) : GallerySideEffect() + data class ShareResource(val path: Path) : GallerySideEffect() + data class EditResource(val path: Path) : GallerySideEffect() + data class OpenLink(val url: String) : GallerySideEffect() + data class ViewInExternalApp(val path: Path) : GallerySideEffect() + + data object NotifyTagsChanged : GallerySideEffect() + data class ShowEditTagsDialog( + val resource: ResourceId, + val rootAndFav: RootAndFav, + val resources: List, + val index: ResourceIndex, + val storage: TagStorage, + val statsStorage: StatsStorage + ) : GallerySideEffect() + + // workaround to not show checkbox select animation when we change page + data object AbortSelectAnimation : GallerySideEffect() + + data object NotifyResourceChange : GallerySideEffect() + data object NotifyCurrentItemChange : GallerySideEffect() +} diff --git a/app/src/main/java/dev/arkbuilders/navigator/presentation/screen/gallery/previewpager/PreviewImageViewHolder.kt b/app/src/main/java/dev/arkbuilders/navigator/presentation/screen/gallery/pager/PreviewImageViewHolder.kt similarity index 94% rename from app/src/main/java/dev/arkbuilders/navigator/presentation/screen/gallery/previewpager/PreviewImageViewHolder.kt rename to app/src/main/java/dev/arkbuilders/navigator/presentation/screen/gallery/pager/PreviewImageViewHolder.kt index 244b2221..5f973863 100644 --- a/app/src/main/java/dev/arkbuilders/navigator/presentation/screen/gallery/previewpager/PreviewImageViewHolder.kt +++ b/app/src/main/java/dev/arkbuilders/navigator/presentation/screen/gallery/pager/PreviewImageViewHolder.kt @@ -1,8 +1,9 @@ -package dev.arkbuilders.navigator.presentation.screen.gallery.previewpager +package dev.arkbuilders.navigator.presentation.screen.gallery.pager import android.annotation.SuppressLint import androidx.core.view.GestureDetectorCompat import androidx.core.view.isVisible +import androidx.lifecycle.viewModelScope import androidx.recyclerview.widget.RecyclerView import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView import com.ortiz.touchview.OnTouchImageViewListener @@ -12,20 +13,19 @@ import dev.arkbuilders.arklib.data.preview.PreviewLocator import dev.arkbuilders.arklib.data.preview.PreviewStatus import dev.arkbuilders.arklib.utils.ImageUtils import dev.arkbuilders.navigator.databinding.ItemImageBinding -import dev.arkbuilders.navigator.presentation.screen.gallery.GalleryPresenter +import dev.arkbuilders.navigator.presentation.screen.gallery.GalleryViewModel import dev.arkbuilders.navigator.presentation.utils.makeVisibleAndSetOnClickListener import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import moxy.presenterScope import timber.log.Timber @SuppressLint("ClickableViewAccessibility") class PreviewImageViewHolder( private val binding: ItemImageBinding, - private val presenter: GalleryPresenter, + private val viewModel: GalleryViewModel, private val gestureDetector: GestureDetectorCompat ) : RecyclerView.ViewHolder(binding.root) { @@ -55,7 +55,7 @@ class PreviewImageViewHolder( if (meta is Metadata.Video) { icPlay.makeVisibleAndSetOnClickListener { - presenter.onPlayButtonClick() + viewModel.onPlayButtonClick() } } else { icPlay.isVisible = false @@ -64,7 +64,7 @@ class PreviewImageViewHolder( if (!locator.isGenerated()) { progress.isVisible = true Timber.d("join preview generation for $id") - joinPreviewJob = presenter.presenterScope.launch { + joinPreviewJob = viewModel.viewModelScope.launch { locator.join() if (!isActive) return@launch diff --git a/app/src/main/java/dev/arkbuilders/navigator/presentation/screen/gallery/previewpager/PreviewPlainTextViewHolder.kt b/app/src/main/java/dev/arkbuilders/navigator/presentation/screen/gallery/pager/PreviewPlainTextViewHolder.kt similarity index 82% rename from app/src/main/java/dev/arkbuilders/navigator/presentation/screen/gallery/previewpager/PreviewPlainTextViewHolder.kt rename to app/src/main/java/dev/arkbuilders/navigator/presentation/screen/gallery/pager/PreviewPlainTextViewHolder.kt index 4f9ccdef..f32d0c5d 100644 --- a/app/src/main/java/dev/arkbuilders/navigator/presentation/screen/gallery/previewpager/PreviewPlainTextViewHolder.kt +++ b/app/src/main/java/dev/arkbuilders/navigator/presentation/screen/gallery/pager/PreviewPlainTextViewHolder.kt @@ -1,4 +1,4 @@ -package dev.arkbuilders.navigator.presentation.screen.gallery.previewpager +package dev.arkbuilders.navigator.presentation.screen.gallery.pager import android.annotation.SuppressLint import androidx.core.view.GestureDetectorCompat @@ -21,8 +21,4 @@ class PreviewPlainTextViewHolder( fun setContent(text: String) = with(binding) { tvContent.text = text } - - fun reset() = with(binding) { - tvContent.text = "" - } } diff --git a/app/src/main/java/dev/arkbuilders/navigator/presentation/screen/gallery/pager/PreviewsPager.kt b/app/src/main/java/dev/arkbuilders/navigator/presentation/screen/gallery/pager/PreviewsPager.kt new file mode 100644 index 00000000..defcae2f --- /dev/null +++ b/app/src/main/java/dev/arkbuilders/navigator/presentation/screen/gallery/pager/PreviewsPager.kt @@ -0,0 +1,136 @@ +package dev.arkbuilders.navigator.presentation.screen.gallery.pager + +import android.annotation.SuppressLint +import android.content.Context +import android.view.GestureDetector +import android.view.LayoutInflater +import android.view.MotionEvent +import android.view.ViewGroup +import androidx.core.view.GestureDetectorCompat +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.RecyclerView +import dev.arkbuilders.arklib.data.meta.Kind +import dev.arkbuilders.arklib.utils.ImageUtils +import dev.arkbuilders.arklib.utils.extension +import dev.arkbuilders.navigator.databinding.ItemImageBinding +import dev.arkbuilders.navigator.databinding.ItemPreviewPlainTextBinding +import dev.arkbuilders.navigator.presentation.screen.gallery.GalleryViewModel +import dev.arkbuilders.navigator.presentation.screen.gallery.domain.GalleryItem +import dev.arkbuilders.navigator.presentation.screen.resources.adapter.ResourceDiffUtilCallback +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.io.FileReader +import java.nio.file.Path + +class PreviewsPager( + val lifecycleScope: CoroutineScope, + val context: Context, + val viewModel: GalleryViewModel +) : RecyclerView.Adapter() { + private var galleryItems = emptyList() + + fun dispatchUpdates(newItems: List) { + if (newItems == galleryItems) { + return + } + val diff = DiffUtil.calculateDiff( + ResourceDiffUtilCallback( + galleryItems.map { it.resource.id }, + newItems.map { it.resource.id } + ) + ) + galleryItems = newItems + diff.dispatchUpdatesTo(this) + } + + override fun getItemCount(): Int { + return galleryItems.size + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = + if (viewType == Kind.PLAINTEXT.ordinal) { + PreviewPlainTextViewHolder( + ItemPreviewPlainTextBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ), + getGestureDetector() + ) + } else { + PreviewImageViewHolder( + ItemImageBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ), + viewModel, + getGestureDetector() + ) + } + + override fun getItemViewType(position: Int) = + galleryItems[position].metadata.kind.ordinal + + @SuppressLint("ClickableViewAccessibility") + override fun onBindViewHolder( + holder: RecyclerView.ViewHolder, + position: Int + ) { + lifecycleScope.launch { + when (holder) { + is PreviewPlainTextViewHolder -> { + holder.pos = position + val item = galleryItems[position] + val text = readText(item.path) + text.onSuccess { + holder.setContent(it) + } + } + + is PreviewImageViewHolder -> { + holder.reset() + holder.pos = position + val item = galleryItems[position] + val placeholder = + ImageUtils.iconForExtension(extension(item.path)) + holder.setSource( + placeholder, + item.id(), + item.metadata, + item.preview + ) + } + } + } + } + + override fun onViewRecycled(holder: RecyclerView.ViewHolder) { + super.onViewRecycled(holder) + if (holder is PreviewImageViewHolder) { + holder.onRecycled() + } + } + + private fun getGestureDetector(): GestureDetectorCompat { + val listener = object : GestureDetector.SimpleOnGestureListener() { + override fun onSingleTapConfirmed(e: MotionEvent): Boolean { + viewModel.onPreviewsItemClick() + return true + } + } + return GestureDetectorCompat(context, listener) + } + + private suspend fun readText(source: Path): Result = + withContext(Dispatchers.IO) { + try { + val content = FileReader(source.toFile()).readText() + Result.success(content) + } catch (e: Exception) { + Result.failure(e) + } + } +} diff --git a/app/src/main/java/dev/arkbuilders/navigator/presentation/screen/gallery/previewpager/PreviewsPager.kt b/app/src/main/java/dev/arkbuilders/navigator/presentation/screen/gallery/previewpager/PreviewsPager.kt deleted file mode 100644 index e20b451e..00000000 --- a/app/src/main/java/dev/arkbuilders/navigator/presentation/screen/gallery/previewpager/PreviewsPager.kt +++ /dev/null @@ -1,81 +0,0 @@ -package dev.arkbuilders.navigator.presentation.screen.gallery.previewpager - -import android.annotation.SuppressLint -import android.content.Context -import android.view.GestureDetector -import android.view.LayoutInflater -import android.view.MotionEvent -import android.view.ViewGroup -import androidx.core.view.GestureDetectorCompat -import androidx.recyclerview.widget.RecyclerView -import dev.arkbuilders.navigator.databinding.ItemImageBinding -import dev.arkbuilders.navigator.databinding.ItemPreviewPlainTextBinding -import dev.arkbuilders.navigator.presentation.screen.gallery.GalleryPresenter -import dev.arkbuilders.arklib.data.meta.Kind - -class PreviewsPager( - val context: Context, - val presenter: GalleryPresenter -) : RecyclerView.Adapter() { - - override fun getItemCount() = presenter.galleryItems.size - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = - if (viewType == Kind.PLAINTEXT.ordinal) { - PreviewPlainTextViewHolder( - ItemPreviewPlainTextBinding.inflate( - LayoutInflater.from(parent.context), - parent, - false - ), - getGestureDetector() - ) - } else { - PreviewImageViewHolder( - ItemImageBinding.inflate( - LayoutInflater.from(parent.context), - parent, - false - ), - presenter, - getGestureDetector() - ) - } - - override fun getItemViewType(position: Int) = - presenter.getKind(position) - - @SuppressLint("ClickableViewAccessibility") - override fun onBindViewHolder( - holder: RecyclerView.ViewHolder, - position: Int - ) { - when (holder) { - is PreviewPlainTextViewHolder -> { - holder.pos = position - presenter.bindPlainTextView(holder) - } - is PreviewImageViewHolder -> { - holder.pos = position - presenter.bindView(holder) - } - } - } - - override fun onViewRecycled(holder: RecyclerView.ViewHolder) { - super.onViewRecycled(holder) - if (holder is PreviewImageViewHolder) { - holder.onRecycled() - } - } - - private fun getGestureDetector(): GestureDetectorCompat { - val listener = object : GestureDetector.SimpleOnGestureListener() { - override fun onSingleTapConfirmed(e: MotionEvent): Boolean { - presenter.onPreviewsItemClick() - return true - } - } - return GestureDetectorCompat(context, listener) - } -} diff --git a/app/src/main/java/dev/arkbuilders/navigator/presentation/screen/resources/ResourcesFragment.kt b/app/src/main/java/dev/arkbuilders/navigator/presentation/screen/resources/ResourcesFragment.kt index 7f70b0ff..68c41a24 100644 --- a/app/src/main/java/dev/arkbuilders/navigator/presentation/screen/resources/ResourcesFragment.kt +++ b/app/src/main/java/dev/arkbuilders/navigator/presentation/screen/resources/ResourcesFragment.kt @@ -28,7 +28,6 @@ import dev.arkbuilders.navigator.presentation.dialog.ConfirmationDialogFragment import dev.arkbuilders.navigator.presentation.dialog.StorageExceptionDialogFragment import dev.arkbuilders.navigator.presentation.dialog.sort.SortDialogFragment import dev.arkbuilders.navigator.presentation.dialog.tagssort.TagsSortDialogFragment -import dev.arkbuilders.navigator.presentation.screen.gallery.GalleryFragment import dev.arkbuilders.navigator.presentation.screen.main.MainActivity import dev.arkbuilders.navigator.presentation.screen.resources.adapter.ResourcesRVAdapter import dev.arkbuilders.navigator.presentation.utils.FullscreenHelper @@ -44,6 +43,7 @@ import dev.arkbuilders.arkfilepicker.folders.RootAndFav import dev.arkbuilders.arkfilepicker.presentation.onArkPathPicked import dev.arkbuilders.arklib.ResourceId import dev.arkbuilders.arklib.user.tags.Tag +import dev.arkbuilders.navigator.presentation.screen.gallery.GalleryFragment import timber.log.Timber import java.nio.file.Path import javax.inject.Inject diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c5ed6d76..c11dd963 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -131,4 +131,9 @@ Close Corrupted Storage %1$s storage is corrupted. \nCause: %2$s. \nOpening the folder isn\'t safe + Changes detected, indexing + Providing data storage + Providing metadata storage + Providing previews storage + Providing root index