mirror of
https://github.com/sudosylabs/vnidrop.git
synced 2026-08-05 02:29:55 +02:00
refactor(ui): establish modular Compose foundation
This commit is contained in:
@@ -2,4 +2,4 @@ package com.vnidrop.app
|
||||
|
||||
import androidx.compose.ui.window.ComposeUIViewController
|
||||
|
||||
fun MainViewController() = ComposeUIViewController { App() }
|
||||
fun MainViewController() = ComposeUIViewController { App(rememberIosAppDependencies()) }
|
||||
|
||||
@@ -1,28 +1,48 @@
|
||||
package com.vnidrop.app
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import com.vnidrop.app.core.rememberFileSystemService
|
||||
import com.vnidrop.app.notifications.IosLocalNotificationService
|
||||
import platform.Foundation.NSBundle
|
||||
import platform.Foundation.NSTemporaryDirectory
|
||||
import platform.UIKit.UIDevice
|
||||
|
||||
class IOSPlatform : Platform {
|
||||
private val device: UIDevice = UIDevice.currentDevice
|
||||
@Composable
|
||||
fun rememberIosAppDependencies(): AppDependencies {
|
||||
val fileSystemService = rememberFileSystemService()
|
||||
return remember(fileSystemService) {
|
||||
val device = UIDevice.currentDevice
|
||||
AppDependencies(
|
||||
environment = PlatformEnvironment(
|
||||
name = device.systemName() + " " + device.systemVersion,
|
||||
appVersion = NSBundle.mainBundle.objectForInfoDictionaryKey("CFBundleShortVersionString") as? String ?: "0.1.0",
|
||||
defaultCoreDataDir = NSTemporaryDirectory() + "vnidrop",
|
||||
defaultUsername = device.name.takeIf(String::isNotBlank) ?: "Receiver",
|
||||
),
|
||||
deviceInfoProvider = IosDeviceInfoProvider(device),
|
||||
fileSystemService = fileSystemService,
|
||||
localNotificationService = IosLocalNotificationService(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override val name: String = device.systemName() + " " + device.systemVersion
|
||||
override val defaultCoreDataDir: String = NSTemporaryDirectory() + "vnidrop"
|
||||
override val defaultReceiveDir: String = NSTemporaryDirectory() + "vnidrop-receive"
|
||||
override val deviceInfo: DeviceInfo = DeviceInfo(
|
||||
private class IosDeviceInfoProvider(
|
||||
private val device: UIDevice,
|
||||
) : DeviceInfoProvider {
|
||||
override suspend fun load(): DeviceInfo = DeviceInfo(
|
||||
deviceName = device.name,
|
||||
deviceModel = device.model,
|
||||
operatingSystem = device.systemName() + " " + device.systemVersion,
|
||||
network = null,
|
||||
batteryLevel = batteryLevel(device),
|
||||
batteryLevel = runCatching {
|
||||
val wasMonitoring = device.batteryMonitoringEnabled
|
||||
try {
|
||||
device.batteryMonitoringEnabled = true
|
||||
device.batteryLevel.takeIf { it >= 0.0 }?.let { "${(it * 100).toInt()}%" }
|
||||
} finally {
|
||||
device.batteryMonitoringEnabled = wasMonitoring
|
||||
}
|
||||
}.getOrNull(),
|
||||
)
|
||||
}
|
||||
|
||||
actual fun getPlatform(): Platform = IOSPlatform()
|
||||
|
||||
private fun batteryLevel(device: UIDevice): String? =
|
||||
runCatching {
|
||||
device.batteryMonitoringEnabled = true
|
||||
val level = device.batteryLevel
|
||||
if (level >= 0.0) "${(level * 100).toInt()}%" else null
|
||||
}.getOrNull()
|
||||
|
||||
@@ -73,15 +73,6 @@ actual fun rememberReceiveFolderPicker(
|
||||
}
|
||||
}
|
||||
|
||||
actual suspend fun sharePickedFile(
|
||||
repository: CoreRepository,
|
||||
file: PickedShareFile,
|
||||
transferName: String,
|
||||
senderName: String,
|
||||
) {
|
||||
repository.shareSecurityScopedFileUrl(file.value, file.displayName, transferName, senderName)
|
||||
}
|
||||
|
||||
private class DocumentPickerDelegate(
|
||||
private val onFilePicked: (PickedShareFile) -> Unit,
|
||||
private val onError: (String) -> Unit,
|
||||
|
||||
@@ -42,6 +42,18 @@ private class IosFileSystemService : FileSystemService {
|
||||
|
||||
override fun createReceiveOutputSink(folder: ReceiveFolder): ReceiveOutputSink? = null
|
||||
|
||||
override suspend fun sharePickedFile(
|
||||
repository: CoreGateway,
|
||||
file: PickedShareFile,
|
||||
transferName: String,
|
||||
senderName: String,
|
||||
): Result<Share> = repository.shareSecurityScopedFileUrl(
|
||||
file.value,
|
||||
file.displayName,
|
||||
transferName,
|
||||
senderName,
|
||||
)
|
||||
|
||||
private fun validateSecurityScopedUrl(value: String): FolderAccessStatus {
|
||||
val url = NSURL.URLWithString(value) ?: NSURL.fileURLWithPath(value)
|
||||
val didStartAccess = url.startAccessingSecurityScopedResource()
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
package com.vnidrop.app.notifications
|
||||
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import platform.Foundation.NSURL
|
||||
import platform.UIKit.UIApplication
|
||||
import platform.UIKit.UIApplicationOpenNotificationSettingsURLString
|
||||
import platform.UserNotifications.UNAuthorizationOptionAlert
|
||||
import platform.UserNotifications.UNAuthorizationOptionSound
|
||||
import platform.UserNotifications.UNAuthorizationStatusAuthorized
|
||||
import platform.UserNotifications.UNAuthorizationStatusDenied
|
||||
import platform.UserNotifications.UNAuthorizationStatusEphemeral
|
||||
import platform.UserNotifications.UNAuthorizationStatusNotDetermined
|
||||
import platform.UserNotifications.UNAuthorizationStatusProvisional
|
||||
import platform.UserNotifications.UNMutableNotificationContent
|
||||
import platform.UserNotifications.UNNotificationRequest
|
||||
import platform.UserNotifications.UNUserNotificationCenter
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
class IosLocalNotificationService : LocalNotificationService {
|
||||
private val center = UNUserNotificationCenter.currentNotificationCenter()
|
||||
private val _permission = MutableStateFlow(NotificationPermission.NotDetermined)
|
||||
override val permission: StateFlow<NotificationPermission> = _permission.asStateFlow()
|
||||
|
||||
override suspend fun refreshPermission(): NotificationPermission = suspendCancellableCoroutine { continuation ->
|
||||
center.getNotificationSettingsWithCompletionHandler { settings ->
|
||||
val mapped = when (settings?.authorizationStatus) {
|
||||
UNAuthorizationStatusAuthorized,
|
||||
UNAuthorizationStatusProvisional,
|
||||
UNAuthorizationStatusEphemeral -> NotificationPermission.Granted
|
||||
UNAuthorizationStatusDenied -> NotificationPermission.Denied
|
||||
UNAuthorizationStatusNotDetermined -> NotificationPermission.NotDetermined
|
||||
else -> NotificationPermission.Unsupported
|
||||
}
|
||||
_permission.value = mapped
|
||||
if (continuation.isActive) continuation.resume(mapped)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun requestPermission(): NotificationPermission {
|
||||
val current = refreshPermission()
|
||||
if (current != NotificationPermission.NotDetermined) return current
|
||||
return suspendCancellableCoroutine { continuation ->
|
||||
center.requestAuthorizationWithOptions(
|
||||
options = UNAuthorizationOptionAlert or UNAuthorizationOptionSound,
|
||||
completionHandler = { granted, _ ->
|
||||
val result = if (granted) NotificationPermission.Granted else NotificationPermission.Denied
|
||||
_permission.value = result
|
||||
if (continuation.isActive) continuation.resume(result)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun openSettings(): Result<Unit> {
|
||||
val url = NSURL.URLWithString(UIApplicationOpenNotificationSettingsURLString)
|
||||
?: return Result.failure(IllegalStateException("Notification settings URL is unavailable"))
|
||||
val opened = suspendCancellableCoroutine { continuation ->
|
||||
UIApplication.sharedApplication.openURL(url, emptyMap<Any?, Any>()) { success ->
|
||||
if (continuation.isActive) continuation.resume(success)
|
||||
}
|
||||
}
|
||||
return if (opened) Result.success(Unit) else Result.failure(IllegalStateException("Could not open notification settings"))
|
||||
}
|
||||
|
||||
override suspend fun publish(notification: LocalNotification): Result<Unit> = runCatching {
|
||||
check(refreshPermission() == NotificationPermission.Granted) { "Notification permission is not granted" }
|
||||
val content = UNMutableNotificationContent().apply {
|
||||
setTitle(notification.title)
|
||||
setBody(notification.body)
|
||||
setSound(platform.UserNotifications.UNNotificationSound.defaultSound)
|
||||
}
|
||||
val request = UNNotificationRequest.requestWithIdentifier(notification.id, content, null)
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
center.addNotificationRequest(request) { error ->
|
||||
if (!continuation.isActive) return@addNotificationRequest
|
||||
if (error == null) {
|
||||
continuation.resume(Unit)
|
||||
} else {
|
||||
continuation.resumeWith(
|
||||
Result.failure(IllegalStateException(error.localizedDescription)),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun cancel(id: String) {
|
||||
center.removePendingNotificationRequestsWithIdentifiers(listOf(id))
|
||||
center.removeDeliveredNotificationsWithIdentifiers(listOf(id))
|
||||
}
|
||||
|
||||
override suspend fun cancelAll() {
|
||||
center.removeAllPendingNotificationRequests()
|
||||
center.removeAllDeliveredNotifications()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user