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
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ jobs:
- name: Analyze
shell: pwsh
run: fvm flutter analyze --no-fatal-infos
- name: Build Web Client
shell: pwsh
run: fvm flutter build web --no-wasm-dry-run --no-tree-shake-icons
- name: Run Tests
shell: pwsh
run: fvm flutter test
2 changes: 2 additions & 0 deletions lib/const/settings.dart
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,8 @@ class Settings {

static final Uri windowsStoreLink = Uri.parse(
"https://apps.microsoft.com/detail/9PBWHHZRQFW6?hl=en-us&gl=US&ocid=pdpshare");
static final Uri stableWindowsInstallerLink = Uri.parse(
"https://sunkenintime.github.io/icarus/downloads/windows/stable/icarus-setup-latest.exe");
static ThemeData appTheme = ThemeData(
colorScheme: const ColorScheme.dark(
// primary: Color.fromARGB(255, 129, 75, 223),
Expand Down
29 changes: 6 additions & 23 deletions lib/main.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import 'dart:async';
import 'dart:developer' as developer;
import 'dart:io';
import 'dart:ui' show PlatformDispatcher;

import 'package:app_links/app_links.dart';
Expand All @@ -12,9 +11,9 @@ import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:hive_ce_flutter/adapters.dart';
import 'package:icarus/services/deep_link_registrar.dart';
import 'package:icarus/services/desktop_runtime.dart';
import 'package:supabase_flutter/supabase_flutter.dart';

import 'package:windows_single_instance/windows_single_instance.dart';
import 'package:icarus/const/custom_icons.dart';
import 'package:icarus/const/hive_boxes.dart';
import 'package:icarus/const/app_navigator.dart';
Expand Down Expand Up @@ -42,7 +41,6 @@ import 'package:path/path.dart' as path;
import 'package:path_provider/path_provider.dart';
import 'package:shadcn_ui/shadcn_ui.dart';
import 'package:toastification/toastification.dart';
import 'package:window_manager/window_manager.dart';

late CustomMouseCursor staticDrawingCursor;
WebViewEnvironment? webViewEnvironment;
Expand Down Expand Up @@ -114,15 +112,7 @@ Future<void> main(List<String> args) async {
_publishDeepLink(Uri.base, source: 'web_location');
}

if (!kIsWeb && Platform.isWindows) {
await WindowsSingleInstance.ensureSingleInstance(
args,
'icarus_single_instance',
onSecondWindow: (args) {
publishSecondInstanceArgs(args);
},
);
}
await ensureIcarusSingleInstance(args);

if (kIsWeb) {
// On web, Hive uses IndexedDB; no path needed.
Expand Down Expand Up @@ -172,15 +162,9 @@ Future<void> main(List<String> args) async {
// await Hive.box<StrategyData>(HiveBoxNames.strategiesBox).clear();

if (!kIsWeb) {
await windowManager.ensureInitialized();
WindowOptions windowOptions = const WindowOptions(
title:
"Icarus: Valorant Strategies & Line ups ${Settings.versionName}",
await initializeIcarusDesktopWindow(
"Icarus: Valorant Strategies & Line ups ${Settings.versionName}",
);
windowManager.waitUntilReadyToShow(windowOptions, () async {
await windowManager.show();
await windowManager.focus();
});
}
runApp(
UncontrolledProviderScope(
Expand All @@ -201,7 +185,7 @@ Future<void> main(List<String> args) async {
}

Future<void> warmUpWebViewEnvironment() {
if (kIsWeb || !Platform.isWindows) {
if (!isWindowsRuntime) {
isWebViewWarmupComplete = true;
return Future.value();
}
Expand Down Expand Up @@ -281,8 +265,7 @@ Future<void> _initializePersistedDebugLog() async {
}

Future<void> _initWebViewEnvironment() async {
if (kIsWeb) return;
if (Platform.isWindows) {
if (isWindowsRuntime) {
if (isWebViewInitialized && webViewEnvironment != null) {
return;
}
Expand Down
19 changes: 15 additions & 4 deletions lib/providers/auth_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -707,10 +707,6 @@ class AuthProvider extends Notifier<AppAuthState> {
isConvexUserReady: false,
convexAuthStatus: ConvexAuthStatus.signedOut,
);
Settings.showToast(
message: 'Signed out. Your local strategies stay on this device.',
backgroundColor: Settings.tacticalVioletTheme.primary,
);
} catch (error, stackTrace) {
log(
'Sign out failed: $error',
Expand All @@ -724,6 +720,21 @@ class AuthProvider extends Notifier<AppAuthState> {
convexAuthStatus: ConvexAuthStatus.incident,
errorMessage: 'Sign out failed. Please try again.',
);
return;
}

try {
Settings.showToast(
message: 'Signed out. Your local strategies stay on this device.',
backgroundColor: Settings.tacticalVioletTheme.primary,
);
} catch (error, stackTrace) {
log(
'Signed out, but the confirmation toast could not be shown: $error',
name: 'auth',
error: error,
stackTrace: stackTrace,
);
}
}

Expand Down
66 changes: 2 additions & 64 deletions lib/services/deep_link_registrar.dart
Original file line number Diff line number Diff line change
@@ -1,64 +1,2 @@
import 'dart:developer';
import 'dart:io';

import 'package:win32_registry/win32_registry.dart';

Future<void> registerDeepLinkProtocol(String scheme) async {
if (!Platform.isWindows) {
return;
}

final appPath = Platform.resolvedExecutable;
final expectedCommand = '"$appPath" "%1"';

final protocolRegKey = 'Software\\Classes\\$scheme';
const protocolRegValue = RegistryValue.string('URL Protocol', '');
const protocolCmdRegKey = 'shell\\open\\command';

final currentCommand = _readCurrentProtocolCommand(scheme);
final looksLikeDevBuild = _looksLikeDevBuildPath(appPath);
final canOverwriteDevRegistration = !looksLikeDevBuild ||
const bool.fromEnvironment('ICARUS_FORCE_PROTOCOL_REGISTER');

if (!canOverwriteDevRegistration &&
currentCommand != null &&
currentCommand.isNotEmpty &&
currentCommand != expectedCommand) {
log(
'Deep link registration skipped for dev executable. '
'existing="$currentCommand" '
'resolvedExecutable="$appPath"',
name: 'deep_link_registrar',
);
return;
}

final regKey = Registry.currentUser.createKey(protocolRegKey);
regKey.createValue(protocolRegValue);
regKey
.createKey(protocolCmdRegKey)
.createValue(RegistryValue.string('', expectedCommand));

log(
'Deep link registration updated. '
'scheme="$scheme" command="$expectedCommand"',
name: 'deep_link_registrar',
);
}

String? _readCurrentProtocolCommand(String scheme) {
final path = 'Software\\Classes\\$scheme\\shell\\open\\command';
try {
final key = Registry.openPath(RegistryHive.currentUser, path: path);
final value = key.getStringValue('');
key.close();
return value;
} catch (_) {
return null;
}
}

bool _looksLikeDevBuildPath(String appPath) {
final normalized = appPath.toLowerCase().replaceAll('/', '\\');
return normalized.contains('\\build\\windows\\x64\\runner\\debug\\');
}
export 'deep_link_registrar_stub.dart'
if (dart.library.io) 'deep_link_registrar_native.dart';
64 changes: 64 additions & 0 deletions lib/services/deep_link_registrar_native.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import 'dart:developer';
import 'dart:io';

import 'package:win32_registry/win32_registry.dart';

Future<void> registerDeepLinkProtocol(String scheme) async {
if (!Platform.isWindows) {
return;
}

final appPath = Platform.resolvedExecutable;
final expectedCommand = '"$appPath" "%1"';

final protocolRegKey = 'Software\\Classes\\$scheme';
const protocolRegValue = RegistryValue.string('URL Protocol', '');
const protocolCmdRegKey = 'shell\\open\\command';

final currentCommand = _readCurrentProtocolCommand(scheme);
final looksLikeDevBuild = _looksLikeDevBuildPath(appPath);
final canOverwriteDevRegistration = !looksLikeDevBuild ||
const bool.fromEnvironment('ICARUS_FORCE_PROTOCOL_REGISTER');

if (!canOverwriteDevRegistration &&
currentCommand != null &&
currentCommand.isNotEmpty &&
currentCommand != expectedCommand) {
log(
'Deep link registration skipped for dev executable. '
'existing="$currentCommand" '
'resolvedExecutable="$appPath"',
name: 'deep_link_registrar',
);
return;
}

final regKey = Registry.currentUser.createKey(protocolRegKey);
regKey.createValue(protocolRegValue);
regKey
.createKey(protocolCmdRegKey)
.createValue(RegistryValue.string('', expectedCommand));

log(
'Deep link registration updated. '
'scheme="$scheme" command="$expectedCommand"',
name: 'deep_link_registrar',
);
}

String? _readCurrentProtocolCommand(String scheme) {
final path = 'Software\\Classes\\$scheme\\shell\\open\\command';
try {
final key = Registry.openPath(RegistryHive.currentUser, path: path);
final value = key.getStringValue('');
key.close();
return value;
} catch (_) {
return null;
}
}

bool _looksLikeDevBuildPath(String appPath) {
final normalized = appPath.toLowerCase().replaceAll('/', '\\');
return normalized.contains('\\build\\windows\\x64\\runner\\debug\\');
}
1 change: 1 addition & 0 deletions lib/services/deep_link_registrar_stub.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Future<void> registerDeepLinkProtocol(String scheme) async {}
2 changes: 2 additions & 0 deletions lib/services/desktop_runtime.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export 'desktop_runtime_stub.dart'
if (dart.library.io) 'desktop_runtime_native.dart';
28 changes: 28 additions & 0 deletions lib/services/desktop_runtime_native.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import 'dart:io';

import 'package:icarus/const/second_instance_args.dart';
import 'package:window_manager/window_manager.dart';
import 'package:windows_single_instance/windows_single_instance.dart';

bool get isWindowsRuntime => Platform.isWindows;

Future<void> ensureIcarusSingleInstance(List<String> args) async {
if (!Platform.isWindows) {
return;
}

await WindowsSingleInstance.ensureSingleInstance(
args,
'icarus_single_instance',
onSecondWindow: publishSecondInstanceArgs,
);
}

Future<void> initializeIcarusDesktopWindow(String title) async {
await windowManager.ensureInitialized();
final windowOptions = WindowOptions(title: title);
await windowManager.waitUntilReadyToShow(windowOptions, () async {
await windowManager.show();
await windowManager.focus();
});
}
5 changes: 5 additions & 0 deletions lib/services/desktop_runtime_stub.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
bool get isWindowsRuntime => false;

Future<void> ensureIcarusSingleInstance(List<String> args) async {}

Future<void> initializeIcarusDesktopWindow(String title) async {}
10 changes: 5 additions & 5 deletions lib/widgets/demo_dialog.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@ class DemoDialog extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
return ShadDialog.alert(
title: const Text('Demo Version'),
title: const Text('Browser beta'),
description: const SizedBox(
width: 400,
child: Text(
'You are running the web version of this application, which has limited functionality. '
'For the best experience, please install the Windows version from the Microsoft Store — it is free.',
'The browser client supports the cloud library and shared strategies. '
'Some editing and file tools still require the Windows app.',
),
),
actions: [
Expand All @@ -31,9 +31,9 @@ class DemoDialog extends ConsumerWidget {
ShadButton(
leading: const Icon(Icons.download),
onPressed: () async {
await launchUrl(Settings.windowsStoreLink);
await launchUrl(Settings.stableWindowsInstallerLink);
},
child: const Text('Download'),
child: const Text('Download app'),
)
],
);
Expand Down
5 changes: 3 additions & 2 deletions lib/widgets/demo_tag.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import 'package:flutter/material.dart';

import 'package:flutter_riverpod/flutter_riverpod.dart';

import 'package:icarus/const/settings.dart';
import 'package:icarus/widgets/demo_dialog.dart';
import 'package:shadcn_ui/shadcn_ui.dart';

Expand All @@ -11,7 +12,7 @@ class DemoTag extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final theme = Theme.of(context);
const accent = Colors.red;
final accent = Settings.tacticalVioletTheme.primary;

return InkWell(
mouseCursor: SystemMouseCursors.click,
Expand Down Expand Up @@ -48,7 +49,7 @@ class DemoTag extends ConsumerWidget {
),
const SizedBox(width: 6),
Text(
'DEMO',
'WEB BETA',
style: theme.textTheme.labelMedium?.copyWith(
color: Colors.white,
fontWeight: FontWeight.w700,
Expand Down
14 changes: 0 additions & 14 deletions lib/widgets/folder_navigator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ import 'package:icarus/services/windows_desktop_update_controller.dart';
import 'package:icarus/strategy_view.dart';
import 'package:icarus/widgets/current_path_bar.dart';
import 'package:icarus/widgets/desktop_update_dialog.dart';
import 'package:icarus/widgets/demo_dialog.dart';
import 'package:icarus/widgets/demo_tag.dart';
import 'package:icarus/widgets/dialogs/auth/auth_dialog.dart';
import 'package:icarus/widgets/dialogs/confirm_alert_dialog.dart';
Expand Down Expand Up @@ -66,14 +65,11 @@ class _FolderNavigatorState extends ConsumerState<FolderNavigator> {
void initState() {
super.initState();

// Show the demo warning only once after the first frame on web.
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!_warnedOnce) {
_warnedOnce = true;

_warnWebView();

_warnDemo();
}
});
}
Expand All @@ -92,16 +88,6 @@ class _FolderNavigatorState extends ConsumerState<FolderNavigator> {
);
}

void _warnDemo() async {
if (!kIsWeb) return;
await showShadDialog<void>(
context: context,
builder: (context) {
return const DemoDialog();
},
);
}

void _showDesktopOnlyToast() {
Settings.showToast(
message: 'This feature is only supported in the Windows version.',
Expand Down
Loading
Loading