diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index a3bce7e7e..92becda4b 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -149,6 +149,7 @@ dependencies {
implementation(libs.nestedScrollView)
implementation(libs.precompose.molecule)
implementation(libs.compose.placeholder.material3)
+ implementation(libs.bundles.xr)
if (project.file("google-services.json").exists()) {
implementation(platform(libs.firebase.bom))
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index d844da2a1..29052b411 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -4,7 +4,7 @@
-
+
disabledIconColor
- it.selected -> MaterialTheme.colorScheme.primary
- else -> unselectedIconColor
+ scope.itemList.forEach {
+ Box(
+ modifier =
+ it.modifier
+ .weight(1f)
+ .combinedClickable(
+ interactionSource = it.interactionSource,
+ onClick = it.onClick,
+ indication = LocalIndication.current,
+ onLongClick = it.onLongClick,
+ ).height(bottomBarHeight),
+ contentAlignment = Alignment.Center,
+ ) {
+ val colors =
+ it.colors?.navigationBarItemColors ?: NavigationBarItemDefaults.colors()
+ val color =
+ with(colors) {
+ when {
+ !it.enabled -> disabledIconColor
+ it.selected -> MaterialTheme.colorScheme.primary
+ else -> unselectedIconColor
+ }
+ }
+ val iconColor by animateColorAsState(
+ targetValue = color,
+ animationSpec = tween(100),
+ )
+ CompositionLocalProvider(LocalContentColor provides iconColor) {
+ NavigationItemIcon(icon = it.icon, badge = it.badge)
}
}
- val iconColor by animateColorAsState(
- targetValue = color,
- animationSpec = tween(100),
- )
- CompositionLocalProvider(LocalContentColor provides iconColor) {
- NavigationItemIcon(icon = it.icon, badge = it.badge)
}
}
}
@@ -320,6 +347,45 @@ fun NavigationSuiteScaffold2(
}
}
+@Composable
+private fun OptionalSubspace(content: @Composable () -> Unit) {
+ val session = LocalSession.current
+ if (LocalSpatialCapabilities.current.isSpatialUiEnabled) {
+ androidx.xr.compose.spatial.Subspace {
+ SpatialPanel(
+ SubspaceModifier
+ .width(1280.dp)
+ .height(800.dp)
+ .resizable()
+ .movable(),
+ ) {
+ content.invoke()
+
+ Orbiter(
+ position = OrbiterEdge.Top,
+ offset = EdgeOffset.inner(offset = 20.dp),
+ alignment = Alignment.End,
+ shape = SpatialRoundedCornerShape(CornerSize(28.dp)),
+ ) {
+ FilledTonalIconButton(
+ onClick = {
+ session?.requestHomeSpaceMode()
+ },
+ modifier = Modifier.size(56.dp),
+ ) {
+ FAIcon(
+ FontAwesomeIcons.Solid.DownLeftAndUpRightToCenter,
+ contentDescription = null,
+ )
+ }
+ }
+ }
+ }
+ } else {
+ content.invoke()
+ }
+}
+
@Composable
private fun ColumnScope.DrawerContent(
drawerHeader: @Composable (ColumnScope.() -> Unit)?,
diff --git a/app/src/main/java/dev/dimension/flare/ui/screen/bluesky/BlueskyFeedsScreen.kt b/app/src/main/java/dev/dimension/flare/ui/screen/bluesky/BlueskyFeedsScreen.kt
index be7011cf5..a8c4bf38b 100644
--- a/app/src/main/java/dev/dimension/flare/ui/screen/bluesky/BlueskyFeedsScreen.kt
+++ b/app/src/main/java/dev/dimension/flare/ui/screen/bluesky/BlueskyFeedsScreen.kt
@@ -78,10 +78,13 @@ internal fun BlueskyFeedsRoute(
) {
val scaffoldNavigator =
rememberListDetailPaneScaffoldNavigator()
+ val scope = rememberCoroutineScope()
BackHandler(
scaffoldNavigator.canNavigateBack(),
) {
- scaffoldNavigator.navigateBack()
+ scope.launch {
+ scaffoldNavigator.navigateBack()
+ }
}
ListDetailPaneScaffold(
@@ -92,22 +95,26 @@ internal fun BlueskyFeedsRoute(
BlueskyFeedsScreen(
accountType = accountType,
toFeed = {
- scaffoldNavigator.navigateTo(
- ListDetailPaneScaffoldRole.Detail,
- BlueskyFeedUri(it.id),
- )
+ scope.launch {
+ scaffoldNavigator.navigateTo(
+ ListDetailPaneScaffoldRole.Detail,
+ BlueskyFeedUri(it.id),
+ )
+ }
},
)
}
},
detailPane = {
AnimatedPane {
- scaffoldNavigator.currentDestination?.content?.let {
+ scaffoldNavigator.currentDestination?.contentKey?.let {
BlueskyFeedScreen(
accountType = accountType,
uri = it.value,
onBack = {
- scaffoldNavigator.navigateBack()
+ scope.launch {
+ scaffoldNavigator.navigateBack()
+ }
},
)
}
diff --git a/app/src/main/java/dev/dimension/flare/ui/screen/dm/DMListScreen.kt b/app/src/main/java/dev/dimension/flare/ui/screen/dm/DMListScreen.kt
index d6a46e3b6..b8eada8eb 100644
--- a/app/src/main/java/dev/dimension/flare/ui/screen/dm/DMListScreen.kt
+++ b/app/src/main/java/dev/dimension/flare/ui/screen/dm/DMListScreen.kt
@@ -81,6 +81,7 @@ internal fun DMScreenRoute(
) {
val scaffoldNavigator =
rememberListDetailPaneScaffoldNavigator()
+ val scope = rememberCoroutineScope()
if (initialUserKey != null) {
LaunchedEffect(initialUserKey) {
scaffoldNavigator.navigateTo(
@@ -92,7 +93,9 @@ internal fun DMScreenRoute(
BackHandler(
scaffoldNavigator.canNavigateBack(),
) {
- scaffoldNavigator.navigateBack()
+ scope.launch {
+ scaffoldNavigator.navigateBack()
+ }
}
}
@@ -104,22 +107,28 @@ internal fun DMScreenRoute(
DMListScreen(
accountType = accountType,
onItemClicked = { key ->
- scaffoldNavigator.navigateTo(
- ListDetailPaneScaffoldRole.Detail,
- DMPaneNavArgs(key.toString(), isUserKey = false),
- )
+ scope.launch {
+ scaffoldNavigator.navigateTo(
+ ListDetailPaneScaffoldRole.Detail,
+ DMPaneNavArgs(key.toString(), isUserKey = false),
+ )
+ }
},
)
}
},
detailPane = {
AnimatedPane {
- scaffoldNavigator.currentDestination?.content?.let { args ->
+ scaffoldNavigator.currentDestination?.contentKey?.let { args ->
if (args.isUserKey) {
UserDMConversationScreen(
accountType = accountType,
userKey = MicroBlogKey.valueOf(args.key),
- onBack = scaffoldNavigator::navigateBack,
+ onBack = {
+ scope.launch {
+ scaffoldNavigator.navigateBack()
+ }
+ },
navigationState = navigationState,
toProfile = {
navigator.navigate(ProfileRouteDestination(userKey = it, accountType = accountType))
@@ -129,7 +138,11 @@ internal fun DMScreenRoute(
DMConversationScreen(
accountType = accountType,
roomKey = MicroBlogKey.valueOf(args.key),
- onBack = scaffoldNavigator::navigateBack,
+ onBack = {
+ scope.launch {
+ scaffoldNavigator.navigateBack()
+ }
+ },
navigationState = navigationState,
toProfile = {
navigator.navigate(ProfileRouteDestination(userKey = it, accountType = accountType))
diff --git a/app/src/main/java/dev/dimension/flare/ui/screen/home/HomeScreen.kt b/app/src/main/java/dev/dimension/flare/ui/screen/home/HomeScreen.kt
index b47bbd574..b869ece07 100644
--- a/app/src/main/java/dev/dimension/flare/ui/screen/home/HomeScreen.kt
+++ b/app/src/main/java/dev/dimension/flare/ui/screen/home/HomeScreen.kt
@@ -62,6 +62,8 @@ import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
+import androidx.xr.compose.platform.LocalSession
+import androidx.xr.compose.platform.LocalSpatialCapabilities
import com.ramcosta.composedestinations.DestinationsNavHost
import com.ramcosta.composedestinations.animations.NavHostAnimatedDestinationStyle
import com.ramcosta.composedestinations.generated.NavGraphs
@@ -86,9 +88,11 @@ import com.ramcosta.composedestinations.utils.dialogComposable
import com.ramcosta.composedestinations.utils.toDestinationsNavigator
import compose.icons.FontAwesomeIcons
import compose.icons.fontawesomeicons.Solid
+import compose.icons.fontawesomeicons.solid.DownLeftAndUpRightToCenter
import compose.icons.fontawesomeicons.solid.EllipsisVertical
import compose.icons.fontawesomeicons.solid.Gear
import compose.icons.fontawesomeicons.solid.Pen
+import compose.icons.fontawesomeicons.solid.UpRightAndDownLeftFromCenter
import dev.dimension.flare.R
import dev.dimension.flare.data.model.AllListTabItem
import dev.dimension.flare.data.model.Bluesky
@@ -166,6 +170,8 @@ internal fun HomeScreen(
navBackStackEntry?.destination?.route
}
}
+ val session = LocalSession.current
+ val spatialCapabilities = LocalSpatialCapabilities.current
val hapticFeedback = LocalHapticFeedback.current
state.tabs
.onSuccess { tabs ->
@@ -359,6 +365,38 @@ internal fun HomeScreen(
}
},
footerItems = {
+ if (!spatialCapabilities.isSpatialUiEnabled) {
+ item(
+ selected = false,
+ onClick = {
+ if (spatialCapabilities.isSpatialUiEnabled) {
+ session?.requestHomeSpaceMode()
+ } else {
+ session?.requestFullSpaceMode()
+ }
+ },
+ icon = {
+ if (spatialCapabilities.isSpatialUiEnabled) {
+ FAIcon(
+ imageVector = FontAwesomeIcons.Solid.DownLeftAndUpRightToCenter,
+ contentDescription = null,
+ )
+ } else {
+ FAIcon(
+ imageVector = FontAwesomeIcons.Solid.UpRightAndDownLeftFromCenter,
+ contentDescription = null,
+ )
+ }
+ },
+ label = {
+ if (spatialCapabilities.isSpatialUiEnabled) {
+ Text("Spatial")
+ } else {
+ Text("Spatial")
+ }
+ },
+ )
+ }
accountTypeState.user.onSuccess {
item(
selected = currentRoute == SettingsRouteDestination.route,
diff --git a/app/src/main/java/dev/dimension/flare/ui/screen/list/ListScreen.kt b/app/src/main/java/dev/dimension/flare/ui/screen/list/ListScreen.kt
index e5903c52d..a7538987b 100644
--- a/app/src/main/java/dev/dimension/flare/ui/screen/list/ListScreen.kt
+++ b/app/src/main/java/dev/dimension/flare/ui/screen/list/ListScreen.kt
@@ -32,6 +32,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -78,6 +79,7 @@ import dev.dimension.flare.ui.screen.home.TimelineRoute
import dev.dimension.flare.ui.theme.MediumAlpha
import dev.dimension.flare.ui.theme.screenHorizontalPadding
import io.github.fornewid.placeholder.material3.placeholder
+import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize
import moe.tlaster.precompose.molecule.producePresenter
@@ -93,10 +95,13 @@ internal fun ListScreenRoute(
) {
val scaffoldNavigator =
rememberListDetailPaneScaffoldNavigator()
+ val scope = rememberCoroutineScope()
BackHandler(
scaffoldNavigator.canNavigateBack(),
) {
- scaffoldNavigator.navigateBack()
+ scope.launch {
+ scaffoldNavigator.navigateBack()
+ }
}
ListDetailPaneScaffold(
directive = scaffoldNavigator.scaffoldDirective,
@@ -106,13 +111,15 @@ internal fun ListScreenRoute(
ListScreen(
accountType = accountType,
toList = { item ->
- scaffoldNavigator.navigateTo(
- ListDetailPaneScaffoldRole.Detail,
- ListDetailPaneNavArgs(
- id = item.id,
- title = item.title,
- ),
- )
+ scope.launch {
+ scaffoldNavigator.navigateTo(
+ ListDetailPaneScaffoldRole.Detail,
+ ListDetailPaneNavArgs(
+ id = item.id,
+ title = item.title,
+ ),
+ )
+ }
},
createList = {
navigator.navigate(CreateListRouteDestination(accountType = accountType))
@@ -139,7 +146,7 @@ internal fun ListScreenRoute(
},
detailPane = {
AnimatedPane {
- scaffoldNavigator.currentDestination?.content?.let { args ->
+ scaffoldNavigator.currentDestination?.contentKey?.let { args ->
TimelineRoute(
navigator = navigator,
tabItem =
diff --git a/app/src/main/java/dev/dimension/flare/ui/screen/settings/SettingsScreen.kt b/app/src/main/java/dev/dimension/flare/ui/screen/settings/SettingsScreen.kt
index 5b8ee082e..30fe228f2 100644
--- a/app/src/main/java/dev/dimension/flare/ui/screen/settings/SettingsScreen.kt
+++ b/app/src/main/java/dev/dimension/flare/ui/screen/settings/SettingsScreen.kt
@@ -20,6 +20,7 @@ import androidx.compose.material3.adaptive.navigation.rememberListDetailPaneScaf
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.platform.UriHandler
@@ -60,6 +61,7 @@ import dev.dimension.flare.ui.presenter.home.UserState
import dev.dimension.flare.ui.presenter.invoke
import dev.dimension.flare.ui.screen.home.NavigationState
import dev.dimension.flare.ui.screen.home.Router
+import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize
import moe.tlaster.precompose.molecule.producePresenter
@@ -75,10 +77,13 @@ internal fun SettingsRoute(
val uriHandler = LocalUriHandler.current
val scaffoldNavigator =
rememberListDetailPaneScaffoldNavigator()
+ val scope = rememberCoroutineScope()
BackHandler(
scaffoldNavigator.canNavigateBack(),
) {
- scaffoldNavigator.navigateBack()
+ scope.launch {
+ scaffoldNavigator.navigateBack()
+ }
}
ListDetailPaneScaffold(
directive = scaffoldNavigator.scaffoldDirective,
@@ -87,42 +92,58 @@ internal fun SettingsRoute(
AnimatedPane {
SettingsScreen(
toAccounts = {
- scaffoldNavigator.navigateTo(ListDetailPaneScaffoldRole.Detail, SettingsDetailDestination.Accounts)
+ scope.launch {
+ scaffoldNavigator.navigateTo(ListDetailPaneScaffoldRole.Detail, SettingsDetailDestination.Accounts)
+ }
},
toAppearance = {
- scaffoldNavigator.navigateTo(ListDetailPaneScaffoldRole.Detail, SettingsDetailDestination.Appearance)
+ scope.launch {
+ scaffoldNavigator.navigateTo(ListDetailPaneScaffoldRole.Detail, SettingsDetailDestination.Appearance)
+ }
},
toStorage = {
- scaffoldNavigator.navigateTo(ListDetailPaneScaffoldRole.Detail, SettingsDetailDestination.Storage)
+ scope.launch {
+ scaffoldNavigator.navigateTo(ListDetailPaneScaffoldRole.Detail, SettingsDetailDestination.Storage)
+ }
},
toAbout = {
- scaffoldNavigator.navigateTo(ListDetailPaneScaffoldRole.Detail, SettingsDetailDestination.About)
+ scope.launch {
+ scaffoldNavigator.navigateTo(ListDetailPaneScaffoldRole.Detail, SettingsDetailDestination.About)
+ }
},
toTabCustomization = {
- scaffoldNavigator.navigateTo(ListDetailPaneScaffoldRole.Detail, SettingsDetailDestination.TabCustomization)
+ scope.launch {
+ scaffoldNavigator.navigateTo(ListDetailPaneScaffoldRole.Detail, SettingsDetailDestination.TabCustomization)
+ }
},
toLocalFilter = {
- scaffoldNavigator.navigateTo(ListDetailPaneScaffoldRole.Detail, SettingsDetailDestination.LocalFilter)
+ scope.launch {
+ scaffoldNavigator.navigateTo(ListDetailPaneScaffoldRole.Detail, SettingsDetailDestination.LocalFilter)
+ }
},
toGuestSettings = {
navigator.navigate(GuestSettingRouteDestination)
},
toLocalHistory = {
- scaffoldNavigator.navigateTo(ListDetailPaneScaffoldRole.Detail, SettingsDetailDestination.LocalHistory)
+ scope.launch {
+ scaffoldNavigator.navigateTo(ListDetailPaneScaffoldRole.Detail, SettingsDetailDestination.LocalHistory)
+ }
},
)
}
},
detailPane = {
AnimatedPane {
- scaffoldNavigator.currentDestination?.content?.let { item ->
+ scaffoldNavigator.currentDestination?.contentKey?.let { item ->
Router(navGraph = NavGraphs.root, item.toDestination()) {
dependency(
ProxyDestinationsNavigator(
scaffoldNavigator,
destinationsNavigator,
navigateBack = {
- scaffoldNavigator.navigateBack()
+ scope.launch {
+ scaffoldNavigator.navigateBack()
+ }
},
uriHandler = uriHandler,
),
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 5a5907d3a..3733f893e 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -47,6 +47,7 @@ firebase-crashlytics = "3.0.2"
materialKolor = "2.0.0"
room = "2.7.0-alpha12"
compose-multiplatform = "1.7.1"
+xr = "1.0.0-alpha01"
[libraries]
bluesky = { module = "moe.tlaster.ozone:bluesky", version.ref = "bluesky" }
@@ -90,6 +91,10 @@ room-paging = { group = "androidx.room", name = "room-paging", version.ref = "ro
room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" }
sqlite-bundled = { group = "androidx.sqlite", name = "sqlite-bundled", version = "2.5.0-alpha12" }
nestedScrollView = { group = "com.github.Tlaster", name = "NestedScrollView", version = "1.0.3" }
+xr-compose = { group = "androidx.xr.compose", name = "compose", version.ref = "xr" }
+xr-runtime = { group = "androidx.xr.runtime", name = "runtime", version.ref = "xr" }
+xr-scenecore = { group = "androidx.xr.scenecore", name = "scenecore", version.ref = "xr" }
+xr-material3 = { group = "androidx.xr.compose.material3", name = "material3", version.ref = "xr" }
media3-exoplayer = { group = "androidx.media3", name = "media3-exoplayer", version.ref = "media3" }
media3-ui = { group = "androidx.media3", name = "media3-ui", version.ref = "media3" }
@@ -185,6 +190,7 @@ compose-destinations = ["compose-destinations"]
media3 = ["media3-exoplayer", "media3-ui", "media3-hls"]
firebase = ["firebase-analytics-ktx", "firebase-crashlytics-ktx"]
ktorfit = ["ktorfit-lib", "ktorfit-converters-response", "ktorfit-converters-flow", "ktorfit-converters-call"]
+xr = ["xr-compose", "xr-runtime", "xr-scenecore", "xr-material3"]
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }