Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ plugins {
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.compose)
id("com.google.devtools.ksp")
id("kotlin-parcelize")
}

android {
Expand Down
26 changes: 26 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="32"
Expand Down Expand Up @@ -37,12 +40,35 @@

<data android:scheme="https" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />

<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />

<data
android:host="downloads"
android:scheme="hviewer" />

</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/plain" />
</intent-filter>
</activity>
<service
android:name=".ui.page.post.DownloadService"
android:foregroundServiceType="dataSync" />
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths" />
</provider>
</application>

</manifest>
82 changes: 82 additions & 0 deletions app/src/main/java/com/paulcoding/hviewer/helper/DownloadState.kt
Original file line number Diff line number Diff line change
@@ -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
)
13 changes: 13 additions & 0 deletions app/src/main/java/com/paulcoding/hviewer/helper/File.kt
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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 {
Expand Down
32 changes: 32 additions & 0 deletions app/src/main/java/com/paulcoding/hviewer/helper/ImageDownloader.kt
Original file line number Diff line number Diff line change
@@ -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
}
}
}
4 changes: 3 additions & 1 deletion app/src/main/java/com/paulcoding/hviewer/model/SiteModel.kt
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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<String, String> = mapOf(),
) {
) : Parcelable {
private val icon
get() = "https://www.google.com/s2/favicons?sz=64&domain=$baseUrl"

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.paulcoding.hviewer.ui

import androidx.compose.runtime.staticCompositionLocalOf
import com.paulcoding.hviewer.model.SiteConfig

val LocalHostsMap = staticCompositionLocalOf<Map<String, SiteConfig>> { mapOf() }
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.paulcoding.hviewer.ui.component

import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import com.paulcoding.hviewer.R


@Composable
fun ConfirmDialog(
showDialog: Boolean,
title: String = "",
text: String = "",
onDismiss: () -> Unit,
onConfirm: () -> Unit
) {
if (showDialog) {
AlertDialog(
onDismissRequest = { onDismiss() },
title = { Text(text = title) },
text = { Text(text = text) },
confirmButton = {
TextButton(onClick = { onConfirm() }) {
Text(stringResource(R.string.confirm))
}
},
dismissButton = {
TextButton(onClick = { onDismiss() }) {
Text(stringResource(R.string.cancel), color = Color.Red)
}
}
)
}
}
Loading