Skip to content
Merged
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: 1 addition & 7 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,9 @@
"request": "launch",
"program": "${workspaceFolder}/build/OpenNOW",
"args": [],
"stopAtEntry": false,
"cwd": "${workspaceFolder}",
"environment": [],
"externalConsole": false,
"MIMode": "lldb",
"preLaunchTask": "build",
"logging": {
"engineLogging": false
}

}
]
}
2 changes: 1 addition & 1 deletion .vscode/tasks.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
{
"label": "build",
"type": "shell",
"command": "make -B all",
"command": "make",
"args": [],
"group": {
"kind": "build",
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ WEBRTC_LIBS :=
endif

CXXFLAGS := -std=c++20 -Wall -Wextra -Wpedantic -Wno-deprecated-declarations -Wno-gnu-conditional-omitted-operand -fobjc-arc -Isrc $(WEBRTC_CFLAGS)
LDFLAGS := -framework Cocoa -framework QuartzCore -framework AuthenticationServices -framework AVFoundation -framework AVKit -framework CoreMedia -framework OpenGL -framework GameController -framework ApplicationServices -framework CoreAudio $(WEBRTC_LIBS)
LDFLAGS := -framework Cocoa -framework QuartzCore -framework Metal -framework MetalKit -framework CoreImage -framework AuthenticationServices -framework AVFoundation -framework AVKit -framework CoreMedia -framework OpenGL -framework GameController -framework ApplicationServices -framework CoreAudio $(WEBRTC_LIBS)

.PHONY: all run clean

Expand Down
104 changes: 95 additions & 9 deletions src/OPNAppDelegate.mm
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ @interface AppDelegate ()
@property (nonatomic, strong) OPNSettingsView *settingsView;
@property (nonatomic, strong) OPNStoreView *storeView;
@property (nonatomic, strong) OPNStreamViewController *streamingController;
@property (nonatomic, copy) NSString *currentStreamTitle;
@property (nonatomic, assign) OPN::AuthScreen activeStreamReturnScreen;
@property (nonatomic, assign) BOOL streamLibraryOverlayActive;
@property (nonatomic, strong) NSTimer *gameLibraryRefreshTimer;
@property (nonatomic, assign) std::vector<OPN::GameInfo> cachedGameLibrary;
@property (nonatomic, assign) std::string cachedGameLibraryFingerprint;
Expand All @@ -44,6 +47,8 @@ - (void)saveWindowPresentation;
- (void)startGameLibraryRefreshTimer;
- (void)stopGameLibraryRefreshTimer;
- (void)launchGame:(const OPN::GameInfo &)game variantIndex:(int)variantIndex returnScreen:(OPN::AuthScreen)returnScreen;
- (void)showLibraryOverlayForActiveStream;
- (void)returnToActiveStreamFromLibraryOverlay;
- (void)loadStorePanelsWithRetry:(BOOL)canRetry;
- (void)refreshGameLibraryInBackground;
- (void)fetchGameLibraryWithRetry:(BOOL)canRetry
Expand Down Expand Up @@ -104,11 +109,10 @@ static void OPNConfigureStreamWindow(NSWindow *window) {

static NSString *OPNFormatHours(double hours) {
if (!std::isfinite(hours) || hours < 0) hours = 0;
double rounded = hours >= 10.0 ? std::round(hours) : std::round(hours * 10.0) / 10.0;
if (std::fabs(rounded - std::round(rounded)) < 0.01) {
return [NSString stringWithFormat:@"%.0fh", rounded];
}
return [NSString stringWithFormat:@"%.1fh", rounded];
NSInteger totalMinutes = MAX(0, (NSInteger)llround(hours * 60.0));
NSInteger wholeHours = totalMinutes / 60;
NSInteger minutes = totalMinutes % 60;
return [NSString stringWithFormat:@"%ldh %02ldm", (long)wholeHours, (long)minutes];
}

static NSString *OPNFormatRemainingPlayTime(const OPN::SubscriptionInfo &subscription) {
Expand Down Expand Up @@ -243,6 +247,10 @@ - (void)applicationDidFinishLaunching:(NSNotification *)notification {
selector:@selector(windowFullScreenStateChanged:)
name:NSWindowDidExitFullScreenNotification
object:self.window];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(interfacePreferencesChanged:)
name:OPNInterfacePreferencesDidChangeNotification
object:nil];
{
OPN::AuthCredentials creds = self.pendingCredentials;
creds.stayLoggedIn = AuthService::Shared().GetStayLoggedIn();
Expand Down Expand Up @@ -292,8 +300,8 @@ - (void)applicationWillTerminate:(NSNotification *)notification {
[self.window saveFrameUsingName:OPNMainWindowFrameAutosaveName];
[self saveWindowPresentation];
[self stopGameLibraryRefreshTimer];
// Clear streaming controller reference (block will be released with controller)
if (self.streamingController) {
[self.streamingController shutdownForApplicationTermination];
self.streamingController = nil;
}
}
Expand All @@ -319,9 +327,24 @@ - (void)windowFullScreenStateChanged:(NSNotification *)notification {
[self saveWindowPresentation];
}

- (void)interfacePreferencesChanged:(NSNotification *)notification {
(void)notification;
if (!self.rootView || OpnDerivedAccentColorsEnabled()) return;
self.rootView.controllerAccentRGB = OpnCurrentAccentRGB();
}

- (void)launchGame:(const OPN::GameInfo &)game variantIndex:(int)variantIndex returnScreen:(OPN::AuthScreen)returnScreen {
using namespace OPN;

if (self.streamingController) {
NSLog(@"[AppDelegate] Ignoring game launch while stream is active: title=%s, id=%s", game.title.c_str(), game.id.c_str());
return;
}

self.catalogView = nil;
self.storeView = nil;
self.settingsView = nil;

NSLog(@"[AppDelegate] Game selected: title=%s, id=%s, uuid=%s, variantIndex=%d", game.title.c_str(), game.id.c_str(), game.uuid.c_str(), variantIndex);

std::string apiToken = self.currentSession.idToken.empty()
Expand Down Expand Up @@ -349,7 +372,10 @@ - (void)launchGame:(const OPN::GameInfo &)game variantIndex:(int)variantIndex re
appId:effectiveAppId
apiToken:apiToken
accountLinked:accountLinked
selectedStore:selectedStore];
selectedStore:selectedStore];
self.currentStreamTitle = game.title.empty() ? @"Current Stream" : [NSString stringWithUTF8String:game.title.c_str()];
self.activeStreamReturnScreen = returnScreen;
self.streamLibraryOverlayActive = NO;

__weak __typeof__(self) weakSelf = self;
streamVC.onStreamEnd = ^(BOOL success, const std::string &error) {
Expand All @@ -358,20 +384,30 @@ - (void)launchGame:(const OPN::GameInfo &)game variantIndex:(int)variantIndex re
std::string errorCopy = error;
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"[AppDelegate] Stream ended, restoring previous screen. Success=%d", success);
strongSelf.streamLibraryOverlayActive = NO;
[strongSelf transitionToScreen:returnScreen];
strongSelf.streamingController = nil;
strongSelf.currentStreamTitle = nil;
if (!success && !errorCopy.empty()) {
[strongSelf showError:errorCopy canRetry:YES];
}
});
};
streamVC.onControllerLibraryRequested = ^{
__typeof__(self) strongSelf = weakSelf;
if (!strongSelf) return;
dispatch_async(dispatch_get_main_queue(), ^{
[strongSelf showLibraryOverlayForActiveStream];
});
};

NSRect preservedFrame = self.window.frame;
BOOL preserveFrame = !OPNWindowIsFullScreen(self.window);
[streamVC setInitialViewFrame:self.window.contentView.bounds];
streamVC.view.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
OPNConfigureStreamWindow(self.window);
self.window.contentViewController = streamVC;
OpnDisableFocusHighlights(streamVC.view);
if (preserveFrame) {
[self.window setFrame:preservedFrame display:YES animate:NO];
}
Expand All @@ -387,6 +423,32 @@ - (void)launchGame:(const OPN::GameInfo &)game variantIndex:(int)variantIndex re
NSLog(@"[AppDelegate] Window setup complete");
}

- (void)showLibraryOverlayForActiveStream {
if (!self.streamingController || self.streamLibraryOverlayActive) return;
self.streamLibraryOverlayActive = YES;
[self.streamingController prepareForLibraryPictureInPicture];
[self transitionToScreen:OPN::AuthScreen::Catalog];
}

- (void)returnToActiveStreamFromLibraryOverlay {
if (!self.streamingController) return;
NSRect preservedFrame = self.window.frame;
BOOL preserveFrame = !OPNWindowIsFullScreen(self.window);
NSView *streamView = [self.streamingController streamPictureInPictureView];
[streamView removeFromSuperview];
[self.streamingController setInitialViewFrame:self.window.contentView.bounds];
self.streamingController.view.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
OPNConfigureStreamWindow(self.window);
self.window.contentViewController = self.streamingController;
OpnDisableFocusHighlights(self.streamingController.view);
if (preserveFrame) {
[self.window setFrame:preservedFrame display:YES animate:NO];
}
self.streamLibraryOverlayActive = NO;
[self.streamingController restoreFromLibraryPictureInPicture];
[self.window makeKeyAndOrderFront:nil];
}

#pragma mark - Screen Transitions

- (void)installLibraryRootIfNeeded {
Expand All @@ -397,6 +459,7 @@ - (void)installLibraryRootIfNeeded {
self.window.contentViewController = nil;
self.rootView = [[OPNBackdropView alloc] initWithFrame:self.window.contentView.bounds];
self.rootView.wantsLayer = YES;
self.rootView.layer.opaque = NO;
self.rootView.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
__weak __typeof__(self) weakSelf = self;
self.rootView.onStoreSelected = ^{
Expand Down Expand Up @@ -435,11 +498,14 @@ - (void)installLibraryRootIfNeeded {
[NSApp terminate:strongSelf];
};
self.window.contentView = self.rootView;
OpnDisableFocusHighlights(self.rootView);
}

if (!self.contentContainer || self.contentContainer.superview != self.rootView) {
self.contentContainer = [[NSView alloc] initWithFrame:self.rootView.bounds];
self.contentContainer.wantsLayer = YES;
self.contentContainer.layer.opaque = NO;
self.contentContainer.layer.backgroundColor = NSColor.clearColor.CGColor;
self.contentContainer.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
[self.rootView addSubview:self.contentContainer];
}
Expand Down Expand Up @@ -490,6 +556,7 @@ - (void)transitionToScreen:(OPN::AuthScreen)screen {
};

[self.contentContainer addSubview:view];
OpnDisableFocusHighlights(view);
self.window.title = @"OpenNOW";
break;
}
Expand Down Expand Up @@ -559,6 +626,7 @@ - (void)transitionToScreen:(OPN::AuthScreen)screen {
};

[self.contentContainer addSubview:store];
OpnDisableFocusHighlights(store);
self.window.title = @"OpenNOW - Store";
[self loadStorePanelsWithRetry:YES];
break;
Expand Down Expand Up @@ -604,6 +672,12 @@ - (void)transitionToScreen:(OPN::AuthScreen)screen {
strongSelf.rootView.gameCountText = [NSString stringWithFormat:@"%ld %@", (long)count, count == 1 ? @"game" : @"games"];
};

catalog.onFocusedArtworkAccentChanged = ^(unsigned accentRGB) {
__typeof__(self) strongSelf = weakSelf;
if (!strongSelf || !strongSelf.rootView) return;
strongSelf.rootView.controllerAccentRGB = OpnDerivedAccentColorsEnabled() ? accentRGB : OpnCurrentAccentRGB();
};

catalog.onSelectGame = ^(const GameInfo &game, int variantIndex) {
__typeof__(self) strongSelf = weakSelf;
if (!strongSelf) return;
Expand All @@ -616,7 +690,18 @@ - (void)transitionToScreen:(OPN::AuthScreen)screen {
[strongSelf browseCatalogWithSearch:searchQuery sortId:sortId filterIds:filterIds canRetry:YES];
};

if (self.streamingController && self.streamLibraryOverlayActive) {
[catalog setStreamPictureInPictureView:[self.streamingController streamPictureInPictureView]
title:self.currentStreamTitle ?: @"Current Stream"];
catalog.onStreamPictureInPictureSelected = ^{
__typeof__(self) strongSelf = weakSelf;
if (!strongSelf) return;
[strongSelf returnToActiveStreamFromLibraryOverlay];
};
}

[self.contentContainer addSubview:catalog];
OpnDisableFocusHighlights(catalog);
self.window.title = @"OpenNOW";

// Fetch user info if displayName not already set (OAuth flow)
Expand Down Expand Up @@ -670,6 +755,7 @@ - (void)transitionToScreen:(OPN::AuthScreen)screen {
settings.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
self.settingsView = settings;
[self.contentContainer addSubview:settings];
OpnDisableFocusHighlights(settings);
self.window.title = @"OpenNOW - Settings";
break;
}
Expand Down Expand Up @@ -1174,8 +1260,8 @@ - (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)sender
- (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender {
(void)sender;
if (self.streamingController) {
[self.streamingController requestQuitGameConfirmation];
return NSTerminateCancel;
[self.streamingController shutdownForApplicationTermination];
self.streamingController = nil;
}
return NSTerminateNow;
}
Expand Down
35 changes: 35 additions & 0 deletions src/common/OPNCoreAnimationCoordinator.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
#pragma once

#import <Cocoa/Cocoa.h>
#import <QuartzCore/QuartzCore.h>

@class MTKView;

@interface OPNCoreAnimationCoordinator : NSObject

+ (instancetype)sharedCoordinator;
+ (CAMediaTimingFunction *)appleQuinticTimingFunction;

- (void)animateFocusForCardLayer:(CALayer *)cardLayer
glowLayer:(CALayer *)glowLayer
focused:(BOOL)focused
prominence:(CGFloat)prominence
accentColor:(NSColor *)accentColor;

- (void)animateCardLayer:(CALayer *)cardLayer
metadataContainer:(NSView *)metadataContainer
backgroundLayer:(CALayer *)backgroundLayer
expanded:(BOOL)expanded
accentColor:(NSColor *)accentColor;

- (void)springScrollClipView:(NSClipView *)clipView
toX:(CGFloat)targetX
velocity:(CGFloat)velocity;

- (void)configureMetalViewForProMotion:(MTKView *)metalView;

- (void)extractDominantColorFromImage:(CGImageRef)image
cacheKey:(NSString *)cacheKey
completion:(void (^)(NSColor *color))completion;

@end
Loading
Loading