diff --git a/app/build.gradle.kts b/app/build.gradle.kts index a9442a5..41c059e 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -5,6 +5,7 @@ plugins { alias(libs.plugins.kotlin.android) alias(libs.plugins.kotlin.compose) id("com.google.devtools.ksp") + id("kotlin-parcelize") } android { diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index eacb4b2..9d5874d 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -4,6 +4,9 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/paulcoding/hviewer/helper/File.kt b/app/src/main/java/com/paulcoding/hviewer/helper/File.kt index 2f5bffe..75bb367 100644 --- a/app/src/main/java/com/paulcoding/hviewer/helper/File.kt +++ b/app/src/main/java/com/paulcoding/hviewer/helper/File.kt @@ -1,6 +1,7 @@ package com.paulcoding.hviewer.helper import android.content.Context +import android.os.Environment import com.google.gson.Gson import com.google.gson.reflect.TypeToken import java.io.File @@ -19,9 +20,21 @@ val Context.crashLogDir val Context.configFile get() = File(scriptsDir, CONFIG_FILE) +val downloadDir: File = File( + Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), "HViewer" +) + fun Context.setupPaths() { scriptsDir.mkdir() crashLogDir.mkdir() + + if (!downloadDir.exists()) { + downloadDir.mkdirs() + val nomediaFile = File(downloadDir, ".nomedia") + if (!nomediaFile.exists()) { + nomediaFile.createNewFile() + } + } } fun Context.writeFile(data: String, fileName: String, fileDir: File = scriptsDir): File { diff --git a/app/src/main/java/com/paulcoding/hviewer/helper/ImageDownloader.kt b/app/src/main/java/com/paulcoding/hviewer/helper/ImageDownloader.kt new file mode 100644 index 0000000..748aae5 --- /dev/null +++ b/app/src/main/java/com/paulcoding/hviewer/helper/ImageDownloader.kt @@ -0,0 +1,35 @@ +package com.paulcoding.hviewer.helper + +import kotlinx.coroutines.* +import okhttp3.OkHttpClient +import okhttp3.Request +import java.io.File +import java.io.FileOutputStream +import java.io.InputStream + +object ImageDownloader { + private val client = OkHttpClient() + + suspend fun downloadImage(url: String, outputFile: File): Boolean { + return withContext(Dispatchers.IO) { + try { + val request = Request.Builder().url(url).build() + val response = client.newCall(request).execute() + + if (!response.isSuccessful) return@withContext false + + val inputStream: InputStream? = response.body?.byteStream() + val outputStream = FileOutputStream(outputFile) + + inputStream?.copyTo(outputStream) + outputStream.close() + inputStream?.close() + + return@withContext true + } catch (e: Exception) { + e.printStackTrace() + return@withContext false + } + } + } +} diff --git a/app/src/main/java/com/paulcoding/hviewer/model/SiteModel.kt b/app/src/main/java/com/paulcoding/hviewer/model/SiteModel.kt index 7fc7920..f610a35 100644 --- a/app/src/main/java/com/paulcoding/hviewer/model/SiteModel.kt +++ b/app/src/main/java/com/paulcoding/hviewer/model/SiteModel.kt @@ -1,5 +1,6 @@ package com.paulcoding.hviewer.model +import android.os.Parcelable import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable @@ -12,11 +13,12 @@ import androidx.compose.ui.unit.dp import coil3.compose.AsyncImage import com.paulcoding.hviewer.R +@kotlinx.parcelize.Parcelize data class SiteConfig( val baseUrl: String = "", val scriptFile: String = "", val tags: Map = mapOf(), -) { +) : Parcelable { private val icon get() = "https://www.google.com/s2/favicons?sz=64&domain=$baseUrl" diff --git a/app/src/main/java/com/paulcoding/hviewer/ui/page/AppEntry.kt b/app/src/main/java/com/paulcoding/hviewer/ui/page/AppEntry.kt index 571cb4e..0d32dfc 100644 --- a/app/src/main/java/com/paulcoding/hviewer/ui/page/AppEntry.kt +++ b/app/src/main/java/com/paulcoding/hviewer/ui/page/AppEntry.kt @@ -20,9 +20,12 @@ import androidx.navigation.NamedNavArgument import androidx.navigation.NavBackStackEntry import androidx.navigation.NavDeepLink import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavType import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController +import androidx.navigation.navArgument +import androidx.navigation.navDeepLink import com.paulcoding.hviewer.R import com.paulcoding.hviewer.helper.makeToast import com.paulcoding.hviewer.model.PostItem @@ -31,6 +34,7 @@ import com.paulcoding.hviewer.model.Tag import com.paulcoding.hviewer.network.Github import com.paulcoding.hviewer.preference.Preferences import com.paulcoding.hviewer.ui.favorite.FavoritePage +import com.paulcoding.hviewer.ui.page.downloads.DownloadsPage import com.paulcoding.hviewer.ui.page.editor.EditorPage import com.paulcoding.hviewer.ui.page.editor.ListScriptPage import com.paulcoding.hviewer.ui.page.history.HistoryPage @@ -82,6 +86,7 @@ fun AppEntry(intent: Intent?) { } LaunchedEffect(updatedIntent) { + updatedIntent?.apply { when (action) { Intent.ACTION_SEND -> { @@ -93,7 +98,13 @@ fun AppEntry(intent: Intent?) { } Intent.ACTION_VIEW -> { - handleIntentUrl(data.toString()) + // TODO: why deeplink not working + if (data.toString().startsWith("hviewer://")) { + val route = data.toString().substringAfter("hviewer://") + navController.navigate(route) + } else { + handleIntentUrl(data.toString()) + } } else -> { @@ -120,6 +131,9 @@ fun AppEntry(intent: Intent?) { navToHistory = { navController.navigate(Route.HISTORY) }, + navToDownloads = { + navController.navigate("downloads/") + }, goBack = { navController.popBackStack() }) } animatedComposable(Route.SETTINGS) { @@ -250,6 +264,17 @@ fun AppEntry(intent: Intent?) { appViewModel = appViewModel, siteConfigs = siteConfigs ) } + animatedComposable( + route = "downloads/{path}", + arguments = listOf(navArgument("path") { type = NavType.StringType }), + deepLinks = listOf(navDeepLink { uriPattern = "hviewer://downloads/{path}" }) + ) { backStackEntry -> + val path = backStackEntry.arguments?.getString("path") + DownloadsPage( + goBack = navController::popBackStack, + initialDir = path + ) + } } } diff --git a/app/src/main/java/com/paulcoding/hviewer/ui/page/downloads/DownloadsPage.kt b/app/src/main/java/com/paulcoding/hviewer/ui/page/downloads/DownloadsPage.kt new file mode 100644 index 0000000..ef5e1a7 --- /dev/null +++ b/app/src/main/java/com/paulcoding/hviewer/ui/page/downloads/DownloadsPage.kt @@ -0,0 +1,132 @@ +package com.paulcoding.hviewer.ui.page.downloads + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Close +import androidx.compose.material.icons.outlined.Folder +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.paulcoding.hviewer.R +import com.paulcoding.hviewer.helper.downloadDir +import com.paulcoding.hviewer.ui.component.HBackIcon +import com.paulcoding.hviewer.ui.component.HEmpty +import com.paulcoding.hviewer.ui.component.HIcon +import com.paulcoding.hviewer.ui.page.post.ImageModal +import com.paulcoding.hviewer.ui.page.post.PostImage +import java.io.File + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun DownloadsPage( + goBack: () -> Unit, + initialDir: String? = null, +) { + var dirs by remember { mutableStateOf(emptyList()) } + var selectedDir by remember { mutableStateOf(null) } + + LaunchedEffect(Unit) { + downloadDir.listFiles()?.filter { it.isDirectory }?.toList()?.let { + dirs = it + } + initialDir?.let { + if (File(it).exists()) selectedDir = File(it) + } + } + Scaffold(topBar = { + TopAppBar(title = { Text(stringResource(R.string.downloads)) }, navigationIcon = { + HBackIcon { goBack() } + }, actions = { + if (selectedDir != null) HIcon( + Icons.Outlined.Close, + ) { selectedDir = null } + }) + }) { paddings -> + Column(modifier = Modifier.padding(paddings)) { + if (selectedDir == null) LazyColumn( + modifier = Modifier.padding(horizontal = 12.dp), + contentPadding = PaddingValues(vertical = 12.dp), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + items(dirs) { + Row( + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.clickable { + selectedDir = it + }, + ) { + Icon(Icons.Outlined.Folder, it.name) + Text( + it.name, modifier = Modifier.padding(12.dp) + ) + } + } + if (dirs.isEmpty()) item { + HEmpty() + } + } + else { + ImageList(selectedDir!!) + } + } + } +} + +@Composable +internal fun ImageList(selectedDir: File) { + var selectedImage by remember { mutableStateOf(null) } + var isSystemBarHidden by remember { mutableStateOf(false) } + val listState = rememberLazyListState() + + val images by remember { + derivedStateOf { + selectedDir.listFiles()?.map { it.absolutePath } ?: emptyList() + } + } + + LazyColumn( + state = listState, verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + items(images, key = { it }) { image -> + PostImage( + url = image, + onDoubleTap = { + selectedImage = image + }, + onTap = { + isSystemBarHidden = !isSystemBarHidden + }, + ) + } + if (images.isEmpty()) item { + HEmpty() + } + } + if (selectedImage != null) { + ImageModal(url = selectedImage!!) { + selectedImage = null + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/paulcoding/hviewer/ui/page/post/DownloadService.kt b/app/src/main/java/com/paulcoding/hviewer/ui/page/post/DownloadService.kt new file mode 100644 index 0000000..ff0e305 --- /dev/null +++ b/app/src/main/java/com/paulcoding/hviewer/ui/page/post/DownloadService.kt @@ -0,0 +1,216 @@ +package com.paulcoding.hviewer.ui.page.post + +import android.annotation.SuppressLint +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.app.Service +import android.app.TaskStackBuilder +import android.content.Intent +import android.net.Uri +import android.os.Build +import android.os.IBinder +import androidx.core.app.NotificationCompat +import androidx.core.net.toUri +import com.paulcoding.hviewer.MainActivity +import com.paulcoding.hviewer.helper.ImageDownloader +import com.paulcoding.hviewer.helper.SCRIPTS_DIR +import com.paulcoding.hviewer.helper.downloadDir +import com.paulcoding.hviewer.model.PostData +import com.paulcoding.hviewer.model.SiteConfig +import com.paulcoding.js.JS +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import java.io.File + +class DownloadService : Service() { + private lateinit var notificationManager: NotificationManager + private lateinit var notificationBuilder: NotificationCompat.Builder + + private val channelId = "DownloadChannel" + private val notificationId = 1 + + private var postPage: Int = 1 + private var postTotalPage: Int = 1 + private var nextPage: String? = null + private var images: MutableList = mutableListOf() + + private fun reset() { + postPage = 1 + postTotalPage = 1 + nextPage = null + images = mutableListOf() + + _downloadStatusFlow.update { DownloadStatus.IDLE } + } + + override fun onCreate() { + super.onCreate() + notificationManager = getSystemService(NotificationManager::class.java) + createNotificationChannel() + } + + override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int { + reset() + + val postUrl = intent.getStringExtra("postUrl") + val postName = intent.getStringExtra("postName") + val siteConfig = intent.getParcelableExtra("siteConfig") + if (postUrl != null && postName != null && siteConfig != null) { + startForeground(notificationId, createNotification("Downloading $postName")) + download(postUrl, postName, siteConfig) + } else { + throw IllegalArgumentException("Missing required parameters") + } + return START_NOT_STICKY + } + + override fun onBind(intent: Intent?): IBinder? { + return null + } + + private suspend fun getImages(js: JS, url: String, page: Int = 1) { + js.callFunction("getImages", arrayOf(url, page)).onSuccess { postData -> + postTotalPage = postData.total + nextPage = postData.next + images = (images + postData.images).toMutableList() + }.onFailure { + throw (it) + } + } + + private fun download(postUrl: String, postName: String, siteConfig: SiteConfig) { + val js = JS( + fileRelativePath = SCRIPTS_DIR + "/${siteConfig.scriptFile}", + properties = mapOf("baseUrl" to siteConfig.baseUrl) + ) + + nextPage = postUrl + + try { + // fetch image urls + _downloadStatusFlow.update { DownloadStatus.DOWNLOADING } + CoroutineScope(Dispatchers.IO).launch { + while (postPage <= postTotalPage) { + delay(1000) // add some delay to avoid getting blocked by the server + nextPage?.let { getImages(js, it, postPage) } + updateNotification("Fetching ($postPage/$postTotalPage) pages") + postPage++ + } + downloadImagesParallel(postName) + } + } catch (e: Exception) { + e.printStackTrace() + stopSelf() // stop the service if an exception occurs + } finally { + _downloadStatusFlow.update { DownloadStatus.IDLE } + } + } + + private suspend fun downloadImagesParallel(postName: String, onFinish: () -> Unit = {}) { + val outputName = postName.replace(Regex("[^a-zA-Z0-9._]"), "_") + coroutineScope { + val outputDir = File(downloadDir, outputName).apply { + if (!exists()) { + mkdirs() + } + } + val paddingLength = images.size.toString().length + val downloadJobs = images.mapIndexed { index, url -> + async { + val file = + File(outputDir, "img_${index.toString().padStart(paddingLength, '0')}.jpg") + + val success = ImageDownloader.downloadImage(url, file) + if (!success) { + println("❌ Failed to download: $url") + } + } + } + var totalProgress = 0 + downloadJobs.chunked(5).forEach { chunkedJobs -> + chunkedJobs.awaitAll() + delay(500) + totalProgress += chunkedJobs.size + updateNotification("${totalProgress}/${images.size} images") + } + println("✅ All images downloaded successfully!") + showDownloadCompleteNotification(outputDir) + onFinish() + } + } + + + private fun createNotification(title: String): Notification { + notificationBuilder = + NotificationCompat.Builder(this, channelId) + .setContentTitle(title) + .setSmallIcon(android.R.drawable.stat_sys_download) + .setPriority(NotificationCompat.PRIORITY_LOW) + .setProgress(0, 0, true) + val notification = notificationBuilder.build() + notificationManager.notify(notificationId, notification) + return notification + } + + private fun updateNotification(msg: String) { + notificationBuilder.setContentText(msg) + notificationManager.notify(notificationId, notificationBuilder.build()) + } + + @SuppressLint("ObsoleteSdkInt") + private fun createNotificationChannel() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val serviceChannel = NotificationChannel( + channelId, "Download Service Channel", NotificationManager.IMPORTANCE_LOW + ) + notificationManager.createNotificationChannel( + serviceChannel + ) + } + } + + private fun showDownloadCompleteNotification(file: File) { + val deepLinkIntent = Intent( + Intent.ACTION_VIEW, + "hviewer://downloads/${Uri.encode(file.absolutePath)}".toUri(), + this, + MainActivity::class.java + ) + + val deepLinkPendingIntent: PendingIntent? = TaskStackBuilder.create(this).run { + addNextIntentWithParentStack(deepLinkIntent) + getPendingIntent(0, PendingIntent.FLAG_MUTABLE) + } + + + val completedNotification = + NotificationCompat.Builder(this, channelId).setContentTitle("Download Complete") + .setContentText("Tap to open") + .setSmallIcon(android.R.drawable.stat_sys_download_done) + .setContentIntent(deepLinkPendingIntent) + .setAutoCancel(true) + .build() + + notificationManager.notify(notificationId, completedNotification) + } + + companion object { + private val _downloadStatusFlow = MutableStateFlow(DownloadStatus.IDLE) + val downloadStatusFlow = _downloadStatusFlow.asStateFlow() + } +} + +enum class DownloadStatus { + IDLE, + DOWNLOADING, +} \ No newline at end of file diff --git a/app/src/main/java/com/paulcoding/hviewer/ui/page/post/Images.kt b/app/src/main/java/com/paulcoding/hviewer/ui/page/post/Images.kt index c0c0b51..46b84bd 100644 --- a/app/src/main/java/com/paulcoding/hviewer/ui/page/post/Images.kt +++ b/app/src/main/java/com/paulcoding/hviewer/ui/page/post/Images.kt @@ -1,5 +1,8 @@ package com.paulcoding.hviewer.ui.page.post +import android.Manifest +import android.content.Intent +import android.os.Build import android.widget.Toast import androidx.compose.animation.core.animateDpAsState import androidx.compose.animation.core.tween @@ -21,6 +24,8 @@ import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.outlined.ArrowBack +import androidx.compose.material.icons.outlined.Download +import androidx.compose.material.icons.outlined.Downloading import androidx.compose.material.icons.outlined.KeyboardArrowUp import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text @@ -35,13 +40,20 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.lifecycle.viewmodel.compose.viewModel +import com.google.accompanist.permissions.ExperimentalPermissionsApi +import com.google.accompanist.permissions.PermissionStatus +import com.google.accompanist.permissions.isGranted +import com.google.accompanist.permissions.rememberPermissionState import com.paulcoding.hviewer.MainApp.Companion.appContext import com.paulcoding.hviewer.helper.BasePaginationHelper import com.paulcoding.hviewer.helper.LoadMoreHandler +import com.paulcoding.hviewer.helper.makeToast +import com.paulcoding.hviewer.model.PostItem import com.paulcoding.hviewer.model.SiteConfig import com.paulcoding.hviewer.ui.component.HIcon import com.paulcoding.hviewer.ui.component.HLoading @@ -49,28 +61,47 @@ import com.paulcoding.hviewer.ui.component.SystemBar import kotlinx.coroutines.launch +@OptIn(ExperimentalPermissionsApi::class) @Composable fun ImageList( - postUrl: String, + post: PostItem, siteConfig: SiteConfig, goBack: () -> Unit, bottomRowActions: @Composable (RowScope.() -> Unit) = {}, ) { val viewModel: PostViewModel = viewModel( - key = postUrl, - factory = PostViewModelFactory(postUrl, siteConfig = siteConfig) + key = post.url, + factory = PostViewModelFactory(post.url, siteConfig = siteConfig) ) + val storagePermission = + rememberPermissionState(Manifest.permission.WRITE_EXTERNAL_STORAGE) { granted -> + if (!granted) + makeToast("Permission Denied!") + } + + val notificationPermission = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + rememberPermissionState(Manifest.permission.POST_NOTIFICATIONS) { granted -> + if (!granted) + makeToast("Notification permission Denied!") + } + } else { + null + } val uiState by viewModel.stateFlow.collectAsState() var selectedImage by remember { mutableStateOf(null) } val listState = rememberLazyListState() val scope = rememberCoroutineScope() + val context = LocalContext.current val translationY by animateDpAsState( targetValue = if (uiState.isSystemBarHidden) (-100).dp else 0.dp, animationSpec = tween(200) ) + val downloadState by DownloadService.downloadStatusFlow.collectAsState() + LaunchedEffect(uiState.error) { uiState.error?.let { Toast.makeText(appContext, it.message ?: it.toString(), Toast.LENGTH_SHORT).show() @@ -90,6 +121,17 @@ fun ImageList( ) } + fun checkPermissionOrDownload(block: () -> Unit) { + if (notificationPermission != null && !notificationPermission.status.isGranted) { + notificationPermission.launchPermissionRequest() + } + if (Build.VERSION.SDK_INT > Build.VERSION_CODES.Q || storagePermission.status == PermissionStatus.Granted) { + block() + } else { + storagePermission.launchPermissionRequest() + } + } + LoadMoreHandler(uiState.images.size, listState, paginationHelper) SystemBar(uiState.isSystemBarHidden) @@ -151,6 +193,22 @@ fun ImageList( horizontalArrangement = Arrangement.SpaceBetween ) { bottomRowActions() + HIcon( + imageVector = if (downloadState == DownloadStatus.IDLE) Icons.Outlined.Download else Icons.Outlined.Downloading, + size = 32, + rounded = true, + enabled = downloadState == DownloadStatus.IDLE, + onClick = { + checkPermissionOrDownload { + val intent = Intent(context, DownloadService::class.java).apply { + putExtra("postUrl", post.url) + putExtra("postName", post.name) + putExtra("siteConfig", siteConfig) + } + context.startForegroundService(intent) + } + } + ) Spacer(modifier = Modifier.weight(1f)) HIcon( Icons.Outlined.KeyboardArrowUp, diff --git a/app/src/main/java/com/paulcoding/hviewer/ui/page/post/PostPage.kt b/app/src/main/java/com/paulcoding/hviewer/ui/page/post/PostPage.kt index 7538d0a..3fd0088 100644 --- a/app/src/main/java/com/paulcoding/hviewer/ui/page/post/PostPage.kt +++ b/app/src/main/java/com/paulcoding/hviewer/ui/page/post/PostPage.kt @@ -18,7 +18,7 @@ fun PostPage( post.getSiteConfig(hostMap)?.let { ImageList( - postUrl = post.url, siteConfig = it, + post = post, siteConfig = it, goBack = goBack ) } diff --git a/app/src/main/java/com/paulcoding/hviewer/ui/page/sites/SitesPage.kt b/app/src/main/java/com/paulcoding/hviewer/ui/page/sites/SitesPage.kt index 0d90fd4..aca94e7 100644 --- a/app/src/main/java/com/paulcoding/hviewer/ui/page/sites/SitesPage.kt +++ b/app/src/main/java/com/paulcoding/hviewer/ui/page/sites/SitesPage.kt @@ -11,6 +11,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Download import androidx.compose.material.icons.outlined.History import androidx.compose.material.icons.outlined.Settings import androidx.compose.material3.ExperimentalMaterial3Api @@ -47,6 +48,7 @@ fun SitesPage( siteConfigs: SiteConfigs, navToSettings: () -> Unit, navToHistory: () -> Unit, + navToDownloads: () -> Unit, refresh: () -> Unit, navToFavorite: () -> Unit, ) { @@ -60,6 +62,9 @@ fun SitesPage( Scaffold(topBar = { TopAppBar(title = { Text(stringResource(R.string.sites)) }, actions = { + HIcon(Icons.Outlined.Download) { + navToDownloads() + } HIcon(Icons.Outlined.History) { navToHistory() } diff --git a/app/src/main/java/com/paulcoding/hviewer/ui/page/tabs/TabsPage.kt b/app/src/main/java/com/paulcoding/hviewer/ui/page/tabs/TabsPage.kt index ad9ecf4..55c6dc2 100644 --- a/app/src/main/java/com/paulcoding/hviewer/ui/page/tabs/TabsPage.kt +++ b/app/src/main/java/com/paulcoding/hviewer/ui/page/tabs/TabsPage.kt @@ -81,7 +81,7 @@ fun TabsPage( if (siteConfig != null) { ImageList( - tab.url, + tab, siteConfig = siteConfig, goBack = goBack, bottomRowActions = { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d30b9ab..4206666 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -31,4 +31,5 @@ Edit local scripts Invalid URL %1$s Invalid Repo + Downloads \ No newline at end of file diff --git a/app/src/main/res/xml/provider_paths.xml b/app/src/main/res/xml/provider_paths.xml new file mode 100644 index 0000000..4cb486c --- /dev/null +++ b/app/src/main/res/xml/provider_paths.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file