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
8 changes: 4 additions & 4 deletions ios/OpenNOWiOS/OpenNOWiOS.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -341,7 +341,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
"ASSETCATALOG_COMPILER_APPICON_NAME[sdk=appletvos*]" = "";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 72;
CURRENT_PROJECT_VERSION = 78;
DEVELOPMENT_TEAM = VR766AGP7G;
ENABLE_APP_SANDBOX = YES;
ENABLE_HARDENED_RUNTIME = YES;
Expand Down Expand Up @@ -377,7 +377,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
"ASSETCATALOG_COMPILER_APPICON_NAME[sdk=appletvos*]" = "";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 72;
CURRENT_PROJECT_VERSION = 78;
DEVELOPMENT_TEAM = VR766AGP7G;
ENABLE_APP_SANDBOX = YES;
ENABLE_HARDENED_RUNTIME = YES;
Expand Down Expand Up @@ -412,7 +412,7 @@
buildSettings = {
APPLICATION_EXTENSION_API_ONLY = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 69;
CURRENT_PROJECT_VERSION = 76;
DEVELOPMENT_TEAM = VR766AGP7G;
GENERATE_INFOPLIST_FILE = NO;
INFOPLIST_FILE = OpenNOWWidget/Info.plist;
Expand Down Expand Up @@ -441,7 +441,7 @@
buildSettings = {
APPLICATION_EXTENSION_API_ONLY = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 69;
CURRENT_PROJECT_VERSION = 76;
DEVELOPMENT_TEAM = VR766AGP7G;
GENERATE_INFOPLIST_FILE = NO;
INFOPLIST_FILE = OpenNOWWidget/Info.plist;
Expand Down
16 changes: 15 additions & 1 deletion ios/OpenNOWiOS/OpenNOWiOS/BrowseView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ struct BrowseView: View {
@State private var selectedGenre: String? = nil
@State private var selectedPlatform: String?
@State private var selectedStore: String?
@State private var favoritesOnly = false
@State private var sortMode: CatalogSortMode = .title
@State private var pendingLaunchRequest: GameLaunchRequest?
@State private var selectedGameForDetails: CloudGame?
Expand All @@ -27,7 +28,8 @@ struct BrowseView: View {
let matchesGenre = selectedGenre == nil || game.genre == selectedGenre
let matchesPlatform = selectedPlatform == nil || game.platform == selectedPlatform
let matchesStore = selectedStore.map { gameResolvedStores(game: game).contains($0) } ?? true
return matchesGenre && matchesPlatform && matchesStore
let matchesFavorite = !favoritesOnly || store.isFavorite(game)
return matchesGenre && matchesPlatform && matchesStore && matchesFavorite
}

switch sortMode {
Expand Down Expand Up @@ -55,6 +57,7 @@ struct BrowseView: View {
selectedGenre != nil ||
selectedPlatform != nil ||
selectedStore != nil ||
favoritesOnly ||
sortMode != .title
}

Expand Down Expand Up @@ -129,6 +132,16 @@ struct BrowseView: View {
}
.buttonStyle(.bordered)

Button {
Haptics.selection()
favoritesOnly.toggle()
} label: {
Label("Favorites", systemImage: favoritesOnly ? "heart.fill" : "heart")
.lineLimit(1)
}
.buttonStyle(.bordered)
.tint(favoritesOnly ? .red : nil)

if hasActiveFilters {
Button {
Haptics.light()
Expand All @@ -151,6 +164,7 @@ struct BrowseView: View {
selectedGenre = nil
selectedPlatform = nil
selectedStore = nil
favoritesOnly = false
sortMode = .title
}

Expand Down
35 changes: 29 additions & 6 deletions ios/OpenNOWiOS/OpenNOWiOS/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ struct MainTabView: View {
.animation(.easeInOut(duration: 0.28), value: store.showStreamLoading && !store.queueOverlayVisible)
.animation(.easeInOut(duration: 0.2), value: presentedStreamerSession?.id)
.onAppear {
persistQueuePillEdgeIfNeeded()
// MainTabView can be recreated by upstream auth/bootstrap state updates.
// Reattach streamer overlay if store already has an active stream session.
if let activeStream = store.streamSession {
Expand Down Expand Up @@ -182,6 +183,7 @@ struct MainTabView: View {
.padding(.bottom, queuePillEdge == .bottom ? bottomQueuePillPadding(in: proxy) : 0)
.queuePillDrag(
edgeRawValue: $queuePillVerticalEdgeRaw,
defaultEdgeRawValue: queuePillEdge.rawValue,
proxy: proxy,
animation: queueSurfaceAnimation
)
Expand Down Expand Up @@ -214,6 +216,11 @@ struct MainTabView: View {
return 12
#endif
}

private func persistQueuePillEdgeIfNeeded() {
guard QueuePillVerticalEdge(rawValue: queuePillVerticalEdgeRaw) == nil else { return }
queuePillVerticalEdgeRaw = queuePillEdge.rawValue
}
}

private struct QueueStatusPill: View {
Expand Down Expand Up @@ -333,11 +340,21 @@ private struct QueuePillBackgroundModifier: ViewModifier {

private struct QueuePillDragModifier: ViewModifier {
@Binding var edgeRawValue: String
let defaultEdgeRawValue: String
let proxy: GeometryProxy
let animation: Animation
@State private var dragOffset: CGFloat = 0
@State private var latchedDuringDrag = false

private var currentEdgeRawValue: String {
switch edgeRawValue {
case "top", "bottom":
return edgeRawValue
default:
return defaultEdgeRawValue
}
}

func body(content: Content) -> some View {
#if os(iOS)
content
Expand All @@ -347,7 +364,7 @@ private struct QueuePillDragModifier: ViewModifier {
.onChanged { value in
guard !latchedDuringDrag else { return }

let currentEdge = edgeRawValue.isEmpty ? "top" : edgeRawValue
let currentEdge = currentEdgeRawValue
let snapDistance = min(max(proxy.size.height * 0.12, 68), 118)
let translation = value.translation.height
let nextEdge: String?
Expand Down Expand Up @@ -385,7 +402,7 @@ private struct QueuePillDragModifier: ViewModifier {
return
}

let currentEdge = edgeRawValue.isEmpty ? "top" : edgeRawValue
let currentEdge = currentEdgeRawValue
let projectedTranslation = value.translation.height + (value.predictedEndTranslation.height * 0.18)
let snapDistance = min(max(proxy.size.height * 0.12, 68), 118)
let nextEdge: String?
Expand All @@ -397,9 +414,7 @@ private struct QueuePillDragModifier: ViewModifier {
nextEdge = nil
}
withAnimation(animation) {
if let nextEdge {
edgeRawValue = nextEdge
}
edgeRawValue = nextEdge ?? currentEdge
dragOffset = 0
}
}
Expand Down Expand Up @@ -430,10 +445,18 @@ extension View {
@ViewBuilder
func queuePillDrag(
edgeRawValue: Binding<String>,
defaultEdgeRawValue: String,
proxy: GeometryProxy,
animation: Animation
) -> some View {
modifier(QueuePillDragModifier(edgeRawValue: edgeRawValue, proxy: proxy, animation: animation))
modifier(
QueuePillDragModifier(
edgeRawValue: edgeRawValue,
defaultEdgeRawValue: defaultEdgeRawValue,
proxy: proxy,
animation: animation
)
)
}
}

Expand Down
115 changes: 81 additions & 34 deletions ios/OpenNOWiOS/OpenNOWiOS/HomeView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,11 @@ struct HomeView: View {
jumpBackInSection
}

if !store.favoriteGames.isEmpty {
sectionHeader("Favorites")
favoritesSection
}

if !store.featuredGames.isEmpty || store.isLoadingGames {
sectionHeader("Featured")
featuredSection
Expand Down Expand Up @@ -220,6 +225,19 @@ struct HomeView: View {
}
}

private var favoritesSection: some View {
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 14) {
ForEach(store.favoriteGames.prefix(12)) { game in
FeaturedGameCard(game: game) {
selectedGameForDetails = game
}
}
}
.padding(.horizontal)
}
}

private func gameGrid(games: [CloudGame]) -> some View {
let columns = [GridItem(.adaptive(minimum: 150, maximum: 200), spacing: 14)]
return LazyVGrid(columns: columns, spacing: 14) {
Expand Down Expand Up @@ -470,6 +488,7 @@ struct GameCardView: View {
struct GameLaunchDetailsSheet: View {
let game: CloudGame
let onLaunch: (GameLaunchOption?) -> Void
@EnvironmentObject private var store: OpenNOWStore
@Environment(\.dismiss) private var dismiss
@State private var selectedOption: GameLaunchOption?

Expand Down Expand Up @@ -503,6 +522,7 @@ struct GameLaunchDetailsSheet: View {
ScrollView {
VStack(alignment: .leading, spacing: 20) {
detailHero
launchOptionsSection

VStack(alignment: .leading, spacing: 12) {
Text("Overview")
Expand Down Expand Up @@ -553,40 +573,6 @@ struct GameLaunchDetailsSheet: View {
.foregroundStyle(.secondary)
}

if !launcherOptions.isEmpty {
VStack(alignment: .leading, spacing: 12) {
Text("Launch With")
.font(.headline)
ForEach(launcherOptions) { option in
Button {
Haptics.selection()
selectedOption = option
} label: {
HStack(spacing: 12) {
StoreGlyph(store: option.storefront)
.frame(width: 34, height: 34)
VStack(alignment: .leading, spacing: 2) {
Text(option.storefront.capitalized)
.font(.subheadline.weight(.semibold))
Text(launchOptionSubtitle(option))
.font(.caption)
.foregroundStyle(.secondary)
.lineLimit(1)
}
Spacer()
if selectedOption?.id == option.id {
Image(systemName: "checkmark.circle.fill")
.foregroundStyle(brandAccent)
}
}
.padding(14)
.glassCard()
}
.buttonStyle(.plain)
}
}
}

if !detailLabels.isEmpty {
VStack(alignment: .leading, spacing: 12) {
Text("Features")
Expand All @@ -609,6 +595,17 @@ struct GameLaunchDetailsSheet: View {
}
.navigationTitle("Game Details")
.toolbar {
ToolbarItem(placement: .topBarLeading) {
Button {
Haptics.selection()
store.toggleFavorite(game)
} label: {
Image(systemName: store.isFavorite(game) ? "heart.fill" : "heart")
.font(.headline.weight(.semibold))
.foregroundStyle(store.isFavorite(game) ? .red : .primary)
}
.accessibilityLabel(store.isFavorite(game) ? "Remove from favorites" : "Add to favorites")
}
ToolbarItem(placement: .topBarTrailing) {
Button {
Haptics.light()
Expand Down Expand Up @@ -681,6 +678,43 @@ struct GameLaunchDetailsSheet: View {
[GridItem(.adaptive(minimum: 150, maximum: 220), spacing: 12)]
}

@ViewBuilder
private var launchOptionsSection: some View {
if !launcherOptions.isEmpty {
VStack(alignment: .leading, spacing: 12) {
Text("Launch With")
.font(.headline)
ForEach(launcherOptions) { option in
Button {
Haptics.selection()
selectedOption = option
} label: {
HStack(spacing: 12) {
StoreGlyph(store: option.storefront)
.frame(width: 34, height: 34)
VStack(alignment: .leading, spacing: 2) {
Text(option.storefront.capitalized)
.font(.subheadline.weight(.semibold))
Text(launchOptionSubtitle(option))
.font(.caption)
.foregroundStyle(.secondary)
.lineLimit(1)
}
Spacer()
if selectedOption?.id == option.id {
Image(systemName: "checkmark.circle.fill")
.foregroundStyle(brandAccent)
}
}
.padding(14)
.glassCard()
}
.buttonStyle(.plain)
}
}
}
}

private var detailHero: some View {
ZStack(alignment: .bottomLeading) {
GameArtworkView(game: game, iconSize: 44)
Expand Down Expand Up @@ -755,6 +789,7 @@ struct GameLaunchDetailsSheet: View {
}

private struct GameArtworkCard: View {
@EnvironmentObject private var store: OpenNOWStore
let game: CloudGame
let artworkHeight: CGFloat
let titleFont: Font
Expand Down Expand Up @@ -815,6 +850,16 @@ private struct GameArtworkCard: View {
}
.clipShape(RoundedRectangle(cornerRadius: 18, style: .continuous))
}
.overlay(alignment: .topTrailing) {
if store.isFavorite(game) {
Image(systemName: "heart.fill")
.font(.caption.weight(.bold))
.foregroundStyle(.white)
.padding(8)
.background(.red.opacity(0.88), in: Circle())
.padding(10)
}
}
.overlay(
RoundedRectangle(cornerRadius: 18, style: .continuous)
.stroke(Color.white.opacity(0.08), lineWidth: 1)
Expand Down Expand Up @@ -1138,6 +1183,7 @@ func gameResolvedStores(game: CloudGame) -> [String] {

#if !os(tvOS)
private struct UIKitGameDetailsPresenter: UIViewControllerRepresentable {
@EnvironmentObject private var store: OpenNOWStore
@Binding var selectedGame: CloudGame?
let onLaunch: (CloudGame, GameLaunchOption?) -> Void

Expand All @@ -1161,6 +1207,7 @@ private struct UIKitGameDetailsPresenter: UIViewControllerRepresentable {
onLaunch(game, option)
selectedGame = nil
}
.environmentObject(store)
)
hosted.modalPresentationStyle = .pageSheet
if let sheet = hosted.sheetPresentationController {
Expand Down
Loading