Feat delete application (#166)

* feat: delete application

* delete app

---------

Co-authored-by: Florent Champigny <florent@bere.al>
This commit is contained in:
Florent CHAMPIGNY 2025-08-28 16:27:40 +02:00 committed by GitHub
parent fafe749dbd
commit a462ead246
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 227 additions and 102 deletions

View file

@ -7,6 +7,7 @@ import io.github.openflocon.data.core.device.datasource.remote.RemoteDeviceDataS
import io.github.openflocon.domain.Protocol
import io.github.openflocon.domain.adb.repository.AdbRepository
import io.github.openflocon.domain.common.DispatcherProvider
import io.github.openflocon.domain.device.models.AppPackageName
import io.github.openflocon.domain.device.models.DeviceAppDomainModel
import io.github.openflocon.domain.device.models.DeviceDomainModel
import io.github.openflocon.domain.device.models.DeviceId
@ -193,6 +194,22 @@ class DevicesRepositoryImpl(
}
}
override suspend fun deleteApplication(
deviceId: DeviceId,
packageName: AppPackageName,
) {
withContext(dispatcherProvider.data) {
localDevicesDataSource.deleteApp(
deviceId = deviceId,
packageName = packageName
)
localCurrentDeviceDataSource.deleteApp(
deviceId = deviceId,
packageName = packageName
)
}
}
// endregion
override val pluginName = listOf(Protocol.FromDevice.Device.Plugin)

View file

@ -82,6 +82,7 @@ fun MainScreen(
recordState = recordState,
onDeviceSelected = viewModel::onDeviceSelected,
deleteDevice = viewModel::deleteDevice,
deleteApp = viewModel::deleteApp,
onAppSelected = viewModel::onAppSelected,
leftPanelState = leftPanelState,
onClickLeftPanelItem = viewModel::onClickLeftPanelItem,
@ -101,6 +102,7 @@ private fun MainScreen(
onDeviceSelected: (DeviceItemUiModel) -> Unit,
deleteDevice: (DeviceItemUiModel) -> Unit,
onAppSelected: (DeviceAppUiModel) -> Unit,
deleteApp: (DeviceAppUiModel) -> Unit,
onTakeScreenshotClicked: () -> Unit,
recordState: RecordVideoStateUiModel,
onRecordClicked: () -> Unit,
@ -125,6 +127,7 @@ private fun MainScreen(
onDeviceSelected = onDeviceSelected,
deleteDevice = deleteDevice,
onAppSelected = onAppSelected,
deleteApp = deleteApp,
onTakeScreenshotClicked = onTakeScreenshotClicked,
recordState = recordState,
onRecordClicked = onRecordClicked,

View file

@ -81,6 +81,12 @@ class MainViewModel(
}
}
fun deleteApp(app: DeviceAppUiModel) {
viewModelScope.launch(dispatcherProvider.viewModel) {
devicesDelegate.deleteApp(app.packageName)
}
}
fun onAppSelected(app: DeviceAppUiModel) {
viewModelScope.launch(dispatcherProvider.viewModel) {
devicesDelegate.selectApp(app.packageName)

View file

@ -1,5 +1,7 @@
package io.github.openflocon.flocondesktop.main.ui.delegates
import io.github.openflocon.domain.device.models.DeviceIdAndPackageNameDomainModel
import io.github.openflocon.domain.device.usecase.DeleteDeviceApplicationUseCase
import io.github.openflocon.domain.device.usecase.DeleteDeviceUseCase
import io.github.openflocon.domain.device.usecase.GetCurrentDeviceIdAndPackageNameUseCase
import io.github.openflocon.domain.device.usecase.ObserveActiveDevicesUseCase
@ -30,8 +32,9 @@ class DevicesDelegate(
observeCurrentDeviceAppsUseCase: ObserveCurrentDeviceAppsUseCase,
observeCurrentDeviceIdAndPackageNameUseCase: ObserveCurrentDeviceIdAndPackageNameUseCase,
observeActiveDevicesUseCase: ObserveActiveDevicesUseCase,
getCurrentDeviceIdAndPackageNameUseCase: GetCurrentDeviceIdAndPackageNameUseCase,
private val getCurrentDeviceIdAndPackageNameUseCase: GetCurrentDeviceIdAndPackageNameUseCase,
private val deleteDeviceUseCase: DeleteDeviceUseCase,
private val deleteDeviceApplicationUseCase: DeleteDeviceApplicationUseCase,
private val closeableDelegate: CloseableDelegate,
) : CloseableScoped by closeableDelegate {
@ -128,7 +131,12 @@ class DevicesDelegate(
deleteDeviceUseCase(deviceId)
}
fun deleteApp(packageName: String) {
TODO("Not yet implemented")
suspend fun deleteApp(packageName: String) {
// only fur the current device id
val currentDeviceId = getCurrentDeviceIdAndPackageNameUseCase()?.deviceId ?: return
deleteDeviceApplicationUseCase(
deviceId = currentDeviceId,
packageName = packageName,
)
}
}

View file

@ -46,6 +46,7 @@ fun MainScreenTopBar(
onDeviceSelected: (DeviceItemUiModel) -> Unit,
deleteDevice: (DeviceItemUiModel) -> Unit,
onAppSelected: (DeviceAppUiModel) -> Unit,
deleteApp: (DeviceAppUiModel) -> Unit,
onTakeScreenshotClicked: () -> Unit,
recordState: RecordVideoStateUiModel,
onRecordClicked: () -> Unit,
@ -64,6 +65,7 @@ fun MainScreenTopBar(
onDeviceSelected = onDeviceSelected,
onAppSelected = onAppSelected,
deleteDevice = deleteDevice,
deleteApp = deleteApp,
)
Spacer(modifier = Modifier.weight(1f))
TopBarActions(

View file

@ -40,8 +40,9 @@ internal fun TopBarDeviceAndAppView(
devicesState: DevicesStateUiModel,
appsState: AppsStateUiModel,
onDeviceSelected: (DeviceItemUiModel) -> Unit,
onAppSelected: (DeviceAppUiModel) -> Unit,
deleteDevice: (DeviceItemUiModel) -> Unit,
onAppSelected: (DeviceAppUiModel) -> Unit,
deleteApp: (DeviceAppUiModel) -> Unit,
modifier: Modifier = Modifier,
) {
Row(
@ -60,6 +61,7 @@ internal fun TopBarDeviceAndAppView(
devicesState = devicesState,
appsState = appsState,
onAppSelected = onAppSelected,
deleteApp = deleteApp,
)
}
}

View file

@ -26,6 +26,7 @@ internal fun TopBarAppDropdown(
devicesState: DevicesStateUiModel,
appsState: AppsStateUiModel,
onAppSelected: (DeviceAppUiModel) -> Unit,
deleteApp: (DeviceAppUiModel) -> Unit,
) {
var expanded by remember { mutableStateOf(false) }
@ -37,11 +38,14 @@ internal fun TopBarAppDropdown(
val modifier = Modifier.menuAnchor(MenuAnchorType.PrimaryNotEditable)
appsState.appSelected?.let {
TopBarAppView(
deviceApp = it,
TopBarSelector(
onClick = { expanded = true },
modifier = modifier,
)
) {
TopBarAppView(
deviceApp = it,
modifier = Modifier.menuAnchor(MenuAnchorType.PrimaryNotEditable),
)
}
} ?: run {
TopBarSelector(
onClick = { expanded = true },
@ -73,6 +77,11 @@ internal fun TopBarAppDropdown(
onAppSelected(app)
expanded = false
},
selected = appsState.appSelected?.packageName == app.packageName,
deleteClick = {
deleteApp(app)
expanded = false
}
)
}
}

View file

@ -2,16 +2,25 @@ package io.github.openflocon.flocondesktop.main.ui.view.topbar
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Close
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toComposeImageBitmap
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
@ -21,44 +30,63 @@ import flocondesktop.composeapp.generated.resources.Res
import flocondesktop.composeapp.generated.resources.smartphone
import io.github.openflocon.flocondesktop.main.ui.model.DeviceAppUiModel
import io.github.openflocon.library.designsystem.FloconTheme
import io.github.openflocon.library.designsystem.components.FloconIcon
import org.jetbrains.compose.resources.painterResource
import org.jetbrains.skia.Image
import kotlin.io.encoding.Base64
@Composable
internal fun TopBarAppView(
deviceApp: DeviceAppUiModel,
onClick: () -> Unit,
modifier: Modifier = Modifier,
onClick: (() -> Unit)? = null,
selected: Boolean = false,
deleteClick: (() -> Unit)? = null,
) {
TopBarSelector(
onClick = onClick,
modifier = modifier,
Row(
modifier = modifier
.then(if (onClick != null) Modifier.clickable {
onClick()
} else Modifier
).padding(horizontal = 8.dp, 4.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Row(
modifier = Modifier.padding(horizontal = 4.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
AppImage(
deviceApp = deviceApp,
modifier = Modifier.size(24.dp),
AppImage(
deviceApp = deviceApp,
modifier = Modifier.size(24.dp),
)
Column {
Text(
text = deviceApp.name,
style = FloconTheme.typography.bodySmall.copy(fontWeight = FontWeight.Bold),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
color = FloconTheme.colorPalette.onPanel,
)
Column {
Text(
text = deviceApp.name,
style = FloconTheme.typography.bodySmall.copy(fontWeight = FontWeight.Bold),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
color = FloconTheme.colorPalette.onPanel,
)
Text(
text = deviceApp.packageName,
style = FloconTheme.typography.bodySmall.copy(
fontSize = 10.sp,
),
color = FloconTheme.colorPalette.onPanel.copy(alpha = 0.8f),
Text(
text = deviceApp.packageName,
style = FloconTheme.typography.bodySmall.copy(
fontSize = 10.sp,
),
color = FloconTheme.colorPalette.onPanel.copy(alpha = 0.8f),
)
}
if (!selected && deleteClick != null) {
Spacer(modifier = Modifier.weight(1f))
Box(
Modifier.clip(RoundedCornerShape(4.dp))
.background(
Color.White.copy(alpha = 0.8f)
).padding(2.dp).clickable {
deleteClick()
},
contentAlignment = Alignment.Center,
) {
FloconIcon(
imageVector = Icons.Outlined.Close,
tint = FloconTheme.colorPalette.panel,
modifier = Modifier.size(14.dp)
)
}
}

View file

@ -41,11 +41,17 @@ internal fun TopBarDeviceDropdown(
when (state) {
DevicesStateUiModel.Empty -> Empty()
DevicesStateUiModel.Loading -> Loading()
is DevicesStateUiModel.WithDevices -> TopBarDeviceView(
device = state.deviceSelected,
onClick = {},
modifier = Modifier.menuAnchor(MenuAnchorType.PrimaryNotEditable),
)
is DevicesStateUiModel.WithDevices -> TopBarSelector(
onClick = {
expanded = true
},
modifier = modifier,
) {
TopBarDeviceView(
device = state.deviceSelected,
modifier = Modifier.menuAnchor(MenuAnchorType.PrimaryNotEditable),
)
}
}
ExposedDropdownMenu(
expanded = expanded,
@ -64,7 +70,6 @@ internal fun TopBarDeviceDropdown(
onDeviceSelected(device)
expanded = false
},
canDelete = true,
onDelete = {
deleteDevice(device)
expanded = false

View file

@ -2,104 +2,109 @@ package io.github.openflocon.flocondesktop.main.ui.view.topbar.device
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.MobileOff
import androidx.compose.material.icons.filled.Smartphone
import androidx.compose.material.icons.outlined.Check
import androidx.compose.material.icons.outlined.Delete
import androidx.compose.material.icons.outlined.Close
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import io.github.openflocon.flocondesktop.main.ui.model.DeviceItemUiModel
import io.github.openflocon.flocondesktop.main.ui.view.topbar.TopBarSelector
import io.github.openflocon.library.designsystem.FloconTheme
import io.github.openflocon.library.designsystem.components.FloconIcon
@Composable
internal fun TopBarDeviceView(
device: DeviceItemUiModel,
onClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
onClick: (() -> Unit)? = null,
selected: Boolean = false,
canDelete: Boolean = false,
onDelete: () -> Unit = {},
onDelete: (() -> Unit)? = null,
) {
TopBarSelector(
onClick = onClick,
enabled = enabled,
modifier = modifier,
Row(
modifier = modifier
.then(if (onClick != null) Modifier.clickable {
onClick()
} else Modifier
).padding(horizontal = 8.dp, 4.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp),
) {
Box(
modifier = Modifier.padding(horizontal = 4.dp)
.graphicsLayer {
alpha = if (device.isActive) 1f else 0.4f
}
) {
Image(
modifier = Modifier.width(20.dp),
imageVector = if (device.isActive.not()) {
Icons.Filled.MobileOff
} else {
Icons.Filled.Smartphone
},
colorFilter = ColorFilter.tint(FloconTheme.colorPalette.onSurface),
contentDescription = null,
)
}
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp),
) {
Box(
modifier = Modifier.padding(horizontal = 4.dp)
.graphicsLayer {
alpha = if (device.isActive) 1f else 0.4f
}
Column(
modifier = Modifier,
verticalArrangement = Arrangement.Center
) {
Image(
modifier = Modifier.width(20.dp),
imageVector = if (device.isActive.not()) {
Icons.Filled.MobileOff
} else {
Icons.Filled.Smartphone
},
colorFilter = ColorFilter.tint(FloconTheme.colorPalette.onSurface),
contentDescription = null,
Text(
text = device.deviceName,
color = FloconTheme.colorPalette.onPanel,
style = FloconTheme.typography.bodySmall.copy(fontWeight = FontWeight.Bold),
)
Text(
text = if (device.isActive.not()) {
"Disconnected"
} else "Connected",
color = FloconTheme.colorPalette.onPanel,
style = FloconTheme.typography.bodySmall.copy(
fontSize = 10.sp,
),
)
}
Row(
verticalAlignment = Alignment.CenterVertically,
) {
Column(
modifier = Modifier,
verticalArrangement = Arrangement.Center
) {
Text(
text = device.deviceName,
color = FloconTheme.colorPalette.onPanel,
style = FloconTheme.typography.bodySmall.copy(fontWeight = FontWeight.Bold),
)
Text(
text = if (device.isActive.not()) {
"Disconnected"
} else "Connected",
color = FloconTheme.colorPalette.onPanel,
style = FloconTheme.typography.bodySmall.copy(
fontSize = 10.sp,
),
)
}
if (selected)
FloconIcon(
imageVector = Icons.Outlined.Check,
tint = FloconTheme.colorPalette.onPanel,
)
else if (canDelete) {
FloconIcon(
imageVector = Icons.Outlined.Delete,
tint = FloconTheme.colorPalette.onPanel,
modifier = Modifier.clickable {
if (!selected && onDelete != null) {
Spacer(modifier = Modifier.weight(1f))
Box(
Modifier.clip(RoundedCornerShape(4.dp))
.background(
Color.White.copy(alpha = 0.8f)
).padding(2.dp).clickable {
onDelete()
}
},
contentAlignment = Alignment.Center,
) {
FloconIcon(
imageVector = Icons.Outlined.Close,
tint = FloconTheme.colorPalette.panel,
modifier = Modifier.size(14.dp)
)
}
}

View file

@ -27,5 +27,6 @@ interface LocalCurrentDeviceDataSource {
// endregion
suspend fun delete(deviceId: DeviceId)
suspend fun deleteApp(deviceId: DeviceId, packageName: AppPackageName)
suspend fun clear()
}

View file

@ -64,6 +64,19 @@ class LocalCurrentDeviceDataSourceInMemory : LocalCurrentDeviceDataSource {
connectedDevicesAndAppsForSession.update { it + deviceIdAndPackageName }
}
override suspend fun deleteApp(deviceId: DeviceId, packageName: AppPackageName) {
connectedDevicesAndAppsForSession.update {
it.filterNot {
it.deviceId == deviceId && it.packageName == packageName
}.toSet()
}
currentDeviceApp.update { map ->
if(map[deviceId] == packageName)
map - deviceId
else map
}
}
override suspend fun delete(deviceId: DeviceId) {
_currentDeviceId.update {
if(it == deviceId)

View file

@ -1,5 +1,6 @@
package io.github.openflocon.domain.device
import io.github.openflocon.domain.device.usecase.DeleteDeviceApplicationUseCase
import io.github.openflocon.domain.device.usecase.DeleteDeviceUseCase
import io.github.openflocon.domain.device.usecase.GetCurrentDeviceIdAndPackageNameUseCase
import io.github.openflocon.domain.device.usecase.GetCurrentDeviceIdUseCase
@ -36,4 +37,5 @@ internal val deviceModule = module {
factoryOf(::StartRecordingVideoUseCase)
factoryOf(::StopRecordingVideoUseCase)
factoryOf(::DeleteDeviceUseCase)
factoryOf(::DeleteDeviceApplicationUseCase)
}

View file

@ -1,5 +1,6 @@
package io.github.openflocon.domain.device.repository
import io.github.openflocon.domain.device.models.AppPackageName
import io.github.openflocon.domain.device.models.DeviceAppDomainModel
import io.github.openflocon.domain.device.models.DeviceDomainModel
import io.github.openflocon.domain.device.models.DeviceId
@ -35,5 +36,9 @@ interface DevicesRepository {
// endregion
suspend fun deleteDevice(deviceId: DeviceId)
suspend fun deleteApplication(
deviceId : DeviceId,
packageName : AppPackageName,
)
suspend fun clear()
}

View file

@ -0,0 +1,19 @@
package io.github.openflocon.domain.device.usecase
import io.github.openflocon.domain.device.models.AppPackageName
import io.github.openflocon.domain.device.models.DeviceId
import io.github.openflocon.domain.device.repository.DevicesRepository
class DeleteDeviceApplicationUseCase(
private val devicesRepository: DevicesRepository,
) {
suspend operator fun invoke(
deviceId: DeviceId,
packageName: AppPackageName,
) {
devicesRepository.deleteApplication(
deviceId = deviceId,
packageName = packageName,
)
}
}