From 2aad112da5849ccfdd7c30411da6f66581da5370 Mon Sep 17 00:00:00 2001 From: RadiationX Date: Mon, 4 Mar 2024 23:52:31 +0500 Subject: [PATCH] [GHP-41537911] feat: add support opening history file --- app-mobile/src/main/AndroidManifest.xml | 49 +++++++----- .../anilibria/navigation/Screens.kt | 5 +- .../ui/activities/main/IntentActivity.kt | 7 +- .../updatechecker/CheckerViewModel.kt | 6 +- .../updatechecker/UpdateCheckerActivity.kt | 2 +- .../anilibria/ui/common/ErrorHandler.kt | 7 +- .../anilibria/ui/common/LinkRouter.kt | 13 +++- .../fragments/history/HistoryFileViewModel.kt | 45 +++++++++++ .../ui/fragments/history/HistoryFragment.kt | 33 +++++++- .../ui/fragments/history/HistoryViewModel.kt | 11 ++- .../release/details/ReleaseInfoFragment.kt | 4 +- .../release/details/ReleaseInfoViewModel.kt | 10 ++- .../fragments/release/list/ReleasesAdapter.kt | 30 ++++++- .../screen/update/UpdateViewModel.kt | 3 +- .../data/datasource/holders/HistoryHolder.kt | 5 +- .../datasource/holders/ReleaseUpdateHolder.kt | 1 + .../data/datasource/storage/HistoryStorage.kt | 15 ++-- .../storage/ReleaseUpdateStorage.kt | 8 +- .../radiationx/data/downloader/LocalFile.kt | 11 +++ .../data/historyfile/HistoryFileRepository.kt | 78 +++++++++++++++++++ .../data/historyfile/mapper/Mapper.kt | 42 ++++++++++ .../historyfile/models/EpisodeAccessExport.kt | 13 ++++ .../data/historyfile/models/HistoryExport.kt | 11 +++ .../models/ReleaseHistoryExport.kt | 9 +++ .../historyfile/models/ReleaseUpdateExport.kt | 11 +++ .../data/repository/HistoryRepository.kt | 8 +- data/src/main/res/xml/filepaths.xml | 3 + .../shared_app/common/SystemUtils.kt | 26 +++---- 28 files changed, 392 insertions(+), 74 deletions(-) create mode 100644 app-mobile/src/main/java/ru/radiationx/anilibria/ui/fragments/history/HistoryFileViewModel.kt create mode 100644 data/src/main/java/ru/radiationx/data/downloader/LocalFile.kt create mode 100644 data/src/main/java/ru/radiationx/data/historyfile/HistoryFileRepository.kt create mode 100644 data/src/main/java/ru/radiationx/data/historyfile/mapper/Mapper.kt create mode 100644 data/src/main/java/ru/radiationx/data/historyfile/models/EpisodeAccessExport.kt create mode 100644 data/src/main/java/ru/radiationx/data/historyfile/models/HistoryExport.kt create mode 100644 data/src/main/java/ru/radiationx/data/historyfile/models/ReleaseHistoryExport.kt create mode 100644 data/src/main/java/ru/radiationx/data/historyfile/models/ReleaseUpdateExport.kt diff --git a/app-mobile/src/main/AndroidManifest.xml b/app-mobile/src/main/AndroidManifest.xml index c8e54cd93..0e14f383d 100644 --- a/app-mobile/src/main/AndroidManifest.xml +++ b/app-mobile/src/main/AndroidManifest.xml @@ -42,26 +42,35 @@ - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app-mobile/src/main/java/ru/radiationx/anilibria/navigation/Screens.kt b/app-mobile/src/main/java/ru/radiationx/anilibria/navigation/Screens.kt index 9a3ed8989..3ae563cac 100644 --- a/app-mobile/src/main/java/ru/radiationx/anilibria/navigation/Screens.kt +++ b/app-mobile/src/main/java/ru/radiationx/anilibria/navigation/Screens.kt @@ -2,6 +2,7 @@ package ru.radiationx.anilibria.navigation import android.content.Context import android.content.Intent +import android.net.Uri import androidx.fragment.app.Fragment import ru.radiationx.anilibria.ui.activities.SettingsActivity import ru.radiationx.anilibria.ui.activities.WebPlayerActivity @@ -105,8 +106,8 @@ object Screens { override fun getFragment() = PageFragment.newInstance(pagePath) } - class History : BaseAppScreen() { - override fun getFragment() = HistoryFragment() + class History(private val importUri: Uri? = null) : BaseAppScreen() { + override fun getFragment() = HistoryFragment.newInstance(importUri) } class Schedule(val day: Int? = null) : BaseAppScreen() { diff --git a/app-mobile/src/main/java/ru/radiationx/anilibria/ui/activities/main/IntentActivity.kt b/app-mobile/src/main/java/ru/radiationx/anilibria/ui/activities/main/IntentActivity.kt index 5085b61dc..86c3275af 100644 --- a/app-mobile/src/main/java/ru/radiationx/anilibria/ui/activities/main/IntentActivity.kt +++ b/app-mobile/src/main/java/ru/radiationx/anilibria/ui/activities/main/IntentActivity.kt @@ -4,6 +4,7 @@ import android.content.Context import android.content.Intent import android.net.Uri import android.os.Bundle +import android.widget.Toast import ru.radiationx.anilibria.navigation.Screens import ru.radiationx.anilibria.presentation.common.ILinkHandler import ru.radiationx.anilibria.ui.activities.BaseActivity @@ -35,7 +36,11 @@ class IntentActivity : BaseActivity() { val intent = Screens.Main(intentUri.toString()).getActivityIntent(this) startActivity(intent) } else { - systemUtils.externalLink(intentUri.toString()) + if (intentUri.scheme?.let { it.startsWith("https") || it.startsWith("http") } == true) { + systemUtils.externalLink(intentUri.toString()) + } else { + Toast.makeText(this, "Действие не поддерживается", Toast.LENGTH_SHORT).show() + } } } finish() diff --git a/app-mobile/src/main/java/ru/radiationx/anilibria/ui/activities/updatechecker/CheckerViewModel.kt b/app-mobile/src/main/java/ru/radiationx/anilibria/ui/activities/updatechecker/CheckerViewModel.kt index 8e5442d96..fbf137f57 100644 --- a/app-mobile/src/main/java/ru/radiationx/anilibria/ui/activities/updatechecker/CheckerViewModel.kt +++ b/app-mobile/src/main/java/ru/radiationx/anilibria/ui/activities/updatechecker/CheckerViewModel.kt @@ -10,8 +10,10 @@ import kotlinx.coroutines.launch import ru.radiationx.anilibria.presentation.common.IErrorHandler import ru.radiationx.data.analytics.features.UpdaterAnalytics import ru.radiationx.data.downloader.DownloadedFile +import ru.radiationx.data.downloader.LocalFile import ru.radiationx.data.downloader.RemoteFileRepository import ru.radiationx.data.downloader.RemoteFile +import ru.radiationx.data.downloader.toLocalFile import ru.radiationx.data.entity.domain.updater.UpdateData import ru.radiationx.data.repository.CheckerRepository import ru.radiationx.quill.QuillExtra @@ -40,7 +42,7 @@ class CheckerViewModel( MutableStateFlow>>(emptyMap()) private val _currentData = MutableStateFlow(CheckerScreenState()) - val openDownloadedFileAction = EventFlow() + val openDownloadedFileAction = EventFlow() val state = combine( _currentLoadings, @@ -98,7 +100,7 @@ class CheckerViewModel( coRunCatching { remoteFileRepository.loadFile(link.url, RemoteFile.Bucket.AppUpdates, progress) }.onSuccess { - openDownloadedFileAction.set(it) + openDownloadedFileAction.set(it.toLocalFile()) }.onFailure { errorHandler.handle(it) } diff --git a/app-mobile/src/main/java/ru/radiationx/anilibria/ui/activities/updatechecker/UpdateCheckerActivity.kt b/app-mobile/src/main/java/ru/radiationx/anilibria/ui/activities/updatechecker/UpdateCheckerActivity.kt index 5de48e1c0..beec9d0eb 100644 --- a/app-mobile/src/main/java/ru/radiationx/anilibria/ui/activities/updatechecker/UpdateCheckerActivity.kt +++ b/app-mobile/src/main/java/ru/radiationx/anilibria/ui/activities/updatechecker/UpdateCheckerActivity.kt @@ -107,7 +107,7 @@ class UpdateCheckerActivity : BaseActivity(R.layout.activity_updater) { }.launchIn(lifecycleScope) viewModel.openDownloadedFileAction.observe().onEach { - systemUtils.openDownloadedFile(it) + systemUtils.openLocalFile(it) }.launchInResumed(this) } diff --git a/app-mobile/src/main/java/ru/radiationx/anilibria/ui/common/ErrorHandler.kt b/app-mobile/src/main/java/ru/radiationx/anilibria/ui/common/ErrorHandler.kt index 03f76d904..740d843c7 100644 --- a/app-mobile/src/main/java/ru/radiationx/anilibria/ui/common/ErrorHandler.kt +++ b/app-mobile/src/main/java/ru/radiationx/anilibria/ui/common/ErrorHandler.kt @@ -1,5 +1,7 @@ package ru.radiationx.anilibria.ui.common +import com.squareup.moshi.JsonDataException +import com.squareup.moshi.JsonEncodingException import ru.radiationx.anilibria.presentation.common.IErrorHandler import ru.radiationx.anilibria.utils.messages.SystemMessenger import ru.radiationx.data.datasource.remote.ApiError @@ -12,7 +14,7 @@ import javax.inject.Inject * Created by radiationx on 23.02.18. */ class ErrorHandler @Inject constructor( - private val systemMessenger: SystemMessenger + private val systemMessenger: SystemMessenger, ) : IErrorHandler { override fun handle(throwable: Throwable, messageListener: ((Throwable, String?) -> Unit)?) { @@ -29,7 +31,8 @@ class ErrorHandler @Inject constructor( is IOException -> "Нет соединения с интернетом" is HttpException -> throwable.message is ApiError -> throwable.userMessage() - else -> throwable.message.orEmpty() + is JsonDataException -> "Неправильный формат данных" + else -> throwable.message ?: "Неизвестная ошибка" } private fun ApiError.userMessage() = when { diff --git a/app-mobile/src/main/java/ru/radiationx/anilibria/ui/common/LinkRouter.kt b/app-mobile/src/main/java/ru/radiationx/anilibria/ui/common/LinkRouter.kt index 7660b5482..7e3a09b0c 100644 --- a/app-mobile/src/main/java/ru/radiationx/anilibria/ui/common/LinkRouter.kt +++ b/app-mobile/src/main/java/ru/radiationx/anilibria/ui/common/LinkRouter.kt @@ -1,5 +1,7 @@ package ru.radiationx.anilibria.ui.common +import android.net.Uri +import android.util.Log import ru.radiationx.anilibria.navigation.BaseAppScreen import ru.radiationx.anilibria.navigation.Screens import ru.radiationx.anilibria.presentation.common.ILinkHandler @@ -14,13 +16,17 @@ import javax.inject.Inject * Created by radiationx on 03.02.18. */ class LinkRouter @Inject constructor( - private val releaseAnalytics: ReleaseAnalytics + private val releaseAnalytics: ReleaseAnalytics, ) : ILinkHandler { private val releaseDetail by lazy { Pattern.compile("\\/release\\/([\\s\\S]*?)\\.html|tracker\\/\\?ELEMENT_CODE=([^&]+)") } + private val historyImport by lazy { + Pattern.compile("^content[\\s\\S]*?\\.json\$") + } + override fun handle(url: String, router: Router?, doNavigate: Boolean): Boolean { findScreen(url)?.also { screen -> if (doNavigate) { @@ -42,6 +48,11 @@ class LinkRouter @Inject constructor( return Screens.ReleaseDetails(code = ReleaseCode(code)) } } + historyImport.matcher(url).let { + if (it.find()) { + return Screens.History(Uri.parse(url)) + } + } return null } diff --git a/app-mobile/src/main/java/ru/radiationx/anilibria/ui/fragments/history/HistoryFileViewModel.kt b/app-mobile/src/main/java/ru/radiationx/anilibria/ui/fragments/history/HistoryFileViewModel.kt new file mode 100644 index 000000000..272982b3f --- /dev/null +++ b/app-mobile/src/main/java/ru/radiationx/anilibria/ui/fragments/history/HistoryFileViewModel.kt @@ -0,0 +1,45 @@ +package ru.radiationx.anilibria.ui.fragments.history + +import android.net.Uri +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.launch +import ru.radiationx.anilibria.ui.common.ErrorHandler +import ru.radiationx.anilibria.utils.messages.SystemMessenger +import ru.radiationx.data.historyfile.HistoryFileRepository +import ru.radiationx.shared.ktx.coRunCatching +import ru.radiationx.shared_app.common.SystemUtils +import toothpick.InjectConstructor + +@InjectConstructor +class HistoryFileViewModel( + private val historyFileRepository: HistoryFileRepository, + private val systemUtils: SystemUtils, + private val errorHandler: ErrorHandler, + private val systemMessenger: SystemMessenger, +) : ViewModel() { + + fun onExportClick() { + viewModelScope.launch { + coRunCatching { + historyFileRepository.exportFile() + }.onSuccess { + systemUtils.shareLocalFile(it) + }.onFailure { + errorHandler.handle(it) + } + } + } + + fun onImportFileSelected(uri: Uri) { + viewModelScope.launch { + coRunCatching { + historyFileRepository.importFile(uri) + }.onSuccess { + systemMessenger.showMessage("История успешно импортирована") + }.onFailure { + errorHandler.handle(it) + } + } + } +} \ No newline at end of file diff --git a/app-mobile/src/main/java/ru/radiationx/anilibria/ui/fragments/history/HistoryFragment.kt b/app-mobile/src/main/java/ru/radiationx/anilibria/ui/fragments/history/HistoryFragment.kt index 04bb8307d..c6ae5736c 100644 --- a/app-mobile/src/main/java/ru/radiationx/anilibria/ui/fragments/history/HistoryFragment.kt +++ b/app-mobile/src/main/java/ru/radiationx/anilibria/ui/fragments/history/HistoryFragment.kt @@ -1,9 +1,11 @@ package ru.radiationx.anilibria.ui.fragments.history +import android.net.Uri import android.os.Bundle import android.view.MenuItem import android.view.View import android.widget.Toast +import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AlertDialog import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.core.view.doOnLayout @@ -31,7 +33,9 @@ import ru.radiationx.anilibria.ui.fragments.release.list.ReleasesAdapter import ru.radiationx.anilibria.utils.Dimensions import ru.radiationx.anilibria.utils.ToolbarHelper import ru.radiationx.quill.viewModel +import ru.radiationx.shared.ktx.android.getExtra import ru.radiationx.shared.ktx.android.postopneEnterTransitionWithTimout +import ru.radiationx.shared.ktx.android.putExtra import ru.radiationx.shared.ktx.android.showWithLifecycle /** @@ -43,6 +47,14 @@ class HistoryFragment : ReleasesAdapter.ItemListener, TopScroller { + companion object { + private const val ARG_IMPORT_URI = "import_uri" + + fun newInstance(importUri: Uri?) = HistoryFragment().putExtra { + putParcelable(ARG_IMPORT_URI, importUri) + } + } + override var sharedViewLocal: View? = null override fun getSharedView(): View? { @@ -56,6 +68,12 @@ class HistoryFragment : private val adapter = ReleasesAdapter( loadMoreListener = { }, loadRetryListener = {}, + importListener = { + importLauncher.launch("application/json") + }, + exportListener = { + fileViewModel.onExportClick() + }, listener = this, emptyPlaceHolder = PlaceholderListItem( R.drawable.ic_history, @@ -74,6 +92,13 @@ class HistoryFragment : } private val viewModel by viewModel() + private val fileViewModel by viewModel() + + private val importLauncher = registerForActivityResult(ActivityResultContracts.GetContent()) { + if (it != null) { + fileViewModel.onImportFileSelected(it) + } + } override val statusBarVisible: Boolean = true @@ -153,6 +178,12 @@ class HistoryFragment : viewModel.state.onEach { showState(it) }.launchIn(viewLifecycleOwner.lifecycleScope) + + val importUri = getExtra(ARG_IMPORT_URI) + if (importUri != null) { + fileViewModel.onImportFileSelected(importUri) + arguments?.remove(ARG_IMPORT_URI) + } } override fun updateDimens(dimensions: Dimensions) { @@ -208,7 +239,7 @@ class HistoryFragment : private fun showState(state: HistoryScreenState) { binding.progressBarList.isVisible = state.data.emptyLoading binding.refreshLayout.isRefreshing = state.data.refreshLoading - adapter.bindState(state.data) + adapter.bindState(state.data, withExport = true) searchAdapter.items = state.searchItems.map { ReleaseListItem(it) } } } \ No newline at end of file diff --git a/app-mobile/src/main/java/ru/radiationx/anilibria/ui/fragments/history/HistoryViewModel.kt b/app-mobile/src/main/java/ru/radiationx/anilibria/ui/fragments/history/HistoryViewModel.kt index bebe870af..02799e485 100644 --- a/app-mobile/src/main/java/ru/radiationx/anilibria/ui/fragments/history/HistoryViewModel.kt +++ b/app-mobile/src/main/java/ru/radiationx/anilibria/ui/fragments/history/HistoryViewModel.kt @@ -2,7 +2,14 @@ package ru.radiationx.anilibria.ui.fragments.history import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import ru.radiationx.anilibria.model.ReleaseItemState import ru.radiationx.anilibria.model.loading.DataLoadingController @@ -32,7 +39,7 @@ class HistoryViewModel( private val historyAnalytics: HistoryAnalytics, private val releaseAnalytics: ReleaseAnalytics, private val shortcutHelper: ShortcutHelper, - private val systemUtils: SystemUtils + private val systemUtils: SystemUtils, ) : ViewModel() { private val loadingController = DataLoadingController(viewModelScope) { diff --git a/app-mobile/src/main/java/ru/radiationx/anilibria/ui/fragments/release/details/ReleaseInfoFragment.kt b/app-mobile/src/main/java/ru/radiationx/anilibria/ui/fragments/release/details/ReleaseInfoFragment.kt index d012e18ad..4c590eda1 100644 --- a/app-mobile/src/main/java/ru/radiationx/anilibria/ui/fragments/release/details/ReleaseInfoFragment.kt +++ b/app-mobile/src/main/java/ru/radiationx/anilibria/ui/fragments/release/details/ReleaseInfoFragment.kt @@ -103,11 +103,11 @@ class ReleaseInfoFragment : BaseDimensionsFragment(R.layout.fragment_list), TopS }.launchInResumed(viewLifecycleOwner) viewModel.openDownloadedFileAction.observe().onEach { - systemUtils.openDownloadedFile(it) + systemUtils.openLocalFile(it) }.launchInResumed(viewLifecycleOwner) viewModel.shareDownloadedFileAction.observe().onEach { - systemUtils.shareDownloadedFile(it) + systemUtils.shareLocalFile(it) }.launchInResumed(viewLifecycleOwner) } diff --git a/app-mobile/src/main/java/ru/radiationx/anilibria/ui/fragments/release/details/ReleaseInfoViewModel.kt b/app-mobile/src/main/java/ru/radiationx/anilibria/ui/fragments/release/details/ReleaseInfoViewModel.kt index e773c6760..67ae15709 100644 --- a/app-mobile/src/main/java/ru/radiationx/anilibria/ui/fragments/release/details/ReleaseInfoViewModel.kt +++ b/app-mobile/src/main/java/ru/radiationx/anilibria/ui/fragments/release/details/ReleaseInfoViewModel.kt @@ -34,8 +34,10 @@ import ru.radiationx.data.analytics.features.mapper.toAnalyticsQuality import ru.radiationx.data.analytics.features.model.AnalyticsPlayer import ru.radiationx.data.datasource.holders.PreferencesHolder import ru.radiationx.data.downloader.DownloadedFile +import ru.radiationx.data.downloader.LocalFile import ru.radiationx.data.downloader.RemoteFile import ru.radiationx.data.downloader.RemoteFileRepository +import ru.radiationx.data.downloader.toLocalFile import ru.radiationx.data.entity.common.AuthState import ru.radiationx.data.entity.common.PlayerQuality import ru.radiationx.data.entity.domain.release.Episode @@ -102,8 +104,8 @@ class ReleaseInfoViewModel( val showFileDonateAction = EventFlow() val showEpisodesMenuAction = EventFlow() val showContextEpisodeAction = EventFlow() - val openDownloadedFileAction = EventFlow() - val shareDownloadedFileAction = EventFlow() + val openDownloadedFileAction = EventFlow() + val shareDownloadedFileAction = EventFlow() private fun updateModifiers(block: (ReleaseDetailModifiersState) -> ReleaseDetailModifiersState) { _state.update { @@ -273,8 +275,8 @@ class ReleaseInfoViewModel( remoteFileRepository.loadFile(url, bucket, progress) }.onSuccess { when (action) { - TorrentAction.Open -> openDownloadedFileAction.set(it) - TorrentAction.Share -> shareDownloadedFileAction.set(it) + TorrentAction.Open -> openDownloadedFileAction.set(it.toLocalFile()) + TorrentAction.Share -> shareDownloadedFileAction.set(it.toLocalFile()) TorrentAction.OpenUrl, TorrentAction.ShareUrl -> { // do nothing } diff --git a/app-mobile/src/main/java/ru/radiationx/anilibria/ui/fragments/release/list/ReleasesAdapter.kt b/app-mobile/src/main/java/ru/radiationx/anilibria/ui/fragments/release/list/ReleasesAdapter.kt index bd3a20487..5ecdfbf85 100644 --- a/app-mobile/src/main/java/ru/radiationx/anilibria/ui/fragments/release/list/ReleasesAdapter.kt +++ b/app-mobile/src/main/java/ru/radiationx/anilibria/ui/fragments/release/list/ReleasesAdapter.kt @@ -3,14 +3,18 @@ package ru.radiationx.anilibria.ui.fragments.release.list import ru.radiationx.anilibria.model.ReleaseItemState import ru.radiationx.anilibria.model.loading.DataLoadingState import ru.radiationx.anilibria.model.loading.needShowPlaceholder +import ru.radiationx.anilibria.ui.adapters.DividerShadowListItem +import ru.radiationx.anilibria.ui.adapters.FeedSectionListItem import ru.radiationx.anilibria.ui.adapters.ListItem import ru.radiationx.anilibria.ui.adapters.LoadErrorListItem import ru.radiationx.anilibria.ui.adapters.LoadMoreListItem import ru.radiationx.anilibria.ui.adapters.PlaceholderDelegate import ru.radiationx.anilibria.ui.adapters.PlaceholderListItem import ru.radiationx.anilibria.ui.adapters.ReleaseListItem +import ru.radiationx.anilibria.ui.adapters.feed.FeedSectionDelegate import ru.radiationx.anilibria.ui.adapters.global.LoadErrorDelegate import ru.radiationx.anilibria.ui.adapters.global.LoadMoreDelegate +import ru.radiationx.anilibria.ui.adapters.other.DividerShadowItemDelegate import ru.radiationx.anilibria.ui.adapters.release.list.ReleaseItemDelegate import ru.radiationx.anilibria.ui.common.adapters.ListItemAdapter @@ -19,26 +23,50 @@ import ru.radiationx.anilibria.ui.common.adapters.ListItemAdapter class ReleasesAdapter( loadMoreListener: () -> Unit, loadRetryListener: () -> Unit, + importListener: (() -> Unit)? = null, + exportListener: (() -> Unit)? = null, listener: ItemListener, private val emptyPlaceHolder: PlaceholderListItem, private val errorPlaceHolder: PlaceholderListItem, ) : ListItemAdapter() { + companion object { + private const val TAG_IMPORT = "import" + private const val TAG_EXPORT = "expor" + } + init { addDelegate(ReleaseItemDelegate(listener)) addDelegate(LoadMoreDelegate(loadMoreListener)) addDelegate(LoadErrorDelegate(loadRetryListener)) addDelegate(PlaceholderDelegate()) + addDelegate(FeedSectionDelegate { + when (it.tag) { + TAG_IMPORT -> importListener?.invoke() + TAG_EXPORT -> exportListener?.invoke() + } + }) + addDelegate(DividerShadowItemDelegate()) } - fun bindState(loadingState: DataLoadingState>) { + fun bindState( + loadingState: DataLoadingState>, + withExport: Boolean = false, + ) { val newItems = mutableListOf() + if (withExport) { + newItems.add(FeedSectionListItem(TAG_IMPORT, "Импортировать историю", hasBg = true)) + newItems.add(FeedSectionListItem(TAG_EXPORT, "Экспортировать историю", hasBg = true)) + } getPlaceholder(loadingState)?.also { newItems.add(it) } loadingState.data?.let { data -> + if (withExport) { + newItems.add(DividerShadowListItem("history")) + } newItems.addAll(data.map { ReleaseListItem(it) }) } diff --git a/app-tv/src/main/java/ru/radiationx/anilibria/screen/update/UpdateViewModel.kt b/app-tv/src/main/java/ru/radiationx/anilibria/screen/update/UpdateViewModel.kt index bca8470d0..b9f78d778 100644 --- a/app-tv/src/main/java/ru/radiationx/anilibria/screen/update/UpdateViewModel.kt +++ b/app-tv/src/main/java/ru/radiationx/anilibria/screen/update/UpdateViewModel.kt @@ -11,6 +11,7 @@ import ru.radiationx.anilibria.screen.LifecycleViewModel import ru.radiationx.anilibria.screen.UpdateSourceScreen import ru.radiationx.data.downloader.RemoteFile import ru.radiationx.data.downloader.RemoteFileRepository +import ru.radiationx.data.downloader.toLocalFile import ru.radiationx.data.entity.domain.updater.UpdateData import ru.radiationx.data.repository.CheckerRepository import ru.radiationx.shared.ktx.coRunCatching @@ -91,7 +92,7 @@ class UpdateViewModel( downloadProgressData ) }.onSuccess { - systemUtils.openDownloadedFile(it) + systemUtils.openLocalFile(it.toLocalFile()) }.onFailure { Timber.e(it) } diff --git a/data/src/main/java/ru/radiationx/data/datasource/holders/HistoryHolder.kt b/data/src/main/java/ru/radiationx/data/datasource/holders/HistoryHolder.kt index e82792ba7..0fe7f3175 100644 --- a/data/src/main/java/ru/radiationx/data/datasource/holders/HistoryHolder.kt +++ b/data/src/main/java/ru/radiationx/data/datasource/holders/HistoryHolder.kt @@ -1,13 +1,12 @@ package ru.radiationx.data.datasource.holders import kotlinx.coroutines.flow.Flow -import ru.radiationx.data.entity.domain.release.Release import ru.radiationx.data.entity.domain.types.ReleaseId interface HistoryHolder { suspend fun getEpisodes(): List fun observeEpisodes(): Flow> - suspend fun putRelease(release: Release) - suspend fun putAllRelease(releases: List) + suspend fun putRelease(id: ReleaseId) + suspend fun putAllRelease(ids: List) suspend fun removerRelease(id: ReleaseId) } \ No newline at end of file diff --git a/data/src/main/java/ru/radiationx/data/datasource/holders/ReleaseUpdateHolder.kt b/data/src/main/java/ru/radiationx/data/datasource/holders/ReleaseUpdateHolder.kt index e3077566a..3196dbecb 100644 --- a/data/src/main/java/ru/radiationx/data/datasource/holders/ReleaseUpdateHolder.kt +++ b/data/src/main/java/ru/radiationx/data/datasource/holders/ReleaseUpdateHolder.kt @@ -11,4 +11,5 @@ interface ReleaseUpdateHolder { suspend fun getRelease(id: ReleaseId): ReleaseUpdate? suspend fun viewRelease(release: Release) suspend fun putInitialRelease(releases: List) + suspend fun putAllRelease(releases: List) } \ No newline at end of file diff --git a/data/src/main/java/ru/radiationx/data/datasource/storage/HistoryStorage.kt b/data/src/main/java/ru/radiationx/data/datasource/storage/HistoryStorage.kt index 6c4905c7b..396b2f7fc 100644 --- a/data/src/main/java/ru/radiationx/data/datasource/storage/HistoryStorage.kt +++ b/data/src/main/java/ru/radiationx/data/datasource/storage/HistoryStorage.kt @@ -9,7 +9,6 @@ import org.json.JSONObject import ru.radiationx.data.DataPreferences import ru.radiationx.data.datasource.SuspendMutableStateFlow import ru.radiationx.data.datasource.holders.HistoryHolder -import ru.radiationx.data.entity.domain.release.Release import ru.radiationx.data.entity.domain.types.ReleaseId import ru.radiationx.shared.ktx.android.mapObjects import javax.inject.Inject @@ -33,26 +32,26 @@ class HistoryStorage @Inject constructor( override fun observeEpisodes(): Flow> = localReleasesRelay - override suspend fun putRelease(release: Release) { + override suspend fun putRelease(id: ReleaseId) { localReleasesRelay.update { localReleases -> val mutableLocalReleases = localReleases.toMutableList() mutableLocalReleases - .firstOrNull { it == release.id } + .firstOrNull { it == id } ?.let { mutableLocalReleases.remove(it) } - mutableLocalReleases.add(release.id) + mutableLocalReleases.add(id) mutableLocalReleases } saveAll() } - override suspend fun putAllRelease(releases: List) { + override suspend fun putAllRelease(ids: List) { localReleasesRelay.update { localReleases -> val mutableLocalReleases = localReleases.toMutableList() - releases.forEach { release -> + ids.forEach { id -> mutableLocalReleases - .firstOrNull { it == release.id } + .firstOrNull { it == id } ?.let { mutableLocalReleases.remove(it) } - mutableLocalReleases.add(release.id) + mutableLocalReleases.add(id) } mutableLocalReleases } diff --git a/data/src/main/java/ru/radiationx/data/datasource/storage/ReleaseUpdateStorage.kt b/data/src/main/java/ru/radiationx/data/datasource/storage/ReleaseUpdateStorage.kt index f1a02cb3f..d85965f1b 100644 --- a/data/src/main/java/ru/radiationx/data/datasource/storage/ReleaseUpdateStorage.kt +++ b/data/src/main/java/ru/radiationx/data/datasource/storage/ReleaseUpdateStorage.kt @@ -22,7 +22,7 @@ import javax.inject.Inject */ class ReleaseUpdateStorage @Inject constructor( @DataPreferences private val sharedPreferences: SharedPreferences, - private val moshi: Moshi + private val moshi: Moshi, ) : ReleaseUpdateHolder { companion object { @@ -53,7 +53,7 @@ class ReleaseUpdateStorage @Inject constructor( timestamp = release.torrentUpdate, lastOpenTimestamp = updItem.timestamp ) - updAllRelease(listOf(newUpdItem)) + putAllRelease(listOf(newUpdItem)) } } @@ -70,10 +70,10 @@ class ReleaseUpdateStorage @Inject constructor( putReleases.add(update) } } - updAllRelease(putReleases) + putAllRelease(putReleases) } - private suspend fun updAllRelease(releases: List) { + override suspend fun putAllRelease(releases: List) { localReleasesRelay.update { updates -> val newIds = releases.map { it.id } updates diff --git a/data/src/main/java/ru/radiationx/data/downloader/LocalFile.kt b/data/src/main/java/ru/radiationx/data/downloader/LocalFile.kt new file mode 100644 index 000000000..033c7d920 --- /dev/null +++ b/data/src/main/java/ru/radiationx/data/downloader/LocalFile.kt @@ -0,0 +1,11 @@ +package ru.radiationx.data.downloader + +import java.io.File + +data class LocalFile( + val file: File, + val name: String, + val mimeType: String, +) + +fun DownloadedFile.toLocalFile() = LocalFile(local, remote.name, remote.mimeType) diff --git a/data/src/main/java/ru/radiationx/data/historyfile/HistoryFileRepository.kt b/data/src/main/java/ru/radiationx/data/historyfile/HistoryFileRepository.kt new file mode 100644 index 000000000..7ca0e583a --- /dev/null +++ b/data/src/main/java/ru/radiationx/data/historyfile/HistoryFileRepository.kt @@ -0,0 +1,78 @@ +package ru.radiationx.data.historyfile + +import android.content.Context +import android.net.Uri +import com.squareup.moshi.Moshi +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import okio.buffer +import okio.sink +import okio.source +import ru.radiationx.data.datasource.holders.EpisodesCheckerHolder +import ru.radiationx.data.datasource.holders.HistoryHolder +import ru.radiationx.data.datasource.holders.ReleaseUpdateHolder +import ru.radiationx.data.downloader.LocalFile +import ru.radiationx.data.historyfile.mapper.toDomain +import ru.radiationx.data.historyfile.mapper.toExport +import ru.radiationx.data.historyfile.models.HistoryExport +import java.io.File +import java.io.FileInputStream +import java.io.FileOutputStream +import java.text.SimpleDateFormat +import java.util.Date +import javax.inject.Inject + +class HistoryFileRepository @Inject constructor( + private val context: Context, + private val historyHolder: HistoryHolder, + private val releaseHolder: ReleaseUpdateHolder, + private val episodesHolder: EpisodesCheckerHolder, + private val moshi: Moshi, +) { + + private val dataAdapter by lazy { + moshi.adapter(HistoryExport::class.java) + } + + suspend fun exportFile(): LocalFile { + return withContext(Dispatchers.IO) { + val data = HistoryExport( + history = historyHolder.getEpisodes().map { it.toExport() }, + updates = releaseHolder.getReleases().map { it.toExport() }, + episodes = episodesHolder.getEpisodes().map { it.toExport() } + ) + val date = SimpleDateFormat("ddMMyyyyHHmm").format(Date()) + + val file = File(getCacheDir(),"anilibria_history_${date}.json") + FileOutputStream(file).sink().buffer().use { + dataAdapter.toJson(it, data) + } + LocalFile(file, file.name, "application/json") + } + } + + suspend fun importFile(uri: Uri) { + withContext(Dispatchers.IO) { + val data = context.contentResolver.openFileDescriptor(uri, "r").use { descriptor -> + requireNotNull(descriptor) { + "File descriptor is null" + } + FileInputStream(descriptor.fileDescriptor).source().buffer().use { + dataAdapter.fromJson(it) + } + } + requireNotNull(data) { + "Readed data by file is null" + } + historyHolder.putAllRelease(data.history.map { it.toDomain() }) + releaseHolder.putAllRelease(data.updates.map { it.toDomain() }) + episodesHolder.putAllEpisode(data.episodes.map { it.toDomain() }) + } + } + + private fun getCacheDir(): File { + val file = File(context.cacheDir, "anilibria_export") + file.mkdir() + return file + } +} \ No newline at end of file diff --git a/data/src/main/java/ru/radiationx/data/historyfile/mapper/Mapper.kt b/data/src/main/java/ru/radiationx/data/historyfile/mapper/Mapper.kt new file mode 100644 index 000000000..f5a8d0404 --- /dev/null +++ b/data/src/main/java/ru/radiationx/data/historyfile/mapper/Mapper.kt @@ -0,0 +1,42 @@ +package ru.radiationx.data.historyfile.mapper + +import ru.radiationx.data.entity.domain.release.EpisodeAccess +import ru.radiationx.data.entity.domain.release.ReleaseUpdate +import ru.radiationx.data.entity.domain.types.EpisodeId +import ru.radiationx.data.entity.domain.types.ReleaseId +import ru.radiationx.data.historyfile.models.EpisodeAccessExport +import ru.radiationx.data.historyfile.models.ReleaseHistoryExport +import ru.radiationx.data.historyfile.models.ReleaseUpdateExport + +fun EpisodeAccessExport.toDomain() = EpisodeAccess( + id = EpisodeId(id, ReleaseId(releaseId)), + seek = seek, + isViewed = isViewed, + lastAccess = lastAccess +) + +fun ReleaseUpdateExport.toDomain() = ReleaseUpdate( + id = ReleaseId(id), + timestamp = timestamp, + lastOpenTimestamp = lastOpenTimestamp +) + +fun ReleaseHistoryExport.toDomain() = ReleaseId(id) + +fun EpisodeAccess.toExport() = EpisodeAccessExport( + id = id.id, + releaseId = id.releaseId.id, + seek = seek, + isViewed = isViewed, + lastAccess = lastAccess +) + +fun ReleaseUpdate.toExport() = ReleaseUpdateExport( + id = id.id, + timestamp = timestamp, + lastOpenTimestamp = lastOpenTimestamp +) + +fun ReleaseId.toExport() = ReleaseHistoryExport( + id = id +) \ No newline at end of file diff --git a/data/src/main/java/ru/radiationx/data/historyfile/models/EpisodeAccessExport.kt b/data/src/main/java/ru/radiationx/data/historyfile/models/EpisodeAccessExport.kt new file mode 100644 index 000000000..07e291099 --- /dev/null +++ b/data/src/main/java/ru/radiationx/data/historyfile/models/EpisodeAccessExport.kt @@ -0,0 +1,13 @@ +package ru.radiationx.data.historyfile.models + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class EpisodeAccessExport( + @Json(name = "eid") val id: String, + @Json(name = "rid") val releaseId: Int, + @Json(name = "s") val seek: Long, + @Json(name = "iv") val isViewed: Boolean, + @Json(name = "la") val lastAccess: Long, +) \ No newline at end of file diff --git a/data/src/main/java/ru/radiationx/data/historyfile/models/HistoryExport.kt b/data/src/main/java/ru/radiationx/data/historyfile/models/HistoryExport.kt new file mode 100644 index 000000000..980fbb44a --- /dev/null +++ b/data/src/main/java/ru/radiationx/data/historyfile/models/HistoryExport.kt @@ -0,0 +1,11 @@ +package ru.radiationx.data.historyfile.models + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class HistoryExport( + @Json(name = "history") val history: List, + @Json(name = "updates") val updates: List, + @Json(name = "episodes") val episodes: List, +) \ No newline at end of file diff --git a/data/src/main/java/ru/radiationx/data/historyfile/models/ReleaseHistoryExport.kt b/data/src/main/java/ru/radiationx/data/historyfile/models/ReleaseHistoryExport.kt new file mode 100644 index 000000000..ea665f9c9 --- /dev/null +++ b/data/src/main/java/ru/radiationx/data/historyfile/models/ReleaseHistoryExport.kt @@ -0,0 +1,9 @@ +package ru.radiationx.data.historyfile.models + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class ReleaseHistoryExport( + @Json(name = "rid") val id: Int, +) \ No newline at end of file diff --git a/data/src/main/java/ru/radiationx/data/historyfile/models/ReleaseUpdateExport.kt b/data/src/main/java/ru/radiationx/data/historyfile/models/ReleaseUpdateExport.kt new file mode 100644 index 000000000..e4bf810b4 --- /dev/null +++ b/data/src/main/java/ru/radiationx/data/historyfile/models/ReleaseUpdateExport.kt @@ -0,0 +1,11 @@ +package ru.radiationx.data.historyfile.models + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class ReleaseUpdateExport( + @Json(name = "rid") val id: Int, + @Json(name = "ts") val timestamp: Int, + @Json(name = "lots") val lastOpenTimestamp: Int +) \ No newline at end of file diff --git a/data/src/main/java/ru/radiationx/data/repository/HistoryRepository.kt b/data/src/main/java/ru/radiationx/data/repository/HistoryRepository.kt index 92844bae0..b64de7d1d 100644 --- a/data/src/main/java/ru/radiationx/data/repository/HistoryRepository.kt +++ b/data/src/main/java/ru/radiationx/data/repository/HistoryRepository.kt @@ -2,7 +2,11 @@ package ru.radiationx.data.repository import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map import kotlinx.coroutines.withContext import ru.radiationx.data.datasource.holders.HistoryHolder import ru.radiationx.data.datasource.holders.ReleaseUpdateHolder @@ -39,7 +43,7 @@ class HistoryRepository @Inject constructor( suspend fun putRelease(releaseItem: Release) { withContext(Dispatchers.IO) { - historyStorage.putRelease(releaseItem) + historyStorage.putRelease(releaseItem.id) updateHolder.viewRelease(releaseItem) } } diff --git a/data/src/main/res/xml/filepaths.xml b/data/src/main/res/xml/filepaths.xml index e7c6bd213..c02d7986a 100644 --- a/data/src/main/res/xml/filepaths.xml +++ b/data/src/main/res/xml/filepaths.xml @@ -3,4 +3,7 @@ + diff --git a/shared-app/src/main/java/ru/radiationx/shared_app/common/SystemUtils.kt b/shared-app/src/main/java/ru/radiationx/shared_app/common/SystemUtils.kt index 0af2cb151..5d62ed929 100644 --- a/shared-app/src/main/java/ru/radiationx/shared_app/common/SystemUtils.kt +++ b/shared-app/src/main/java/ru/radiationx/shared_app/common/SystemUtils.kt @@ -8,7 +8,7 @@ import android.content.Intent import android.content.pm.PackageManager import android.net.Uri import androidx.core.content.FileProvider -import ru.radiationx.data.downloader.DownloadedFile +import ru.radiationx.data.downloader.LocalFile import toothpick.InjectConstructor import java.io.File @@ -25,19 +25,11 @@ class SystemUtils( } } - fun openDownloadedFile(file: DownloadedFile) { - openRemoteFile(file.local, file.remote.name, file.remote.mimeType) - } - - fun shareDownloadedFile(file: DownloadedFile) { - shareRemoteFile(file.local, file.remote.name, file.remote.mimeType) - } - - private fun openRemoteFile(file: File, name: String, mimeType: String) { - val data = getRemoteFileUri(file, name) + fun openLocalFile(file: LocalFile) { + val data = getRemoteFileUri(file.file, file.name) val intent = Intent(Intent.ACTION_VIEW).apply { - setDataAndType(data, mimeType) - putExtra(Intent.EXTRA_TITLE, name) + setDataAndType(data, file.mimeType) + putExtra(Intent.EXTRA_TITLE, file.name) addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); } val chooserIntent = Intent.createChooser(intent, "Открыть в").apply { @@ -46,12 +38,12 @@ class SystemUtils( context.startActivity(chooserIntent); } - private fun shareRemoteFile(file: File, name: String, mimeType: String) { - val data = getRemoteFileUri(file, name) + fun shareLocalFile(file: LocalFile) { + val data = getRemoteFileUri(file.file, file.name) val sendIntent = Intent(Intent.ACTION_SEND).apply { - type = mimeType - putExtra(Intent.EXTRA_TITLE, name) + type = file.mimeType + putExtra(Intent.EXTRA_TITLE, file.name) putExtra(Intent.EXTRA_STREAM, data) addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) }