diff --git a/android/app/src/main/kotlin/io/rebble/cobble/bridges/common/KMPApiBridge.kt b/android/app/src/main/kotlin/io/rebble/cobble/bridges/common/KMPApiBridge.kt index ff78aa50..8db070a4 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/bridges/common/KMPApiBridge.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/bridges/common/KMPApiBridge.kt @@ -37,4 +37,12 @@ class KMPApiBridge @Inject constructor( activity.startActivity(intent) } } + override fun openWatchesView() { + activity?.let { + Timber.d("Opening watches view") + val intent = Intent(activity.context, MainActivity::class.java) + intent.putExtra("navigationPath", Routes.Home.WATCHES_PAGE) + activity.startActivity(intent) + } + } } \ No newline at end of file diff --git a/android/app/src/main/kotlin/io/rebble/cobble/pigeons/Pigeons.java b/android/app/src/main/kotlin/io/rebble/cobble/pigeons/Pigeons.java index 5f43eab1..a6b6d577 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/pigeons/Pigeons.java +++ b/android/app/src/main/kotlin/io/rebble/cobble/pigeons/Pigeons.java @@ -5458,6 +5458,8 @@ public interface KMPApi { void openLockerView(); + void openWatchesView(); + /** The codec used by KMPApi. */ static @NonNull MessageCodec getCodec() { return KMPApiCodec.INSTANCE; @@ -5510,6 +5512,28 @@ static void setup(@NonNull BinaryMessenger binaryMessenger, @Nullable KMPApi api channel.setMessageHandler(null); } } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.KMPApi.openWatchesView", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + try { + api.openWatchesView(); + wrapped.add(0, null); + } + catch (Throwable exception) { + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } } } } diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index e1c0750f..72465a16 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -8,4 +8,10 @@ Bluetooth is off Warnings Background Jobs + Disconnected + Connect to watch + Disconnect from watch + Check for updates + Download Update + Forget Watch \ No newline at end of file diff --git a/android/shared/src/commonMain/composeResources/values/strings.xml b/android/shared/src/commonMain/composeResources/values/strings.xml new file mode 100644 index 00000000..68695698 --- /dev/null +++ b/android/shared/src/commonMain/composeResources/values/strings.xml @@ -0,0 +1,22 @@ + + + Boot URL change requested + An external source requested to change the boot URL, ensure you trust this source as changing to a malicious source shares all web services data with it!\n%s + + Connected + Watch connecting + Bluetooth is off + Warnings + Background Jobs + Disconnected + Connect to watch + Disconnect from watch + Check for updates + Update Available + Download Update + Forget Watch + My watches + Nothing Connected + Background service stopped + Other Watches + \ No newline at end of file diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/data/WatchItem.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/data/WatchItem.kt new file mode 100644 index 00000000..eaa2f190 --- /dev/null +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/data/WatchItem.kt @@ -0,0 +1,9 @@ +package io.rebble.cobble.shared.data + +data class WatchItem( + val name: String, + val softwareVersion: String, + val isConnected: Boolean, + val updateAvailable: Boolean + //TODO Possibly have a variable for the watch icon here +) \ No newline at end of file diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/common/Icons.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/common/Icons.kt index 695a07e4..4b910c6e 100644 --- a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/common/Icons.kt +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/common/Icons.kt @@ -3,8 +3,10 @@ package io.rebble.cobble.shared.ui.common import android.shared.generated.resources.RebbleIcons import android.shared.generated.resources.Res import androidx.compose.foundation.layout.width +import androidx.compose.material3.LocalContentColor import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.unit.dp import org.jetbrains.compose.resources.Font @@ -43,15 +45,15 @@ object RebbleIcons { @Composable fun rocket(modifier: Modifier = Modifier.width(24.dp)) = TextIcon(font(), Char(0xe80d), modifier = modifier) @Composable - fun unpairFromWatch(modifier: Modifier = Modifier.width(24.dp)) = TextIcon(font(), Char(0xe80e), modifier = modifier) + fun unpairFromWatch(modifier: Modifier = Modifier.width(24.dp), tint: Color = LocalContentColor.current) = TextIcon(font(), Char(0xe80e), modifier = modifier, tint = tint) @Composable - fun applyUpdate(modifier: Modifier = Modifier.width(24.dp)) = TextIcon(font(), Char(0xe80f), modifier = modifier) + fun applyUpdate(modifier: Modifier = Modifier.width(24.dp), tint: Color = LocalContentColor.current) = TextIcon(font(), Char(0xe80f), modifier = modifier, tint = tint) @Composable - fun checkForUpdates(modifier: Modifier = Modifier.width(24.dp)) = TextIcon(font(), Char(0xe810), modifier = modifier) + fun checkForUpdates(modifier: Modifier = Modifier.width(24.dp), tint: Color = LocalContentColor.current) = TextIcon(font(), Char(0xe810), modifier = modifier, tint = tint) @Composable - fun disconnectFromWatch(modifier: Modifier = Modifier.width(24.dp)) = TextIcon(font(), Char(0xe811), modifier = modifier) + fun disconnectFromWatch(modifier: Modifier = Modifier.width(24.dp), tint: Color = LocalContentColor.current) = TextIcon(font(), Char(0xe811), modifier = modifier, tint = tint) @Composable - fun connectToWatch(modifier: Modifier = Modifier.width(24.dp)) = TextIcon(font(), Char(0xe812), modifier = modifier) + fun connectToWatch(modifier: Modifier = Modifier.width(24.dp), tint: Color = LocalContentColor.current) = TextIcon(font(), Char(0xe812), modifier = modifier, tint = tint) @Composable fun devices(modifier: Modifier = Modifier.width(24.dp)) = TextIcon(font(), Char(0xe813), modifier = modifier) @Composable diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/nav/Routes.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/nav/Routes.kt index bf37c617..9d9e1b76 100644 --- a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/nav/Routes.kt +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/nav/Routes.kt @@ -6,5 +6,6 @@ object Routes { const val LOCKER_APPS = "locker_apps" const val LOCKER_WATCHFACES = "locker_watchfaces" const val TEST_PAGE = "test_page" + const val WATCHES_PAGE = "watches_page" } } \ No newline at end of file diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/view/MainView.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/view/MainView.kt index 668eb1ee..a81992da 100644 --- a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/view/MainView.kt +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/view/MainView.kt @@ -46,6 +46,9 @@ fun MainView(navController: NavHostController = rememberNavController()) { composable(Routes.Home.TEST_PAGE) { HomeScaffold(HomePage.TestPage, onNavChange = navController::navigate) } + composable(Routes.Home.WATCHES_PAGE) { + HomeScaffold(HomePage.WatchesPage, onNavChange = navController::navigate) + } dialog("${Routes.DIALOG_APP_INSTALL}?uri={uri}", arguments = listOf(navArgument("uri") { nullable = false type = NavType.StringType diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/view/home/HomeScaffold.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/view/home/HomeScaffold.kt index 2cf47dca..92ae5521 100644 --- a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/view/home/HomeScaffold.kt +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/view/home/HomeScaffold.kt @@ -4,6 +4,8 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add import androidx.compose.material3.* import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf @@ -15,11 +17,13 @@ import io.rebble.cobble.shared.ui.common.RebbleIcons import io.rebble.cobble.shared.ui.nav.Routes import io.rebble.cobble.shared.ui.view.home.locker.Locker import io.rebble.cobble.shared.ui.view.home.locker.LockerTabs +import io.rebble.cobble.shared.ui.view.home.watches.WatchesPage import kotlinx.coroutines.launch open class HomePage { class Locker(val tab: LockerTabs) : HomePage() object TestPage : HomePage() + object WatchesPage : HomePage() } @Composable @@ -28,6 +32,7 @@ fun HomeScaffold(page: HomePage, onNavChange: (String) -> Unit) { val scope = rememberCoroutineScope() val searchingState = remember { mutableStateOf(false) } Scaffold( + contentWindowInsets = WindowInsets(0.dp), // Needed in scaffold for edgetoedge to work snackbarHost = { SnackbarHost(snackbarHostState) }, /*topBar = { TopAppBar( @@ -51,6 +56,13 @@ fun HomeScaffold(page: HomePage, onNavChange: (String) -> Unit) { icon = { RebbleIcons.locker() }, label = { Text("Locker") } ) + + NavigationBarItem( + selected = page is HomePage.WatchesPage, + onClick = { onNavChange(Routes.Home.WATCHES_PAGE) }, + icon = { RebbleIcons.devices() }, + label = { Text("Devices") } + ) } }, floatingActionButton = { @@ -67,6 +79,18 @@ fun HomeScaffold(page: HomePage, onNavChange: (String) -> Unit) { }, ) } + is HomePage.WatchesPage -> { + FloatingActionButton( + modifier = Modifier + .padding(16.dp), + onClick = { + searchingState.value = false //TODO Change this so that it actually goes into pairing mode. + }, + content = { + Icon(Icons.Filled.Add, "Pair a watch") + } + ) + } } } ) { innerPadding -> @@ -84,6 +108,9 @@ fun HomeScaffold(page: HomePage, onNavChange: (String) -> Unit) { } }) } + is HomePage.WatchesPage -> { + WatchesPage() + } } } } diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/view/home/watches/WatchBottomSheetContent.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/view/home/watches/WatchBottomSheetContent.kt new file mode 100644 index 00000000..9069e182 --- /dev/null +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/view/home/watches/WatchBottomSheetContent.kt @@ -0,0 +1,176 @@ +package io.rebble.cobble.shared.ui.view.home.watches + +import android.shared.generated.resources.* +import android.shared.generated.resources.Res +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import io.rebble.cobble.shared.data.WatchItem +import io.rebble.cobble.shared.ui.common.AppIconContainer +import io.rebble.cobble.shared.ui.common.RebbleIcons +import kotlinx.coroutines.launch +import org.jetbrains.compose.resources.stringResource + +val CONNECTED_BACKGROUND = Color(121,249,205) +val UPDATE_FOREGROUND = Color(0, 108, 81) + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun WatchBottomSheetContent(watch: WatchItem, + onToggleConnection: () -> Unit, + onForgetWatch: () -> Unit, + onCheckForUpdates: () -> Unit, + clearSelection: () -> Unit) { + + val sheetState = rememberModalBottomSheetState() + val coroutineScope = rememberCoroutineScope() + + ModalBottomSheet( + containerColor = MaterialTheme.colorScheme.surfaceContainer, + onDismissRequest = { + coroutineScope.launch { + sheetState.hide() // First, hide the sheet smoothly + }.invokeOnCompletion { + clearSelection() // Then, reset selected watch + } + }, + sheetState = sheetState + ){ + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + AppIconContainer(color = if (watch.isConnected) { + CONNECTED_BACKGROUND + } else { + MaterialTheme.colorScheme.primaryContainer + }, + content = { RebbleIcons.deadWatchGhost80() } ) //TODO Switch With Watch Icon + + Spacer(modifier = Modifier.width(12.dp)) + + Column { + Text(text = watch.name, + fontWeight = FontWeight.Bold, + style = MaterialTheme.typography.bodyLarge) + Text( + text = if (watch.isConnected && watch.updateAvailable){ + "${watch.softwareVersion} - ${stringResource(Res.string.update_available)}!" + } else if(watch.isConnected) { + "${watch.softwareVersion} - ${stringResource(Res.string.connected)}!" + } else { + stringResource(Res.string.disconnected) + }, + + color = if (watch.isConnected && watch.updateAvailable){ + UPDATE_FOREGROUND + } else { + MaterialTheme.colorScheme.secondary + }, + + fontWeight = FontWeight.SemiBold, + ) + } + } + + HorizontalDivider(thickness = 2.dp, color = MaterialTheme.colorScheme.secondary) + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + .clickable { onToggleConnection() }, + verticalAlignment = Alignment.CenterVertically + ) { + + if (watch.isConnected){ + RebbleIcons.disconnectFromWatch(tint = MaterialTheme.colorScheme.secondary) + } else { + RebbleIcons.connectToWatch(tint = MaterialTheme.colorScheme.secondary) + } + + Spacer(modifier = Modifier.width(12.dp)) + Text( + text = if (watch.isConnected){ + stringResource(Res.string.disconnect_watch) + } else { + stringResource(Res.string.connect_watch) + }, + + fontWeight = FontWeight.SemiBold, + color = MaterialTheme.colorScheme.secondary + ) + } + Row( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + .alpha(if (watch.isConnected) { + 1.0f + } else{ + 0.7f + }) + .clickable(enabled = watch.isConnected, + onClick = { onCheckForUpdates() }), + verticalAlignment = Alignment.CenterVertically + ) { + + if (watch.updateAvailable && watch.isConnected){ + RebbleIcons.applyUpdate(tint = UPDATE_FOREGROUND) + } + else { + RebbleIcons.checkForUpdates(tint = MaterialTheme.colorScheme.secondary) + } + + Spacer(modifier = Modifier.width(12.dp)) + Text( + text = if (watch.updateAvailable && watch.isConnected){ + stringResource(Res.string.download_update) + } else { + stringResource(Res.string.check_for_updates) + }, + + color = if (watch.updateAvailable && watch.isConnected){ + UPDATE_FOREGROUND + } else { + MaterialTheme.colorScheme.secondary + }, + + fontWeight = FontWeight.SemiBold, + ) + } + + HorizontalDivider(thickness = 2.dp, color = MaterialTheme.colorScheme.secondary) + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + .clickable { onForgetWatch() }, + verticalAlignment = Alignment.CenterVertically + ) { + RebbleIcons.unpairFromWatch(tint = Color.Red) + Spacer(modifier = Modifier.width(12.dp)) + Text( + text = stringResource(Res.string.forget_watch), + color = Color.Red + ) + } + } + } +} \ No newline at end of file diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/view/home/watches/WatchesListItem.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/view/home/watches/WatchesListItem.kt new file mode 100644 index 00000000..d6936c85 --- /dev/null +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/view/home/watches/WatchesListItem.kt @@ -0,0 +1,68 @@ +package io.rebble.cobble.shared.ui.view.home.watches + +import android.shared.generated.resources.Res +import android.shared.generated.resources.connected +import android.shared.generated.resources.disconnected +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import io.rebble.cobble.shared.data.WatchItem +import io.rebble.cobble.shared.ui.common.AppIconContainer +import io.rebble.cobble.shared.ui.common.RebbleIcons +import org.jetbrains.compose.resources.stringResource + +@Composable +fun WatchesListItem(watch: WatchItem, + onSelectWatch: () -> Unit, + onToggleConnection: () -> Unit) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 24.dp) + .clickable { onSelectWatch() }, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + AppIconContainer(color = if (watch.isConnected) { + CONNECTED_BACKGROUND + } else { + MaterialTheme.colorScheme.primaryContainer + }, + content = { RebbleIcons.deadWatchGhost80() }) + + Spacer(modifier = Modifier.width(12.dp)) + + Column { + Text(text = watch.name, fontWeight = FontWeight.Bold) + Text( + text = if (watch.isConnected) { + "${stringResource(Res.string.connected)}!" + } else { + stringResource(Res.string.disconnected) + }, + + fontWeight = FontWeight.SemiBold, + color = MaterialTheme.colorScheme.secondary + ) + } + } + // Ensure the icon's click event is separate from the row's click event + Box( + modifier = Modifier.clickable { onToggleConnection() } + ) { + if (watch.isConnected){ + RebbleIcons.disconnectFromWatch(tint = ButtonDefaults.buttonColors().containerColor) + } else { + RebbleIcons.connectToWatch(tint = ButtonDefaults.buttonColors().containerColor) + } + } + } +} \ No newline at end of file diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/view/home/watches/WatchesPage.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/view/home/watches/WatchesPage.kt new file mode 100644 index 00000000..fbdc3054 --- /dev/null +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/view/home/watches/WatchesPage.kt @@ -0,0 +1,100 @@ +package io.rebble.cobble.shared.ui.view.home.watches + +import android.shared.generated.resources.* +import android.shared.generated.resources.Res +import android.shared.generated.resources.bg_service_stopped +import android.shared.generated.resources.my_watches +import android.shared.generated.resources.nothing_connected +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import io.rebble.cobble.shared.ui.common.AppIconContainer +import io.rebble.cobble.shared.ui.common.RebbleIcons +import io.rebble.cobble.shared.ui.viewmodel.WatchesListViewModel +import org.jetbrains.compose.resources.stringResource + + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun WatchesPage(viewModel: WatchesListViewModel = viewModel{ WatchesListViewModel() }) { + val selectedWatch by viewModel.selectedWatch + + if (selectedWatch != null) { + + WatchBottomSheetContent( + watch = selectedWatch!!, + onToggleConnection = { viewModel.toggleConnection(selectedWatch!!, true) }, + onForgetWatch = { viewModel.forgetWatch(selectedWatch!!) }, + onCheckForUpdates = { viewModel.checkForUpdates(selectedWatch!!) }, + clearSelection = { viewModel.clearSelection() } + ) + } + + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.Top + ) { + CenterAlignedTopAppBar( + colors = TopAppBarDefaults.centerAlignedTopAppBarColors( + containerColor = MaterialTheme.colorScheme.primaryContainer, + ), + title = { + Text( + text = stringResource(Res.string.my_watches), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + ) + val connectedWatch = viewModel.connectedWatch + if (connectedWatch == null) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 24.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(25.dp) + ) { + AppIconContainer(color = MaterialTheme.colorScheme.primaryContainer, + content = { RebbleIcons.disconnectFromWatch() }) + + Column { + Text( + text = stringResource(Res.string.nothing_connected), + style = MaterialTheme.typography.bodyLarge, + fontWeight = FontWeight.SemiBold + ) + Text( + text = stringResource(Res.string.bg_service_stopped), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + } else { + WatchesListItem(watch = connectedWatch, + onSelectWatch = { viewModel.selectWatch(connectedWatch) }, + onToggleConnection = { viewModel.toggleConnection(connectedWatch) }) + } + Text(modifier = Modifier + .padding(horizontal = 10.dp), + text = stringResource(Res.string.other_watches)) + HorizontalDivider(thickness = 2.dp, color = MaterialTheme.colorScheme.secondary) + + LazyColumn { + items(viewModel.disconnectedWatches, key = { it.name }) { watch -> + WatchesListItem(watch = watch, + onSelectWatch = { viewModel.selectWatch(watch) }, + onToggleConnection = { viewModel.toggleConnection(watch) }) + } + } + } +} \ No newline at end of file diff --git a/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/viewmodel/WatchesListViewModel.kt b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/viewmodel/WatchesListViewModel.kt new file mode 100644 index 00000000..deb12ca1 --- /dev/null +++ b/android/shared/src/commonMain/kotlin/io/rebble/cobble/shared/ui/viewmodel/WatchesListViewModel.kt @@ -0,0 +1,64 @@ +package io.rebble.cobble.shared.ui.viewmodel + +import androidx.compose.runtime.* +import androidx.lifecycle.ViewModel +import io.rebble.cobble.shared.data.WatchItem + + +class WatchesListViewModel : ViewModel(){ + private val _watches = mutableStateListOf( + WatchItem("Pebble Time 2747","v4.4.0-rbl", false, false), + WatchItem("Asterix ABXY", "v4.4.0-rbl", false, false), + WatchItem("Rebble Steel", "v4.4.0-rbl", true, true) + ) + val watches: List = _watches + + private val _selectedWatch = mutableStateOf(null) + val selectedWatch: State = _selectedWatch + + val connectedWatch: WatchItem? by derivedStateOf { + _watches.find { it.isConnected } + } + + val disconnectedWatches: List by derivedStateOf { + _watches.filter { !it.isConnected } + } + + fun selectWatch(watch: WatchItem) { + _selectedWatch.value = watch + } + + fun clearSelection() { + _selectedWatch.value = null + } + + fun toggleConnection(watch: WatchItem, updateSheet: Boolean = false) { + val index = _watches.indexOfFirst { it.name == watch.name } + if (index != -1) { + // TODO Actually connecting and disconnecting logic here + for (i in 0..<_watches.count()) { // Disconnect any other watches + if (i != index) { //Make sure that the connected watch is unaffected + _watches[i] = _watches[i].copy(isConnected = false) + } + } + _watches[index] = _watches[index].copy(isConnected = !_watches[index].isConnected) + if (updateSheet){ // Sometimes we don't want the sheet to pop up + _selectedWatch.value = _watches[index] // Ensure the bottom sheet updates + } + } + } + + fun forgetWatch(watch: WatchItem) { + val index = _watches.indexOfFirst { it.name == watch.name } + if (index != -1) { + // TODO Remove the watch from the database here + } + } + + fun checkForUpdates(watch: WatchItem) { + val index = _watches.indexOfFirst { it.name == watch.name } + if (index != -1) { + // TODO Check for Updates logic here + } + } +} \ No newline at end of file diff --git a/ios/Runner/Pigeon/Pigeons.h b/ios/Runner/Pigeon/Pigeons.h index ad7fc703..ec382cd4 100644 --- a/ios/Runner/Pigeon/Pigeons.h +++ b/ios/Runner/Pigeon/Pigeons.h @@ -622,6 +622,7 @@ NSObject *KMPApiGetCodec(void); @protocol KMPApi - (void)updateTokenToken:(StringWrapper *)token error:(FlutterError *_Nullable *_Nonnull)error; - (void)openLockerViewWithError:(FlutterError *_Nullable *_Nonnull)error; +- (void)openWatchesViewWithError:(FlutterError *_Nullable *_Nonnull)error; @end extern void KMPApiSetup(id binaryMessenger, NSObject *_Nullable api); diff --git a/ios/Runner/Pigeon/Pigeons.m b/ios/Runner/Pigeon/Pigeons.m index 2b94a967..8036588b 100644 --- a/ios/Runner/Pigeon/Pigeons.m +++ b/ios/Runner/Pigeon/Pigeons.m @@ -3661,4 +3661,21 @@ void KMPApiSetup(id binaryMessenger, NSObject *a [channel setMessageHandler:nil]; } } + { + FlutterBasicMessageChannel *channel = + [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.KMPApi.openWatchesView" + binaryMessenger:binaryMessenger + codec:KMPApiGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(openWatchesViewWithError:)], @"KMPApi api (%@) doesn't respond to @selector(openWatchesViewWithError:)", api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + FlutterError *error; + [api openWatchesViewWithError:&error]; + callback(wrapResult(nil, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } } diff --git a/lib/infrastructure/pigeons/pigeons.g.dart b/lib/infrastructure/pigeons/pigeons.g.dart index 4c1d30ab..f8a09133 100644 --- a/lib/infrastructure/pigeons/pigeons.g.dart +++ b/lib/infrastructure/pigeons/pigeons.g.dart @@ -3593,4 +3593,26 @@ class KMPApi { return; } } + + Future openWatchesView() async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.KMPApi.openWatchesView', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send(null) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else { + return; + } + } } diff --git a/lib/ui/home/home_page.dart b/lib/ui/home/home_page.dart index 522a4774..07a5eb8b 100644 --- a/lib/ui/home/home_page.dart +++ b/lib/ui/home/home_page.dart @@ -57,6 +57,10 @@ class HomePage extends HookConsumerWidget implements CobbleScreen { ), _TabConfig(tr.homePage.store, RebbleIcons.rebble_store, child: StoreTab()), _TabConfig(tr.homePage.watches, RebbleIcons.devices, child: MyWatchesTab()), + // Use the comment below to access the KMP UI version of the Watch Tab + // by clicking the watch tab in flutter UI, first comment the line above + // _TabConfig(tr.homePage.watches, RebbleIcons.devices, + // onSelect: () => KMPApi().openWatchesView(), child: PlaceholderScreen()), _TabConfig(tr.homePage.settings, RebbleIcons.settings, child: Settings()), ]; diff --git a/pigeons/pigeons.dart b/pigeons/pigeons.dart index c0024a63..7e24257c 100644 --- a/pigeons/pigeons.dart +++ b/pigeons/pigeons.dart @@ -550,4 +550,5 @@ abstract class KeepUnusedHack { abstract class KMPApi { void updateToken(StringWrapper token); void openLockerView(); + void openWatchesView(); }