From 0d171f23b2b4ceaf18075acfffb6441493433b07 Mon Sep 17 00:00:00 2001 From: HeroBrine1st Erquilenne Date: Sat, 30 Mar 2024 19:33:54 +0300 Subject: [PATCH] Add download media feature --- .../e621/module/ActivityInjectionCompanion.kt | 1 + .../e621/module/DownloadManagerModule.kt | 61 +++++++++++++++++++ .../component/PostMediaComponent.kt | 9 ++- .../component/post/PostComponent.kt | 7 +++ .../component/root/RootComponentImpl.kt | 4 +- .../ui/component/scaffold/ActionBarMenu.kt | 4 +- .../e621/ui/screen/PostMediaScreen.kt | 29 +++++++-- .../herobrine1st/e621/ui/screen/post/Post.kt | 11 +++- app/src/main/res/values-ru-rRU/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + 10 files changed, 119 insertions(+), 9 deletions(-) create mode 100644 app/src/main/java/ru/herobrine1st/e621/module/DownloadManagerModule.kt diff --git a/app/src/main/java/ru/herobrine1st/e621/module/ActivityInjectionCompanion.kt b/app/src/main/java/ru/herobrine1st/e621/module/ActivityInjectionCompanion.kt index ae5a6f45..a6e1b38b 100644 --- a/app/src/main/java/ru/herobrine1st/e621/module/ActivityInjectionCompanion.kt +++ b/app/src/main/java/ru/herobrine1st/e621/module/ActivityInjectionCompanion.kt @@ -47,6 +47,7 @@ class ActivityInjectionCompanion( ) val mediaModule = MediaModule(applicationContext) + val downloadManagerModule = DownloadManagerModule(applicationContext) val databaseModule by applicationInjectionCompanion::databaseModule val snackbarModule by applicationInjectionCompanion::snackbarModule diff --git a/app/src/main/java/ru/herobrine1st/e621/module/DownloadManagerModule.kt b/app/src/main/java/ru/herobrine1st/e621/module/DownloadManagerModule.kt new file mode 100644 index 00000000..47a28ea4 --- /dev/null +++ b/app/src/main/java/ru/herobrine1st/e621/module/DownloadManagerModule.kt @@ -0,0 +1,61 @@ +/* + * This file is part of ru.herobrine1st.e621. + * + * ru.herobrine1st.e621 is an android client for https://e621.net + * Copyright (C) 2022-2024 HeroBrine1st Erquilenne + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package ru.herobrine1st.e621.module + +import android.app.DownloadManager +import android.content.Context +import android.net.Uri +import android.os.Environment +import ru.herobrine1st.e621.api.model.FileType +import ru.herobrine1st.e621.api.model.NormalizedFile + +class DownloadManagerModule(context: Context) { + private val _downloadManager = + context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager + + val downloadManager: IDownloadManager = DownloadManagerImpl(_downloadManager) + + private class DownloadManagerImpl(val proxy: DownloadManager) : + IDownloadManager { + override fun downloadFile(file: NormalizedFile) { + val url = file.urls.firstNotNullOfOrNull { it }?.let { Uri.parse(it) } ?: return + val request = DownloadManager.Request(url) + request.setTitle(url.pathSegments.last()) + request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED) + + val directory = when (file.type) { + FileType.JPG, FileType.PNG, FileType.GIF -> Environment.DIRECTORY_PICTURES + FileType.WEBM -> Environment.DIRECTORY_MOVIES + FileType.SWF, FileType.UNDEFINED -> Environment.DIRECTORY_DOWNLOADS + } + + request.setDestinationInExternalPublicDir( + directory, + "E621/${url.pathSegments.last()}" + ) + proxy.enqueue(request) + } + } +} + +interface IDownloadManager { + fun downloadFile(file: NormalizedFile) +} \ No newline at end of file diff --git a/app/src/main/java/ru/herobrine1st/e621/navigation/component/PostMediaComponent.kt b/app/src/main/java/ru/herobrine1st/e621/navigation/component/PostMediaComponent.kt index fa19dd69..979573fd 100644 --- a/app/src/main/java/ru/herobrine1st/e621/navigation/component/PostMediaComponent.kt +++ b/app/src/main/java/ru/herobrine1st/e621/navigation/component/PostMediaComponent.kt @@ -26,10 +26,12 @@ import androidx.compose.runtime.setValue import com.arkivanov.decompose.ComponentContext import ru.herobrine1st.e621.api.model.NormalizedFile import ru.herobrine1st.e621.api.model.Post +import ru.herobrine1st.e621.module.IDownloadManager class PostMediaComponent( - post: Post, + private val post: Post, initialFile: NormalizedFile, + private val downloadManager: IDownloadManager, componentContext: ComponentContext, ) : ComponentContext by componentContext { val files = post.files @@ -38,7 +40,6 @@ class PostMediaComponent( check(initialFile in files) { "Initial file does not belong to post" } } - var currentFile by mutableStateOf(initialFile) private set @@ -46,4 +47,8 @@ class PostMediaComponent( check(file in files) { "File does not belong to post" } currentFile = file } + + fun downloadFile() { + downloadManager.downloadFile(post.normalizedFile) + } } \ No newline at end of file diff --git a/app/src/main/java/ru/herobrine1st/e621/navigation/component/post/PostComponent.kt b/app/src/main/java/ru/herobrine1st/e621/navigation/component/post/PostComponent.kt index 01c6a4ca..76eb8a7a 100644 --- a/app/src/main/java/ru/herobrine1st/e621/navigation/component/post/PostComponent.kt +++ b/app/src/main/java/ru/herobrine1st/e621/navigation/component/post/PostComponent.kt @@ -56,6 +56,7 @@ import ru.herobrine1st.e621.api.model.selectSample import ru.herobrine1st.e621.api.search.PoolSearchOptions import ru.herobrine1st.e621.api.search.PostsSearchOptions import ru.herobrine1st.e621.api.search.SearchOptions +import ru.herobrine1st.e621.module.IDownloadManager import ru.herobrine1st.e621.navigation.LifecycleScope import ru.herobrine1st.e621.navigation.component.VideoPlayerComponent import ru.herobrine1st.e621.navigation.component.posts.handleFavouriteChange @@ -86,6 +87,7 @@ class PostComponent( private val favouritesCache: FavouritesCache, private val snackbarAdapter: SnackbarAdapter, private val mediaOkHttpClientProvider: Lazy, + private val downloadManager: IDownloadManager, ) : ComponentContext by componentContext { private val instance = instanceKeeper.getOrCreate { Instance(postId, api, exceptionReporter) @@ -329,6 +331,11 @@ class PostComponent( ) } + fun downloadFile() { + val post = (state as? PostState.Ready) ?: return + downloadManager.downloadFile(post.post.normalizedFile) + } + class Instance( postId: PostId, api: API, diff --git a/app/src/main/java/ru/herobrine1st/e621/navigation/component/root/RootComponentImpl.kt b/app/src/main/java/ru/herobrine1st/e621/navigation/component/root/RootComponentImpl.kt index 3994f1ae..b07720f1 100644 --- a/app/src/main/java/ru/herobrine1st/e621/navigation/component/root/RootComponentImpl.kt +++ b/app/src/main/java/ru/herobrine1st/e621/navigation/component/root/RootComponentImpl.kt @@ -125,7 +125,8 @@ class RootComponentImpl( injectionCompanion.apiModule.api, injectionCompanion.favouritesCache, injectionCompanion.snackbarModule.snackbarAdapter, - injectionCompanion.mediaModule.mediaOkHttpClientLazy + injectionCompanion.mediaModule.mediaOkHttpClientLazy, + injectionCompanion.downloadManagerModule.downloadManager ) ) is Settings -> Child.Settings(SettingsComponent(context)) @@ -169,6 +170,7 @@ class RootComponentImpl( PostMediaComponent( configuration.post, configuration.initialFile, + injectionCompanion.downloadManagerModule.downloadManager, context ) ) diff --git a/app/src/main/java/ru/herobrine1st/e621/ui/component/scaffold/ActionBarMenu.kt b/app/src/main/java/ru/herobrine1st/e621/ui/component/scaffold/ActionBarMenu.kt index a48b39bd..358af215 100644 --- a/app/src/main/java/ru/herobrine1st/e621/ui/component/scaffold/ActionBarMenu.kt +++ b/app/src/main/java/ru/herobrine1st/e621/ui/component/scaffold/ActionBarMenu.kt @@ -61,7 +61,8 @@ fun MenuAction( @Composable fun ActionBarMenu( onNavigateToSettings: () -> Unit, - onOpenBlacklistDialog: () -> Unit + onOpenBlacklistDialog: () -> Unit, + additionalMenuActions: @Composable () -> Unit = {}, ) { var openMenu by remember { mutableStateOf(false) } @@ -76,6 +77,7 @@ fun ActionBarMenu( expanded = openMenu, onDismissRequest = { openMenu = false } ) { + additionalMenuActions() MenuAction(Icons.Outlined.Block, stringResource(R.string.blacklist)) { openMenu = false onOpenBlacklistDialog() diff --git a/app/src/main/java/ru/herobrine1st/e621/ui/screen/PostMediaScreen.kt b/app/src/main/java/ru/herobrine1st/e621/ui/screen/PostMediaScreen.kt index f45a52c9..6fb587ea 100644 --- a/app/src/main/java/ru/herobrine1st/e621/ui/screen/PostMediaScreen.kt +++ b/app/src/main/java/ru/herobrine1st/e621/ui/screen/PostMediaScreen.kt @@ -34,9 +34,14 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Animation +import androidx.compose.material.icons.filled.Download +import androidx.compose.material.icons.filled.Image import androidx.compose.material.icons.filled.MoreVert +import androidx.compose.material.icons.filled.Movie import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.LocalContentColor @@ -55,6 +60,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import ru.herobrine1st.e621.R +import ru.herobrine1st.e621.api.model.FileType import ru.herobrine1st.e621.navigation.component.PostMediaComponent import ru.herobrine1st.e621.ui.component.MAX_SCALE_DEFAULT import ru.herobrine1st.e621.ui.component.post.PostImage @@ -131,22 +137,37 @@ fun PostMediaScreen( ) Spacer(Modifier.weight(1f)) Box { - var showVariants by remember { mutableStateOf(false) } - IconButton(onClick = { showVariants = true }) { + var showMenu by remember { mutableStateOf(false) } + IconButton(onClick = { showMenu = true }) { Icon( Icons.Default.MoreVert, contentDescription = stringResource(R.string.image_fullscreen_select_image_variant) ) } DropdownMenu( - expanded = showVariants, - onDismissRequest = { showVariants = false } + expanded = showMenu, + onDismissRequest = { showMenu = false } ) { + DropdownMenuItem( + text = { Text(stringResource(R.string.download)) }, + leadingIcon = { Icon(Icons.Default.Download, null) }, + onClick = { component.downloadFile() } + ) + HorizontalDivider() component.files.forEach { DropdownMenuItem( text = { Text(it.name.replaceFirstChar { it.uppercase() }) }, + leadingIcon = { + val icon = when (it.type) { + FileType.JPG, FileType.PNG -> Icons.Default.Image + FileType.GIF -> Icons.Default.Animation + FileType.WEBM -> Icons.Default.Movie + FileType.SWF, FileType.UNDEFINED -> return@DropdownMenuItem + } + Icon(icon, null) + }, onClick = { component.setFile(it) } diff --git a/app/src/main/java/ru/herobrine1st/e621/ui/screen/post/Post.kt b/app/src/main/java/ru/herobrine1st/e621/ui/screen/post/Post.kt index 404523b9..7d36bc3f 100644 --- a/app/src/main/java/ru/herobrine1st/e621/ui/screen/post/Post.kt +++ b/app/src/main/java/ru/herobrine1st/e621/ui/screen/post/Post.kt @@ -49,12 +49,14 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.NavigateNext +import androidx.compose.material.icons.filled.Download import androidx.compose.material.icons.filled.ExpandMore import androidx.compose.material.icons.filled.Explicit import androidx.compose.material.icons.outlined.Error import androidx.compose.material3.BottomSheetScaffold import androidx.compose.material3.Button import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon @@ -171,7 +173,14 @@ fun Post( actions = { ActionBarMenu( onNavigateToSettings = screenSharedState.goToSettings, - onOpenBlacklistDialog = screenSharedState.openBlacklistDialog + onOpenBlacklistDialog = screenSharedState.openBlacklistDialog, + additionalMenuActions = { + DropdownMenuItem( + text = { Text(stringResource(R.string.download)) }, + leadingIcon = { Icon(Icons.Default.Download, null) }, + onClick = { component.downloadFile() } + ) + } ) }, scrollBehavior = scrollBehavior diff --git a/app/src/main/res/values-ru-rRU/strings.xml b/app/src/main/res/values-ru-rRU/strings.xml index b89233d5..78b890ce 100644 --- a/app/src/main/res/values-ru-rRU/strings.xml +++ b/app/src/main/res/values-ru-rRU/strings.xml @@ -162,4 +162,5 @@ Последнее обновление: %s Эта коллекция активна Эта коллекция не активна + Загрузить \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f62a3bfa..49d7f8bc 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -161,4 +161,5 @@ You should have received a copy of the GNU General Public License along with thi Last updated: %s This pool is active This pool is not active + Download \ No newline at end of file