Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 9 additions & 6 deletions app/src/main/java/dev/dimension/flare/ui/route/Router.kt
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ internal fun Router(
onBack: () -> Unit,
openDrawer: () -> Unit,
modifier: Modifier = Modifier,
singlePane: Boolean = false,
) {
val listDetailStrategy = rememberListDetailSceneStrategy<NavKey>()
val isBigScreen = isBigScreen()
Expand Down Expand Up @@ -161,12 +162,14 @@ internal fun Router(
entryProvider = navEntryProvider,
)
val sceneStrategies =
remember(listDetailStrategy) {
listOf(
DialogSceneStrategy(),
BottomSheetSceneStrategy(),
listDetailStrategy,
)
remember(listDetailStrategy, singlePane) {
buildList {
add(DialogSceneStrategy())
add(BottomSheetSceneStrategy())
if (!singlePane) {
add(listDetailStrategy)
}
}
}
val predictiveBackSceneDecorator =
rememberAndroidPredictiveBackSceneDecorator<NavKey>(predictiveBackMotionState)
Expand Down
266 changes: 172 additions & 94 deletions app/src/main/java/dev/dimension/flare/ui/screen/home/HomeScreen.kt

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
package dev.dimension.flare.ui.screen.home

import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.plus
import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.IconButton
import androidx.compose.material3.ListItemDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.NavigationDrawerItem
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.SegmentedListItem
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.unit.dp
import compose.icons.FontAwesomeIcons
import compose.icons.fontawesomeicons.Solid
import compose.icons.fontawesomeicons.solid.CaretDown
import compose.icons.fontawesomeicons.solid.CaretUp
import compose.icons.fontawesomeicons.solid.ClockRotateLeft
import compose.icons.fontawesomeicons.solid.Gear
import compose.icons.fontawesomeicons.solid.MagnifyingGlass
import compose.icons.fontawesomeicons.solid.PenToSquare
import compose.icons.fontawesomeicons.solid.Robot
import compose.icons.fontawesomeicons.solid.SquareRss
import compose.icons.fontawesomeicons.solid.UserPlus
import dev.dimension.flare.R
import dev.dimension.flare.model.AccountType
import dev.dimension.flare.ui.component.AvatarComponent
import dev.dimension.flare.ui.component.FAIcon
import dev.dimension.flare.ui.component.RichText
import dev.dimension.flare.ui.component.TabIcon
import dev.dimension.flare.ui.model.UiState
import dev.dimension.flare.ui.model.asText
import dev.dimension.flare.ui.model.asType
import dev.dimension.flare.ui.model.takeSuccess
import dev.dimension.flare.ui.presenter.home.SecondaryTabsPresenter
import dev.dimension.flare.ui.route.Route
import dev.dimension.flare.ui.theme.segmentedShapes2
import kotlinx.collections.immutable.ImmutableList

private val SidebarHorizontalPadding = 16.dp

@Composable
internal fun HomeSecondarySidebar(
secondaryTabs: UiState<ImmutableList<SecondaryTabsPresenter.Item>>,
isLoggedIn: Boolean?,
aiAgentEnabled: Boolean,
currentRoute: Route,
navigate: (Route) -> Unit,
modifier: Modifier = Modifier,
) {
val accounts = secondaryTabs.takeSuccess().orEmpty()
val searchAccount = accounts.firstOrNull()?.accountType ?: AccountType.Guest
var query by remember { mutableStateOf("") }
val keyboardController = LocalSoftwareKeyboardController.current
val submitSearch = {
navigate(Route.Search(accountType = searchAccount, query = query.trim()))
keyboardController?.hide()
Unit
}
val contentPadding =
WindowInsets.safeDrawing
.only(WindowInsetsSides.End + WindowInsetsSides.Vertical)
.asPaddingValues()
.plus(PaddingValues(vertical = 16.dp))
val sidebarRoutes =
buildList<Triple<Int, ImageVector, Route>> {
add(Triple(R.string.draft_box_title, FontAwesomeIcons.Solid.PenToSquare, Route.DraftBox))
add(Triple(R.string.settings_rss_management_title, FontAwesomeIcons.Solid.SquareRss, Route.Rss.Sources))
add(Triple(R.string.settings_local_history_title, FontAwesomeIcons.Solid.ClockRotateLeft, Route.Settings.LocalHistory))
if (aiAgentEnabled) {
add(Triple(R.string.agent_history_title, FontAwesomeIcons.Solid.Robot, Route.Settings.AgentHistory))
}
add(Triple(R.string.settings_title, FontAwesomeIcons.Solid.Gear, Route.Settings.Main))
}

LazyColumn(
modifier = modifier.fillMaxHeight(),
contentPadding = contentPadding,
verticalArrangement = Arrangement.spacedBy(4.dp),
) {
item {
OutlinedTextField(
value = query,
onValueChange = { query = it },
modifier =
Modifier
.fillMaxWidth()
.padding(horizontal = SidebarHorizontalPadding),
placeholder = { Text(stringResource(R.string.discover_search_placeholder)) },
singleLine = true,
leadingIcon = {
FAIcon(
imageVector = FontAwesomeIcons.Solid.MagnifyingGlass,
contentDescription = null,
)
},
trailingIcon = {
IconButton(onClick = submitSearch) {
FAIcon(
imageVector = FontAwesomeIcons.Solid.MagnifyingGlass,
contentDescription = stringResource(R.string.discover_search_placeholder),
)
}
},
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
keyboardActions = KeyboardActions(onSearch = { submitSearch() }),
)
}

if (isLoggedIn == false) {
item {
SidebarItem(
label = stringResource(R.string.login_button),
selected = currentRoute is Route.ServiceSelect,
icon = FontAwesomeIcons.Solid.UserPlus,
onClick = { navigate(Route.ServiceSelect.Selection) },
)
}
}

accounts.forEach { account ->
val tabs =
account.tabs.mapNotNull { tab ->
getDirection(tab)?.let { route -> tab to route }
}
item(key = "account-${account.accountType}") {
var expanded by remember(account.accountType) { mutableStateOf(false) }
Column(verticalArrangement = Arrangement.spacedBy(ListItemDefaults.SegmentedGap)) {
SegmentedListItem(
checked = expanded,
onCheckedChange = { expanded = it },
shapes = ListItemDefaults.segmentedShapes2(0, 1),
content = {
Column {
account.user.takeSuccess()?.let { user ->
RichText(user.name, maxLines = 1)
Text(
user.handle.canonical,
style = MaterialTheme.typography.bodySmall,
maxLines = 1,
)
}
}
},
leadingContent = {
account.user.takeSuccess()?.let { user ->
AvatarComponent(user.avatar)
}
},
trailingContent = {
FAIcon(
imageVector =
if (expanded) {
FontAwesomeIcons.Solid.CaretUp
} else {
FontAwesomeIcons.Solid.CaretDown
},
contentDescription = null,
)
},
modifier = Modifier.padding(horizontal = SidebarHorizontalPadding),
)
AnimatedVisibility(expanded) {
Column(verticalArrangement = Arrangement.spacedBy(ListItemDefaults.SegmentedGap)) {
tabs.forEachIndexed { tabIndex, (tab, route) ->
SegmentedListItem(
selected = currentRoute == route,
onClick = { navigate(route) },
shapes = ListItemDefaults.segmentedShapes2(tabIndex, tabs.size),
content = {
dev.dimension.flare.ui.component
.Text(tab.title.asText())
},
leadingContent = {
TabIcon(
icon = tab.icon.asType(),
title = tab.title.asText(),
iconOnly = true,
)
},
modifier = Modifier.padding(horizontal = SidebarHorizontalPadding),
contentPadding =
ListItemDefaults.ContentPadding
.plus(PaddingValues(start = 16.dp)),
)
}
}
}
}
}
}

item {
HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp))
}
sidebarRoutes.forEach { (label, icon, route) ->
item(key = route) {
SidebarItem(
label = stringResource(label),
selected = currentRoute == route,
icon = icon,
onClick = { navigate(route) },
)
}
}
}
}

@Composable
private fun SidebarItem(
label: String,
selected: Boolean,
icon: ImageVector,
onClick: () -> Unit,
) {
NavigationDrawerItem(
label = { Text(label) },
selected = selected,
onClick = onClick,
icon = {
FAIcon(imageVector = icon, contentDescription = label)
},
modifier = Modifier.padding(horizontal = SidebarHorizontalPadding),
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import dev.dimension.flare.data.model.BottomBarStyle
import dev.dimension.flare.data.model.PostActionStyle
import dev.dimension.flare.data.model.TimelineDisplayMode
import dev.dimension.flare.data.model.appearance.AppearanceKeys
import dev.dimension.flare.data.model.appearance.LargeScreenLayoutMode
import dev.dimension.flare.ui.component.BackButton
import dev.dimension.flare.ui.component.FlareLargeFlexibleTopAppBar
import dev.dimension.flare.ui.component.FlareScaffold
Expand Down Expand Up @@ -131,25 +132,25 @@ internal fun AppearanceLayoutScreen(
)
},
)
SegmentedListItem(
onClick = {
state.update(AppearanceKeys.DeckMode, !globalAppearance.deckMode)
},
shapes = ListItemDefaults.item(),
content = {
Text(text = stringResource(id = R.string.settings_appearance_deck_mode))
},
supportingContent = {
Text(text = stringResource(id = R.string.settings_appearance_deck_mode_description))
SingleChoiceSettingsItem(
headline = { Text(text = stringResource(id = R.string.settings_appearance_large_screen_layout)) },
supporting = {
Text(text = stringResource(id = R.string.settings_appearance_large_screen_layout_description))
},
trailingContent = {
Switch(
checked = globalAppearance.deckMode,
onCheckedChange = {
state.update(AppearanceKeys.DeckMode, it)
},
)
items =
persistentMapOf(
LargeScreenLayoutMode.Auto to
stringResource(id = R.string.settings_appearance_large_screen_layout_auto),
LargeScreenLayoutMode.Deck to
stringResource(id = R.string.settings_appearance_large_screen_layout_deck),
LargeScreenLayoutMode.SingleColumn to
stringResource(id = R.string.settings_appearance_large_screen_layout_single_column),
),
selected = globalAppearance.largeScreenLayoutMode,
onSelected = {
state.update(AppearanceKeys.LargeScreenLayout, it)
},
shapes = ListItemDefaults.item(),
)
SingleChoiceSettingsItem(
headline = { Text(text = stringResource(id = R.string.settings_appearance_timeline_display_mode)) },
Expand Down
5 changes: 5 additions & 0 deletions app/src/main/res/values-zh-rCN/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,11 @@
<string name="settings_appearance_show_bottom_bar_labels_description">在底部导航和侧边栏显示文字标签</string>
<string name="settings_appearance_deck_mode">分栏模式 (Deck)</string>
<string name="settings_appearance_deck_mode_description">在支持的时间轴上使用分栏布局</string>
<string name="settings_appearance_large_screen_layout">大屏幕布局</string>
<string name="settings_appearance_large_screen_layout_description">选择 Flare 在大屏幕上如何使用空间</string>
<string name="settings_appearance_large_screen_layout_auto">自动</string>
<string name="settings_appearance_large_screen_layout_deck">多栏模式</string>
<string name="settings_appearance_large_screen_layout_single_column">单栏</string>
<string name="settings_appearance_in_app_browser">应用内浏览器</string>
<string name="settings_appearance_in_app_browser_description">在 Flare 内部打开外部链接</string>
<string name="settings_appearance_full_width_post">全宽动态</string>
Expand Down
5 changes: 5 additions & 0 deletions app/src/main/res/values-zh-rTW/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,11 @@
<string name="settings_appearance_show_bottom_bar_labels_description">在底部導覽和側邊導覽中顯示文字標籤</string>
<string name="settings_appearance_deck_mode">多欄模式</string>
<string name="settings_appearance_deck_mode_description">為支援的時間軸使用多欄佈局</string>
<string name="settings_appearance_large_screen_layout">大螢幕佈局</string>
<string name="settings_appearance_large_screen_layout_description">選擇 Flare 在大螢幕上如何使用空間</string>
<string name="settings_appearance_large_screen_layout_auto">自動</string>
<string name="settings_appearance_large_screen_layout_deck">多欄模式</string>
<string name="settings_appearance_large_screen_layout_single_column">單欄</string>
<string name="settings_appearance_in_app_browser">內建瀏覽器</string>
<string name="settings_appearance_in_app_browser_description">在 Flare 內部開啟外部連結</string>
<string name="settings_appearance_full_width_post">全寬貼文</string>
Expand Down
5 changes: 5 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,11 @@
<string name="settings_appearance_show_bottom_bar_labels_description">Show text labels in bottom navigation and the side rail</string>
<string name="settings_appearance_deck_mode">Deck mode</string>
<string name="settings_appearance_deck_mode_description">Use a deck layout for supported timelines</string>
<string name="settings_appearance_large_screen_layout">Large-screen layout</string>
<string name="settings_appearance_large_screen_layout_description">Choose how Flare uses space on larger screens</string>
<string name="settings_appearance_large_screen_layout_auto">Auto</string>
<string name="settings_appearance_large_screen_layout_deck">Deck mode</string>
<string name="settings_appearance_large_screen_layout_single_column">Single column</string>
<string name="settings_appearance_in_app_browser">In-app browser</string>
<string name="settings_appearance_in_app_browser_description">Open external links inside Flare</string>
<string name="settings_appearance_full_width_post">Full-width posts</string>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package dev.dimension.flare.ui.component

import dev.dimension.flare.ui.route.Route
import org.junit.Assert.assertEquals
import org.junit.Test

class TopLevelBackStackTest {
@Test
fun secondaryRouteKeepsItsOwnTopLevelStack() {
val backStack = TopLevelBackStack<Route>(Route.Home)

backStack.addTopLevel(Route.Settings.Main)
backStack.add(Route.Settings.AppearanceLayout)
backStack.addTopLevel(Route.DraftBox)
backStack.addTopLevel(Route.Settings.Main)

assertEquals(Route.Settings.Main, backStack.topLevelKey)
assertEquals(Route.Settings.AppearanceLayout, backStack.currentKey)
}
}
Loading
Loading