Skip to content
Open
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
4 changes: 2 additions & 2 deletions Apps/Stratix/Sources/Stratix/Consoles/ConsoleCardView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ struct ConsoleCardView: View {
Spacer(minLength: 10)
Image(systemName: "chevron.right")
.font(.system(size: 18, weight: .bold))
.foregroundStyle(Color.white.opacity(isFocused ? 0.95 : 0.45))
.foregroundStyle(Color.white.opacity(isFocused ? 0.95 : 0.60))
}
.padding(.horizontal, 16)
.frame(maxWidth: .infinity, minHeight: 58, alignment: .leading)
Expand Down Expand Up @@ -156,7 +156,7 @@ struct ConsoleCardView: View {
Text(text)
.lineLimit(1)
}
.font(.system(size: 13, weight: .bold, design: .rounded))
.font(.system(size: 16, weight: .bold, design: .rounded))
.foregroundStyle(foreground)
.padding(.horizontal, 10)
.padding(.vertical, 6)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,30 @@ extension ConsoleListView {
requestPrimaryFocus()
}

/// Applies a pending shell focus hand-off exactly once per generation.
func consumeFocusHandoffIfNeeded() {
guard let request = focusHandoffRequest,
request.route == .consoles,
request.generation != consumedFocusHandoffGeneration else { return }
consumedFocusHandoffGeneration = request.generation
requestPrimaryFocus()
}

/// Schedules the best available focus target once the console route is ready to accept focus.
/// Leaves focus alone when the user is already on a still-valid target so background
/// refreshes never yank focus out from under the remote.
func requestPrimaryFocus() {
if let focusedTarget {
switch focusedTarget {
case .console(let consoleID) where consoleIDs.contains(consoleID):
return
case .refresh, .troubleshoot:
return
default:
break
}
}

guard let preferredTarget = ConsoleListFocusCoordinator.preferredTarget(
isLoading: consoleController.isLoading,
consoleIDs: consoleIDs,
Expand Down
20 changes: 16 additions & 4 deletions Apps/Stratix/Sources/Stratix/Consoles/ConsoleListView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ struct ConsoleListView: View {
@Environment(ConsoleController.self) var consoleController
@Environment(StreamController.self) var streamController
var onRequestSideRailEntry: () -> Void = {}
var focusHandoffRequest: CloudLibraryFocusState.ContentFocusRequest? = nil
@State var consumedFocusHandoffGeneration: Int?
@State var showingStream = false
@State var selectedConsole: RemoteConsole?
@State var showTroubleshootDetails = false
Expand All @@ -30,9 +32,11 @@ struct ConsoleListView: View {
}

init(
onRequestSideRailEntry: @escaping () -> Void = {}
onRequestSideRailEntry: @escaping () -> Void = {},
focusHandoffRequest: CloudLibraryFocusState.ContentFocusRequest? = nil
) {
self.onRequestSideRailEntry = onRequestSideRailEntry
self.focusHandoffRequest = focusHandoffRequest
}

/// Mounts the console shell and presents the stream surface when a console launch is active.
Expand All @@ -43,9 +47,14 @@ struct ConsoleListView: View {
.accessibilityHidden(shellVisibility.isAccessibilityHidden)
.fullScreenCover(isPresented: $showingStream, onDismiss: handleStreamDismissed) {
if let console = selectedConsole {
StreamControllerInputHost(onOverlayToggle: {
streamController.requestOverlayToggle()
}) {
StreamControllerInputHost(
onOverlayToggle: {
streamController.requestOverlayToggle()
},
onMenuPress: {
streamController.requestOverlayToggle()
}
) {
StreamView(context: .home(console: console))
}
.ignoresSafeArea()
Expand Down Expand Up @@ -109,6 +118,9 @@ struct ConsoleListView: View {
.onChange(of: consoleIDs) { _, consoleIDs in
handleConsoleIDsChange(consoleIDs)
}
.onChange(of: focusHandoffRequest) { _, _ in
consumeFocusHandoffIfNeeded()
}
.onMoveCommand { direction in
NavigationPerformanceTracker.recordRemoteMoveStart(surface: "consoles", direction: direction)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,13 @@ import StratixCore

struct CloudLibraryConsolesView: View {
var onRequestSideRailEntry: () -> Void = {}
var focusHandoffRequest: CloudLibraryFocusState.ContentFocusRequest? = nil

var body: some View {
ConsoleListView(onRequestSideRailEntry: onRequestSideRailEntry)
ConsoleListView(
onRequestSideRailEntry: onRequestSideRailEntry,
focusHandoffRequest: focusHandoffRequest
)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,6 @@ extension CloudLibraryTitleDetailScreen {
}
)
.focused($focusedGalleryIndex, equals: index)
.onMoveCommand { direction in
guard direction == .down else { return }
requestDetailPanelFocus()
}
}
}
.padding(.vertical, 4)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,9 @@ extension CloudLibraryTitleDetailScreen {
if let contextLabel = state.contextLabel, !contextLabel.isEmpty {
HStack(spacing: 8) {
Image(systemName: "location.fill")
.font(.system(size: 12, weight: .bold))
.font(.system(size: 14, weight: .bold))
Text(contextLabel)
.font(.system(size: 14, weight: .bold, design: .rounded))
.font(.system(size: 17, weight: .bold, design: .rounded))
.lineLimit(1)
}
.foregroundStyle(StratixTheme.Colors.textMuted)
Expand All @@ -85,17 +85,17 @@ extension CloudLibraryTitleDetailScreen {

if let subtitle = state.subtitle, !subtitle.isEmpty {
Text(subtitle)
.font(.system(size: 19, weight: .semibold, design: .rounded))
.font(.system(size: 22, weight: .semibold, design: .rounded))
.foregroundStyle(StratixTheme.Colors.textSecondary)
.lineLimit(2)
.frame(maxWidth: textMaxWidth, alignment: .leading)
}

if let descriptionText = state.descriptionText, !descriptionText.isEmpty {
Text(descriptionText)
.font(.system(size: 17, weight: .medium, design: .rounded))
.font(.system(size: 20, weight: .medium, design: .rounded))
.foregroundStyle(StratixTheme.Colors.textSecondary)
.lineLimit(3)
.lineLimit(2)
.frame(maxWidth: textMaxWidth, alignment: .leading)
}

Expand All @@ -107,9 +107,9 @@ extension CloudLibraryTitleDetailScreen {
if let gallerySummaryText {
HStack(spacing: 10) {
Image(systemName: "photo.on.rectangle.angled")
.font(.system(size: 13, weight: .bold))
.font(.system(size: 15, weight: .bold))
Text(gallerySummaryText)
.font(.system(size: 14, weight: .semibold, design: .rounded))
.font(.system(size: 17, weight: .semibold, design: .rounded))
.lineLimit(1)
}
.foregroundStyle(StratixTheme.Colors.textMuted)
Expand All @@ -118,9 +118,9 @@ extension CloudLibraryTitleDetailScreen {
if let achievementSummaryText {
HStack(spacing: 10) {
Image(systemName: "rosette")
.font(.system(size: 13, weight: .bold))
.font(.system(size: 15, weight: .bold))
Text(achievementSummaryText)
.font(.system(size: 14, weight: .semibold, design: .rounded))
.font(.system(size: 17, weight: .semibold, design: .rounded))
.lineLimit(1)
}
.foregroundStyle(StratixTheme.Colors.textMuted)
Expand Down Expand Up @@ -241,7 +241,7 @@ extension CloudLibraryTitleDetailScreen {
GlassCard(cornerRadius: 20, fill: Color.black.opacity(0.34), stroke: Color.white.opacity(0.12), shadowOpacity: 0.15) {
VStack(alignment: .leading, spacing: 12) {
Text("Rating & info")
.font(.system(size: 16, weight: .bold, design: .rounded))
.font(.system(size: 18, weight: .bold, design: .rounded))
.foregroundStyle(StratixTheme.Colors.textPrimary)

if let rating = state.ratingText, !rating.isEmpty {
Expand All @@ -252,7 +252,7 @@ extension CloudLibraryTitleDetailScreen {

if let legal = state.legalText, !legal.isEmpty {
Text(legal)
.font(.system(size: 14, weight: .medium, design: .rounded))
.font(.system(size: 16, weight: .medium, design: .rounded))
.foregroundStyle(StratixTheme.Colors.textSecondary)
.fixedSize(horizontal: false, vertical: true)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,6 @@ extension CloudLibraryTitleDetailScreen {
ForEach(state.detailPanels) { panel in
DetailPanelCardView(panel: panel)
.focused($focusedDetailPanelID, equals: panel.id)
.onMoveCommand { direction in
guard direction == .up else { return }
requestGalleryFocus()
}
}
}
.focusSection()
Expand All @@ -37,11 +33,11 @@ private struct DetailPanelCardView: View {
var body: some View {
VStack(alignment: .leading, spacing: 10) {
Text(panel.title)
.font(.system(size: 18, weight: .bold, design: .rounded))
.font(.system(size: 21, weight: .bold, design: .rounded))
.foregroundStyle(StratixTheme.Colors.textPrimary)

Text(panel.body)
.font(.system(size: 15, weight: .medium, design: .rounded))
.font(.system(size: 18, weight: .medium, design: .rounded))
.foregroundStyle(StratixTheme.Colors.textSecondary)
.fixedSize(horizontal: false, vertical: true)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,10 @@ struct CloudLibraryTitleDetailScreen: View, Equatable {

@Environment(\.dismiss) var dismiss
@State var galleryPresentation: GalleryPresentation?
@State var lastFocusedGalleryIndex: Int?
@State var lastFocusedDetailPanelID: String?
@State var readiness = CloudLibraryTitleDetailReadinessState()
@State var readinessTimeoutTask: Task<Void, Never>?
@FocusState var focusedGalleryIndex: Int?
@FocusState var focusedDetailPanelID: String?
@State var pendingFocusTask: Task<Void, Never>?

let heroHeight = StratixTheme.Detail.heroHeight
let heroPosterWidth = StratixTheme.Detail.heroPosterWidth
Expand All @@ -44,14 +41,13 @@ struct CloudLibraryTitleDetailScreen: View, Equatable {
}

var body: some View {
Group {
if interceptExitCommand {
detailBase
.onExitCommand(perform: goBack)
} else {
detailBase
}
}
detailBase
.onExitCommand(perform: exitCommandAction)
}

private var exitCommandAction: (() -> Void)? {
guard interceptExitCommand else { return nil }
return { goBack() }
}

private var detailBase: some View {
Expand Down Expand Up @@ -81,16 +77,6 @@ struct CloudLibraryTitleDetailScreen: View, Equatable {
readinessTimeoutTask?.cancel()
readinessTimeoutTask = nil
}
.onChange(of: focusedGalleryIndex) { _, newValue in
if let index = newValue {
lastFocusedGalleryIndex = index
}
}
.onChange(of: focusedDetailPanelID) { _, newValue in
if let panelID = newValue {
lastFocusedDetailPanelID = panelID
}
}
}

private var contentScroll: some View {
Expand All @@ -115,40 +101,6 @@ struct CloudLibraryTitleDetailScreen: View, Equatable {
.scrollIndicators(.hidden)
}

func requestGalleryFocus() {
guard !state.gallery.isEmpty else { return }

let targetIndex = state.gallery.indices.contains(lastFocusedGalleryIndex ?? -1)
? lastFocusedGalleryIndex ?? 0
: 0
scheduleFocusTask {
focusedGalleryIndex = targetIndex
}
}

func requestDetailPanelFocus() {
guard !state.detailPanels.isEmpty else { return }

let rememberedID = lastFocusedDetailPanelID
let targetID = state.detailPanels.contains(where: { $0.id == rememberedID })
? rememberedID
: state.detailPanels.first?.id
guard let targetID else { return }

scheduleFocusTask {
focusedDetailPanelID = targetID
}
}

private func scheduleFocusTask(_ updateFocus: @escaping @MainActor () -> Void) {
pendingFocusTask?.cancel()
pendingFocusTask = Task { @MainActor in
await Task.yield()
guard !Task.isCancelled else { return }
updateFocus()
}
}

private func goBack() {
if let onBack {
onBack()
Expand Down
Loading
Loading