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/DownloadState.kt b/app/src/main/java/com/paulcoding/hviewer/helper/DownloadState.kt
new file mode 100644
index 0000000..755c0ca
--- /dev/null
+++ b/app/src/main/java/com/paulcoding/hviewer/helper/DownloadState.kt
@@ -0,0 +1,82 @@
+package com.paulcoding.hviewer.helper
+
+import android.Manifest
+import android.content.Intent
+import android.os.Build
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.platform.LocalContext
+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.R
+import com.paulcoding.hviewer.model.PostItem
+import com.paulcoding.hviewer.ui.LocalHostsMap
+import com.paulcoding.hviewer.ui.page.post.DownloadService
+import com.paulcoding.hviewer.ui.page.post.DownloadStatus
+
+
+@OptIn(ExperimentalPermissionsApi::class)
+@Composable
+fun rememberDownloadState(post: PostItem): DownloadState {
+ val hostsMap = LocalHostsMap.current
+
+ val downloadState by DownloadService.downloadStatusFlow.collectAsState()
+ val context = LocalContext.current
+
+ 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
+ }
+
+ 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()
+ }
+ }
+
+ fun download() {
+ val siteConfig =
+ post.getSiteConfig(hostsMap) ?: throw Exception("Site config not found: ${post.url}")
+
+ checkPermissionOrDownload {
+ makeToast(context.getString(R.string.downloading_post, post.name))
+ val intent = Intent(context, DownloadService::class.java).apply {
+ putExtra("postUrl", post.url)
+ putExtra("postName", post.name)
+ putExtra("siteConfig", siteConfig)
+ }
+ context.startForegroundService(intent)
+ }
+ }
+
+ return DownloadState(
+ status = downloadState,
+ isDownloading = downloadState == DownloadStatus.DOWNLOADING,
+ download = ::download
+ )
+}
+
+class DownloadState(
+ val status: DownloadStatus,
+ val isDownloading: Boolean,
+ val download: () -> Unit
+)
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..3cc3492
--- /dev/null
+++ b/app/src/main/java/com/paulcoding/hviewer/helper/ImageDownloader.kt
@@ -0,0 +1,32 @@
+package com.paulcoding.hviewer.helper
+
+import kotlinx.coroutines.withContext
+import okhttp3.OkHttpClient
+import okhttp3.Request
+import java.io.File
+import java.io.FileOutputStream
+import java.io.InputStream
+import kotlin.coroutines.CoroutineContext
+
+object ImageDownloader {
+ private val client = OkHttpClient()
+
+ suspend fun downloadImage(context: CoroutineContext, url: String, outputFile: File): Boolean {
+ return withContext(context) {
+ println("🔵 Downloading image: $url")
+ 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
+ }
+ }
+}
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/CompositionLocals.kt b/app/src/main/java/com/paulcoding/hviewer/ui/CompositionLocals.kt
new file mode 100644
index 0000000..adabe6b
--- /dev/null
+++ b/app/src/main/java/com/paulcoding/hviewer/ui/CompositionLocals.kt
@@ -0,0 +1,6 @@
+package com.paulcoding.hviewer.ui
+
+import androidx.compose.runtime.staticCompositionLocalOf
+import com.paulcoding.hviewer.model.SiteConfig
+
+val LocalHostsMap = staticCompositionLocalOf