mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
Refactor and unify firmware update logic across platforms (#4966)
This commit is contained in:
parent
d8e295cafb
commit
89547afe6b
102 changed files with 7206 additions and 3485 deletions
|
|
@ -14,18 +14,30 @@
|
|||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
@file:Suppress("TooManyFunctions")
|
||||
|
||||
package org.meshtastic.core.ui.util
|
||||
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.provider.Settings
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.core.net.toUri
|
||||
import co.touchlab.kermit.Logger
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.jetbrains.compose.resources.StringResource
|
||||
import org.jetbrains.compose.resources.getString
|
||||
import org.meshtastic.core.common.util.CommonUri
|
||||
import org.meshtastic.core.common.util.MeshtasticUri
|
||||
import java.net.URLEncoder
|
||||
|
||||
@Composable
|
||||
|
|
@ -116,6 +128,61 @@ actual fun rememberSaveFileLauncher(
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
actual fun rememberOpenFileLauncher(onUriReceived: (CommonUri?) -> Unit): (mimeType: String) -> Unit {
|
||||
val launcher =
|
||||
rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri ->
|
||||
onUriReceived(uri?.let { CommonUri(it) })
|
||||
}
|
||||
return remember(launcher) { { mimeType -> launcher.launch(mimeType) } }
|
||||
}
|
||||
|
||||
@Suppress("Wrapping")
|
||||
@Composable
|
||||
actual fun rememberReadTextFromUri(): suspend (CommonUri, Int) -> String? {
|
||||
val context = LocalContext.current
|
||||
return remember(context) {
|
||||
{ uri, maxChars ->
|
||||
withContext(Dispatchers.IO) {
|
||||
@Suppress("TooGenericExceptionCaught")
|
||||
try {
|
||||
val androidUri = Uri.parse(uri.toString())
|
||||
context.contentResolver.openInputStream(androidUri)?.use { stream ->
|
||||
stream.bufferedReader().use { reader ->
|
||||
val buffer = CharArray(maxChars)
|
||||
val read = reader.read(buffer)
|
||||
if (read > 0) String(buffer, 0, read) else null
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Logger.e(e) { "Failed to read text from URI: $uri" }
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
actual fun KeepScreenOn(enabled: Boolean) {
|
||||
val view = LocalView.current
|
||||
DisposableEffect(enabled) {
|
||||
if (enabled) {
|
||||
view.keepScreenOn = true
|
||||
}
|
||||
onDispose {
|
||||
if (enabled) {
|
||||
view.keepScreenOn = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
actual fun PlatformBackHandler(enabled: Boolean, onBack: () -> Unit) {
|
||||
BackHandler(enabled = enabled, onBack = onBack)
|
||||
}
|
||||
|
||||
@Composable
|
||||
actual fun rememberRequestLocationPermission(onGranted: () -> Unit, onDenied: () -> Unit): () -> Unit {
|
||||
val launcher =
|
||||
|
|
|
|||
|
|
@ -14,10 +14,14 @@
|
|||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
@file:Suppress("TooManyFunctions")
|
||||
|
||||
package org.meshtastic.core.ui.util
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import org.jetbrains.compose.resources.StringResource
|
||||
import org.meshtastic.core.common.util.CommonUri
|
||||
import org.meshtastic.core.common.util.MeshtasticUri
|
||||
|
||||
/** Returns a function to open the platform's NFC settings. */
|
||||
@Composable expect fun rememberOpenNfcSettings(): () -> Unit
|
||||
|
|
@ -37,9 +41,24 @@ import org.jetbrains.compose.resources.StringResource
|
|||
/** Returns a launcher function to prompt the user to save a file. The callback receives the saved file URI. */
|
||||
@Composable
|
||||
expect fun rememberSaveFileLauncher(
|
||||
onUriReceived: (org.meshtastic.core.common.util.MeshtasticUri) -> Unit,
|
||||
onUriReceived: (MeshtasticUri) -> Unit,
|
||||
): (defaultFilename: String, mimeType: String) -> Unit
|
||||
|
||||
/** Returns a launcher function to prompt the user to open/pick a file. The callback receives the selected file URI. */
|
||||
@Composable expect fun rememberOpenFileLauncher(onUriReceived: (CommonUri?) -> Unit): (mimeType: String) -> Unit
|
||||
|
||||
/**
|
||||
* Returns a suspend function that reads up to [maxChars] characters of text from a [CommonUri]. Returns `null` if the
|
||||
* file is empty or cannot be read.
|
||||
*/
|
||||
@Composable expect fun rememberReadTextFromUri(): suspend (uri: CommonUri, maxChars: Int) -> String?
|
||||
|
||||
/** Keeps the screen awake while [enabled] is true. No-op on platforms that don't support it. */
|
||||
@Composable expect fun KeepScreenOn(enabled: Boolean)
|
||||
|
||||
/** Intercepts the platform back gesture/button while [enabled] is true. No-op on platforms without a system back. */
|
||||
@Composable expect fun PlatformBackHandler(enabled: Boolean, onBack: () -> Unit)
|
||||
|
||||
/** Returns a launcher to request location permissions. */
|
||||
@Composable expect fun rememberRequestLocationPermission(onGranted: () -> Unit, onDenied: () -> Unit = {}): () -> Unit
|
||||
|
||||
|
|
|
|||
|
|
@ -21,6 +21,8 @@ import androidx.compose.ui.platform.ClipEntry
|
|||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.TextLinkStyles
|
||||
import org.jetbrains.compose.resources.StringResource
|
||||
import org.meshtastic.core.common.util.CommonUri
|
||||
import org.meshtastic.core.common.util.MeshtasticUri
|
||||
|
||||
actual fun createClipEntry(text: String, label: String): ClipEntry =
|
||||
throw UnsupportedOperationException("ClipEntry instantiation not supported on iOS stub")
|
||||
|
|
@ -39,9 +41,18 @@ actual fun annotatedStringFromHtml(html: String, linkStyles: TextLinkStyles?): A
|
|||
|
||||
@Composable
|
||||
actual fun rememberSaveFileLauncher(
|
||||
onUriReceived: (org.meshtastic.core.common.util.MeshtasticUri) -> Unit,
|
||||
onUriReceived: (MeshtasticUri) -> Unit,
|
||||
): (defaultFilename: String, mimeType: String) -> Unit = { _, _ -> }
|
||||
|
||||
@Composable
|
||||
actual fun rememberOpenFileLauncher(onUriReceived: (CommonUri?) -> Unit): (mimeType: String) -> Unit = { _ -> }
|
||||
|
||||
@Composable actual fun rememberReadTextFromUri(): suspend (CommonUri, Int) -> String? = { _, _ -> null }
|
||||
|
||||
@Composable actual fun KeepScreenOn(enabled: Boolean) {}
|
||||
|
||||
@Composable actual fun PlatformBackHandler(enabled: Boolean, onBack: () -> Unit) {}
|
||||
|
||||
@Composable actual fun rememberRequestLocationPermission(onGranted: () -> Unit, onDenied: () -> Unit): () -> Unit = {}
|
||||
|
||||
@Composable actual fun rememberOpenLocationSettings(): () -> Unit = {}
|
||||
|
|
|
|||
|
|
@ -14,11 +14,22 @@
|
|||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
@file:Suppress("TooManyFunctions")
|
||||
|
||||
package org.meshtastic.core.ui.util
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import co.touchlab.kermit.Logger
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.jetbrains.compose.resources.StringResource
|
||||
import org.meshtastic.core.common.util.CommonUri
|
||||
import org.meshtastic.core.common.util.MeshtasticUri
|
||||
import java.awt.Desktop
|
||||
import java.awt.FileDialog
|
||||
import java.awt.Frame
|
||||
import java.io.File
|
||||
import java.net.URI
|
||||
|
||||
/** JVM stub — NFC settings are not available on Desktop. */
|
||||
@Composable
|
||||
|
|
@ -47,12 +58,68 @@ actual fun rememberOpenUrl(): (url: String) -> Unit = { url ->
|
|||
}
|
||||
}
|
||||
|
||||
/** JVM stub — Save file launcher is a no-op on desktop until implemented. */
|
||||
/** JVM — Opens a native file dialog to save a file. */
|
||||
@Composable
|
||||
actual fun rememberSaveFileLauncher(
|
||||
onUriReceived: (org.meshtastic.core.common.util.MeshtasticUri) -> Unit,
|
||||
): (defaultFilename: String, mimeType: String) -> Unit = { _, _ ->
|
||||
Logger.w { "File saving not implemented on Desktop" }
|
||||
onUriReceived: (MeshtasticUri) -> Unit,
|
||||
): (defaultFilename: String, mimeType: String) -> Unit = { defaultFilename, _ ->
|
||||
val dialog = FileDialog(null as Frame?, "Save File", FileDialog.SAVE)
|
||||
dialog.file = defaultFilename
|
||||
dialog.isVisible = true
|
||||
val file = dialog.file
|
||||
val dir = dialog.directory
|
||||
if (file != null && dir != null) {
|
||||
val path = File(dir, file)
|
||||
onUriReceived(MeshtasticUri(path.toURI().toString()))
|
||||
}
|
||||
}
|
||||
|
||||
/** JVM — Opens a native file dialog to pick a file. */
|
||||
@Composable
|
||||
actual fun rememberOpenFileLauncher(onUriReceived: (CommonUri?) -> Unit): (mimeType: String) -> Unit = { _ ->
|
||||
val dialog = FileDialog(null as? Frame, "Open File", FileDialog.LOAD)
|
||||
dialog.isVisible = true
|
||||
val file = dialog.file
|
||||
val dir = dialog.directory
|
||||
if (file != null && dir != null) {
|
||||
val path = File(dir, file)
|
||||
onUriReceived(CommonUri(path.toURI()))
|
||||
}
|
||||
}
|
||||
|
||||
/** JVM — Reads text from a file URI. */
|
||||
@Composable
|
||||
actual fun rememberReadTextFromUri(): suspend (CommonUri, Int) -> String? = { uri, maxChars ->
|
||||
withContext(Dispatchers.IO) {
|
||||
@Suppress("TooGenericExceptionCaught")
|
||||
try {
|
||||
val file = File(URI(uri.toString()))
|
||||
if (file.exists()) {
|
||||
file.bufferedReader().use { reader ->
|
||||
val buffer = CharArray(maxChars)
|
||||
val read = reader.read(buffer)
|
||||
if (read > 0) String(buffer, 0, read) else null
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Logger.e(e) { "Failed to read text from URI: $uri" }
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** JVM no-op — Keep screen on is not applicable on Desktop. */
|
||||
@Composable
|
||||
actual fun KeepScreenOn(enabled: Boolean) {
|
||||
// No-op on JVM/Desktop
|
||||
}
|
||||
|
||||
/** JVM no-op — Desktop has no system back gesture. */
|
||||
@Composable
|
||||
actual fun PlatformBackHandler(enabled: Boolean, onBack: () -> Unit) {
|
||||
// No-op on JVM/Desktop — no system back button
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue