Compare commits

...

1 Commits

Author SHA1 Message Date
mlsmaycon
555f5233cc add flutter example app test 2026-04-28 03:41:36 +02:00
88 changed files with 18364 additions and 0 deletions

6
client/flutter_ui/.gitignore vendored Normal file
View File

@@ -0,0 +1,6 @@
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
.packages
build/
coverage/

View File

@@ -0,0 +1,36 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: "02085feb3f5d8a8156e5e28512b9d99351d510c0"
channel: "stable"
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: 02085feb3f5d8a8156e5e28512b9d99351d510c0
base_revision: 02085feb3f5d8a8156e5e28512b9d99351d510c0
- platform: linux
create_revision: 02085feb3f5d8a8156e5e28512b9d99351d510c0
base_revision: 02085feb3f5d8a8156e5e28512b9d99351d510c0
- platform: macos
create_revision: 02085feb3f5d8a8156e5e28512b9d99351d510c0
base_revision: 02085feb3f5d8a8156e5e28512b9d99351d510c0
- platform: windows
create_revision: 02085feb3f5d8a8156e5e28512b9d99351d510c0
base_revision: 02085feb3f5d8a8156e5e28512b9d99351d510c0
# User provided section
# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- 'lib/main.dart'
- 'ios/Runner.xcodeproj/project.pbxproj'

View File

@@ -0,0 +1,115 @@
# Flutter UI Migration
## Current Boundary
Keep the daemon as-is and replace only the desktop UI process. The Flutter app
should continue to talk to `DaemonService` from `client/proto/daemon.proto`.
The current UI is not a simple settings window. It owns:
- tray/menu-bar state and nested menu actions
- gRPC connection management and event subscription
- connect, disconnect, login, and session-expired flows
- profile switching, deregistration, and profile windows
- network route and exit-node selection
- advanced settings
- debug bundle creation and upload status dialogs
- enforced update notifications and progress windows
- OS sleep/wake notification to the daemon
- single-instance signaling and quick-actions windows
## Phases
1. Scaffold and generated gRPC client
- Done: generated Dart stubs from `client/proto/daemon.proto`.
- Done: app defaults to a gRPC-backed implementation and keeps
`--fake-daemon` for UI-only work.
- Remaining: replace the development user agent suffix with the release
version at build time.
2. Core connection parity
- Done: status polling and `SubscribeEvents` refresh hooks.
- Done: `connect()` runs `Login` → optional SSO browser handoff via
`openExternalUrl``WaitSSOLogin``Up`, with an `awaitingLogin` snapshot
state and a banner that exposes the verification URI and user code.
- Done: `disconnect()` calls `Down`.
- Match current daemon address defaults:
- Windows: `tcp://127.0.0.1:41731`
- Unix-like desktop: `unix:///var/run/netbird.sock`
3. Settings, profiles, and networks
- Done: `GetConfig`/`SetConfig` for the toggleable settings (auto-connect,
allow SSH, quantum resistance, lazy connections, block inbound,
notifications). Read-only fields (management URL, interface, port, MTU)
still need editable forms.
- Done: profile add/switch/remove/logout via `AddProfile`,
`SwitchProfile`, `RemoveProfile`, `Logout`.
- Done: network list with overlap filtering, per-route
`SelectNetworks`/`DeselectNetworks`, and exit-node single-selection.
4. Desktop integration
- Done: tray icon and menu via `tray_manager` (status header, profile,
Connect/Disconnect, Show window, Quit) with status-aware icons that fall
back to template variants on macOS.
- Done: window lifecycle via `window_manager` — close hides instead of
exiting; tray "Quit" actually destroys the window.
- Done: native notifications via `local_notifier`, fed by the daemon's
`SubscribeEvents` stream and gated by the `notifications` setting (with
CRITICAL severity always firing).
- Done: browser launch and clipboard via `Process.run` and
`flutter/services` Clipboard.
- Remaining: file/folder reveal for debug bundles, single-instance
signaling, quick-actions invocation, and sleep/wake forwarding through
`NotifyOSLifecycle`. Settings/Networks submenus on the tray are deferred
until the window-side flows are stable.
- Note: `local_notifier` uses macOS's deprecated `NSUserNotificationCenter`
(warns at build time). Plan to swap to `flutter_local_notifications`
before release.
5. Debug and update flows
- Done: rich debug bundle screen with anonymize, system-info, upload (URL),
and run-with-trace + duration. State machine drives `GetLogLevel`
`SetLogLevel(TRACE)``Down``SetSyncResponsePersistence``Up`
progress over duration → `StopCPUProfile``DebugBundle`, with restore
of original log level and persistence in a finally. Result dialog covers
uploaded, upload-failed, and local-only outcomes with copy/open actions.
- Done: enforced-update modal triggered by daemon `progress_window=show`
metadata. Polls `GetInstallerResult` with a 15-min timeout, blocks close
for 10 s, then surfaces success (auto-close) or failure (error message).
- Remaining: hook a "Check for updates" / "Install now" button into the
About surface that calls `TriggerUpdate` directly.
6. Release pipeline
- Update `.github/workflows/release.yml` UI build steps.
- Update `client/netbird.wxs`, `release_files/install.sh`, and
`release_files/ui-post-install.sh` where they assume the Go UI artifact.
- Update updater restart behavior in `client/internal/updater/installer`.
- Preserve public artifact names until installers and updater logic are
intentionally migrated.
## RPCs Used By The Current UI
The first production implementation should cover:
- `Status`, `Up`, `Down`
- `Login`, `WaitSSOLogin`, `Logout`
- `GetConfig`, `SetConfig`, `GetFeatures`
- `SubscribeEvents`
- `ListNetworks`, `SelectNetworks`, `DeselectNetworks`
- `ListProfiles`, `AddProfile`, `SwitchProfile`, `RemoveProfile`,
`GetActiveProfile`
- `DebugBundle`, `GetLogLevel`, `SetLogLevel`, `SetSyncResponsePersistence`,
`StartCPUProfile`, `StopCPUProfile`
- `TriggerUpdate`, `GetInstallerResult`
- `NotifyOSLifecycle`
## Risk Register
- Desktop tray support differs sharply across Windows, macOS, and Linux.
- Linux app indicators and desktop-session startup need distro-level testing.
- The updater currently restarts `netbird-ui` by process/app name on Windows and
macOS, so artifact naming changes must be coordinated.
- Dart gRPC over Unix domain sockets must be validated against the daemon's
existing `unix://` address behavior.
- Flutter desktop packaging is separate from Go builds, so release CI needs a
new toolchain and cache strategy.

View File

@@ -0,0 +1,54 @@
# NetBird Flutter UI
This is the migration workspace for a Flutter-based replacement for `client/ui`.
The existing Go/Fyne UI remains the production UI until this package reaches
feature and release-pipeline parity.
## Scope
The first target is the desktop UI only. The NetBird daemon, service lifecycle,
network engine, and daemon gRPC API stay in Go.
Initial parity target:
- tray/menu-bar entry with connection status and connect/disconnect actions
- settings and feature flags backed by `DaemonService.GetConfig` and `SetConfig`
- profile management
- network and exit-node selection
- daemon event subscription and desktop notifications
- login/session-expired flow
- debug bundle flow
- enforced-update progress window
- Windows, macOS, and Linux packaging integration
## Bootstrap
Flutter and Dart are not committed into this repository. After installing the
Flutter SDK, run:
```sh
cd client/flutter_ui
bash tool/bootstrap.sh
bash tool/generate_proto.sh
flutter run -d macos -- --daemon-addr=unix:///var/run/netbird.sock
```
Use `-d windows` or `-d linux` on those platforms. The Windows daemon address is
currently `tcp://127.0.0.1:41731`.
For UI-only development without a daemon, run:
```sh
flutter run -d macos -- --fake-daemon
```
## Layout
- `lib/main.dart`: app entry point and command-line flag parsing
- `lib/src/app_shell.dart`: first-pass desktop shell
- `lib/src/daemon_client.dart`: daemon boundary with fake and gRPC-backed clients
- `lib/src/models.dart`: UI-facing models independent from generated protobufs
- `lib/src/generated/`: generated Dart protobuf and gRPC files
- `tool/bootstrap.sh`: creates Flutter desktop platform folders once Flutter is installed
- `tool/generate_proto.sh`: generates Dart gRPC bindings from `client/proto/daemon.proto`
- `MIGRATION.md`: parity plan and release integration checklist

View File

@@ -0,0 +1,10 @@
include: package:lints/recommended.yaml
analyzer:
exclude:
- lib/src/generated/**
linter:
rules:
avoid_print: true

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

View File

@@ -0,0 +1,53 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:window_manager/window_manager.dart';
import 'src/app_shell.dart';
import 'src/daemon_client.dart';
import 'src/desktop_integration.dart';
Future<void> main(List<String> args) async {
WidgetsFlutterBinding.ensureInitialized();
final daemonAddr = _readFlag(args, 'daemon-addr') ?? _defaultDaemonAddr();
final fakeDaemon = args.contains('--fake-daemon');
await windowManager.ensureInitialized();
const windowOptions = WindowOptions(
size: Size(900, 640),
minimumSize: Size(720, 520),
center: true,
title: 'NetBird',
);
await windowManager.waitUntilReadyToShow(windowOptions, () async {
await windowManager.show();
await windowManager.focus();
});
final client = fakeDaemon
? FakeDaemonClient(daemonAddr: daemonAddr)
: GrpcDaemonClient(daemonAddr: daemonAddr);
final integration = DesktopIntegration(client: client);
await integration.initialize();
runApp(NetBirdFlutterApp(client: client, integration: integration));
}
String? _readFlag(List<String> args, String name) {
final prefix = '--$name=';
for (final arg in args) {
if (arg.startsWith(prefix)) {
return arg.substring(prefix.length);
}
}
return null;
}
String _defaultDaemonAddr() {
if (Platform.isWindows) {
return 'tcp://127.0.0.1:41731';
}
return 'unix:///var/run/netbird.sock';
}

View File

@@ -0,0 +1,889 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'daemon_client.dart';
import 'debug_screen.dart';
import 'desktop_integration.dart';
import 'models.dart';
import 'platform.dart';
import 'update_progress.dart';
class NetBirdFlutterApp extends StatelessWidget {
const NetBirdFlutterApp({required this.client, this.integration, super.key});
final DaemonClient client;
final DesktopIntegration? integration;
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'NetBird',
debugShowCheckedModeBanner: false,
theme: ThemeData(
useMaterial3: true,
colorSchemeSeed: const Color(0xFF008C95),
brightness: Brightness.light,
),
darkTheme: ThemeData(
useMaterial3: true,
colorSchemeSeed: const Color(0xFF008C95),
brightness: Brightness.dark,
),
home: AppShell(client: client, integration: integration),
);
}
}
class AppShell extends StatefulWidget {
const AppShell({required this.client, this.integration, super.key});
final DaemonClient client;
final DesktopIntegration? integration;
@override
State<AppShell> createState() => _AppShellState();
}
class _AppShellState extends State<AppShell> {
late ClientSnapshot _snapshot;
StreamSubscription<ClientSnapshot>? _subscription;
StreamSubscription<UpdateProgressEvent>? _updateSubscription;
StreamSubscription<int>? _tabSubscription;
int _selectedIndex = 0;
bool _busy = false;
bool _updateDialogOpen = false;
@override
void initState() {
super.initState();
_snapshot = ClientSnapshot.initial(widget.client.daemonAddr);
_subscription = widget.client.watchSnapshot().listen((snapshot) {
if (!mounted) {
return;
}
setState(() => _snapshot = snapshot);
});
_updateSubscription = widget.client.watchUpdateRequests().listen(
_showUpdateDialog,
);
_tabSubscription = widget.integration?.tabRequests.listen((index) {
if (!mounted) {
return;
}
setState(() => _selectedIndex = index);
});
}
@override
void dispose() {
_subscription?.cancel();
_updateSubscription?.cancel();
_tabSubscription?.cancel();
widget.client.dispose();
super.dispose();
}
Future<void> _showUpdateDialog(UpdateProgressEvent event) async {
if (!mounted || _updateDialogOpen) {
return;
}
_updateDialogOpen = true;
try {
await showUpdateProgressDialog(
context: context,
client: widget.client,
event: event,
);
} finally {
if (mounted) {
_updateDialogOpen = false;
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Row(
children: [
NavigationRail(
selectedIndex: _selectedIndex,
onDestinationSelected: (index) {
setState(() => _selectedIndex = index);
},
labelType: NavigationRailLabelType.all,
leading: Padding(
padding: const EdgeInsets.symmetric(vertical: 16),
child: _StatusGlyph(status: _snapshot.status),
),
destinations: const [
NavigationRailDestination(
icon: Icon(Icons.hub_outlined),
selectedIcon: Icon(Icons.hub),
label: Text('Status'),
),
NavigationRailDestination(
icon: Icon(Icons.route_outlined),
selectedIcon: Icon(Icons.route),
label: Text('Networks'),
),
NavigationRailDestination(
icon: Icon(Icons.account_circle_outlined),
selectedIcon: Icon(Icons.account_circle),
label: Text('Profiles'),
),
NavigationRailDestination(
icon: Icon(Icons.tune_outlined),
selectedIcon: Icon(Icons.tune),
label: Text('Settings'),
),
NavigationRailDestination(
icon: Icon(Icons.bug_report_outlined),
selectedIcon: Icon(Icons.bug_report),
label: Text('Debug'),
),
],
),
const VerticalDivider(width: 1),
Expanded(child: SafeArea(child: _buildPage(context))),
],
),
);
}
Widget _buildPage(BuildContext context) {
return switch (_selectedIndex) {
0 => _StatusPane(
snapshot: _snapshot,
busy: _busy,
onConnect: () => _run(widget.client.connect),
onDisconnect: () => _run(widget.client.disconnect),
),
1 => _NetworksPane(snapshot: _snapshot, client: widget.client),
2 => _ProfilesPane(snapshot: _snapshot, client: widget.client),
3 => _SettingsPane(snapshot: _snapshot, client: widget.client),
_ => DebugScreen(client: widget.client),
};
}
Future<void> _run(Future<void> Function() action) async {
if (_busy) {
return;
}
setState(() => _busy = true);
try {
await action();
} finally {
if (mounted) {
setState(() => _busy = false);
}
}
}
}
class _Page extends StatelessWidget {
const _Page({required this.title, required this.child, this.actions});
final String title;
final Widget child;
final List<Widget>? actions;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Text(
title,
style: Theme.of(context).textTheme.headlineSmall,
),
),
if (actions != null) ...actions!,
],
),
const SizedBox(height: 20),
Expanded(child: child),
],
),
);
}
}
class _StatusPane extends StatelessWidget {
const _StatusPane({
required this.snapshot,
required this.busy,
required this.onConnect,
required this.onDisconnect,
});
final ClientSnapshot snapshot;
final bool busy;
final VoidCallback onConnect;
final VoidCallback onDisconnect;
@override
Widget build(BuildContext context) {
final connected = snapshot.status == ConnectionStatus.connected;
final connecting =
snapshot.status == ConnectionStatus.connecting ||
snapshot.status == ConnectionStatus.awaitingLogin;
return _Page(
title: 'Status',
child: ListView(
children: [
_InfoRow(label: 'Connection', value: snapshot.status.label),
_InfoRow(label: 'Daemon', value: snapshot.daemonAddr),
_InfoRow(label: 'Daemon version', value: snapshot.daemonVersion),
if (snapshot.pendingLogin != null) ...[
const SizedBox(height: 16),
_LoginBanner(pending: snapshot.pendingLogin!),
],
if (snapshot.errorMessage != null) ...[
const SizedBox(height: 16),
_ErrorBanner(message: snapshot.errorMessage!),
],
const SizedBox(height: 24),
Wrap(
spacing: 12,
runSpacing: 12,
children: [
FilledButton.icon(
onPressed: busy || connected || connecting ? null : onConnect,
icon: const Icon(Icons.power_settings_new),
label: const Text('Connect'),
),
OutlinedButton.icon(
onPressed: busy || !connected ? null : onDisconnect,
icon: const Icon(Icons.power_off),
label: const Text('Disconnect'),
),
],
),
const SizedBox(height: 32),
_SectionLabel('Active profile'),
_ProfileTile(profile: snapshot.activeProfile),
],
),
);
}
}
class _NetworksPane extends StatefulWidget {
const _NetworksPane({required this.snapshot, required this.client});
final ClientSnapshot snapshot;
final DaemonClient client;
@override
State<_NetworksPane> createState() => _NetworksPaneState();
}
class _NetworksPaneState extends State<_NetworksPane> {
NetworkFilter _filter = NetworkFilter.all;
final Set<String> _busyRoutes = {};
@override
Widget build(BuildContext context) {
final networks = widget.snapshot.networks
.where(_filter.matches)
.toList();
return _Page(
title: 'Networks',
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SegmentedButton<NetworkFilter>(
segments: const [
ButtonSegment(
value: NetworkFilter.all,
icon: Icon(Icons.all_inclusive),
label: Text('All'),
),
ButtonSegment(
value: NetworkFilter.overlapping,
icon: Icon(Icons.compare_arrows),
label: Text('Overlapping'),
),
ButtonSegment(
value: NetworkFilter.exitNode,
icon: Icon(Icons.public),
label: Text('Exit nodes'),
),
],
selected: {_filter},
onSelectionChanged: (selected) {
setState(() => _filter = selected.single);
},
),
const SizedBox(height: 16),
if (networks.isEmpty)
const Padding(
padding: EdgeInsets.symmetric(vertical: 24),
child: Text('No networks to show.'),
)
else
Expanded(
child: ListView.separated(
itemCount: networks.length,
separatorBuilder: (_, _) => const Divider(height: 1),
itemBuilder: (context, index) {
final route = networks[index];
final exitNodeMode = _filter == NetworkFilter.exitNode;
return _NetworkTile(
route: route,
exitNodeMode: exitNodeMode,
busy: _busyRoutes.contains(route.id),
onChanged: (selected) =>
_toggle(route, selected, exitNodeMode),
);
},
),
),
],
),
);
}
Future<void> _toggle(
NetworkRoute route,
bool selected,
bool exitNodeMode,
) async {
if (_busyRoutes.contains(route.id)) {
return;
}
setState(() => _busyRoutes.add(route.id));
try {
if (exitNodeMode) {
await widget.client.setExitNode(selected ? route.id : null);
} else {
await widget.client.setNetworkSelection(route.id, selected);
}
} finally {
if (mounted) {
setState(() => _busyRoutes.remove(route.id));
}
}
}
}
class _ProfilesPane extends StatefulWidget {
const _ProfilesPane({required this.snapshot, required this.client});
final ClientSnapshot snapshot;
final DaemonClient client;
@override
State<_ProfilesPane> createState() => _ProfilesPaneState();
}
class _ProfilesPaneState extends State<_ProfilesPane> {
bool _busy = false;
@override
Widget build(BuildContext context) {
return _Page(
title: 'Profiles',
actions: [
FilledButton.tonalIcon(
onPressed: _busy ? null : _showAddDialog,
icon: const Icon(Icons.add),
label: const Text('Add profile'),
),
],
child: ListView.separated(
itemCount: widget.snapshot.profiles.length,
separatorBuilder: (_, _) => const Divider(height: 1),
itemBuilder: (context, index) {
final profile = widget.snapshot.profiles[index];
return _ProfileTile(
profile: profile,
onTap: profile.active || _busy ? null : () => _confirmSwitch(profile),
trailing: _profileMenu(profile),
);
},
),
);
}
Widget _profileMenu(ProfileInfo profile) {
return PopupMenuButton<_ProfileAction>(
enabled: !_busy,
onSelected: (action) => _handleAction(action, profile),
itemBuilder: (context) => [
if (profile.active)
const PopupMenuItem(
value: _ProfileAction.logout,
child: ListTile(
leading: Icon(Icons.logout),
title: Text('Logout'),
contentPadding: EdgeInsets.zero,
),
),
PopupMenuItem(
value: _ProfileAction.remove,
enabled: !profile.active,
child: const ListTile(
leading: Icon(Icons.delete_outline),
title: Text('Remove'),
contentPadding: EdgeInsets.zero,
),
),
],
);
}
Future<void> _handleAction(
_ProfileAction action,
ProfileInfo profile,
) async {
switch (action) {
case _ProfileAction.logout:
await _confirmAndRun(
title: 'Logout from ${profile.name}?',
message:
'This disconnects the active profile and clears its session.',
run: widget.client.logoutActive,
);
case _ProfileAction.remove:
await _confirmAndRun(
title: 'Remove profile ${profile.name}?',
message: 'This deletes the profile from this device.',
run: () => widget.client.removeProfile(profile.name),
);
}
}
Future<void> _confirmSwitch(ProfileInfo profile) async {
await _confirmAndRun(
title: 'Switch to ${profile.name}?',
message: 'The connection will restart with the new profile.',
run: () => widget.client.switchProfile(profile.name),
);
}
Future<void> _showAddDialog() async {
final controller = TextEditingController();
final name = await showDialog<String>(
context: context,
builder: (context) {
return AlertDialog(
title: const Text('Add profile'),
content: TextField(
controller: controller,
autofocus: true,
decoration: const InputDecoration(labelText: 'Profile name'),
onSubmitted: (value) => Navigator.of(context).pop(value.trim()),
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Cancel'),
),
FilledButton(
onPressed: () =>
Navigator.of(context).pop(controller.text.trim()),
child: const Text('Add'),
),
],
);
},
);
if (name == null || name.isEmpty) {
return;
}
await _runBusy(() => widget.client.addProfile(name));
}
Future<void> _confirmAndRun({
required String title,
required String message,
required Future<void> Function() run,
}) async {
final confirm = await showDialog<bool>(
context: context,
builder: (context) {
return AlertDialog(
title: Text(title),
content: Text(message),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: const Text('Cancel'),
),
FilledButton(
onPressed: () => Navigator.of(context).pop(true),
child: const Text('Confirm'),
),
],
);
},
);
if (confirm != true) {
return;
}
await _runBusy(run);
}
Future<void> _runBusy(Future<void> Function() action) async {
if (_busy) {
return;
}
setState(() => _busy = true);
try {
await action();
} finally {
if (mounted) {
setState(() => _busy = false);
}
}
}
}
enum _ProfileAction { logout, remove }
class _SettingsPane extends StatefulWidget {
const _SettingsPane({required this.snapshot, required this.client});
final ClientSnapshot snapshot;
final DaemonClient client;
@override
State<_SettingsPane> createState() => _SettingsPaneState();
}
class _SettingsPaneState extends State<_SettingsPane> {
bool _writing = false;
@override
Widget build(BuildContext context) {
final settings = widget.snapshot.settings;
final disabled = _writing;
return _Page(
title: 'Settings',
child: ListView(
children: [
_InfoRow(label: 'Management URL', value: settings.managementUrl),
_InfoRow(label: 'Interface', value: settings.interfaceName),
_InfoRow(label: 'WireGuard port', value: '${settings.wireguardPort}'),
_InfoRow(label: 'MTU', value: '${settings.mtu}'),
const SizedBox(height: 16),
SwitchListTile(
value: settings.autoConnect,
onChanged: disabled
? null
: (value) =>
_apply(settings.copyWith(autoConnect: value)),
title: const Text('Connect on startup'),
),
SwitchListTile(
value: settings.allowSsh,
onChanged: disabled
? null
: (value) => _apply(settings.copyWith(allowSsh: value)),
title: const Text('Allow SSH'),
),
SwitchListTile(
value: settings.quantumResistance,
onChanged: disabled
? null
: (value) =>
_apply(settings.copyWith(quantumResistance: value)),
title: const Text('Quantum resistance'),
),
SwitchListTile(
value: settings.lazyConnection,
onChanged: disabled
? null
: (value) =>
_apply(settings.copyWith(lazyConnection: value)),
title: const Text('Lazy connections'),
),
SwitchListTile(
value: settings.blockInbound,
onChanged: disabled
? null
: (value) =>
_apply(settings.copyWith(blockInbound: value)),
title: const Text('Block inbound'),
),
SwitchListTile(
value: settings.notifications,
onChanged: disabled
? null
: (value) =>
_apply(settings.copyWith(notifications: value)),
title: const Text('Notifications'),
),
],
),
);
}
Future<void> _apply(ClientSettings updated) async {
setState(() => _writing = true);
try {
await widget.client.updateSettings(updated);
} finally {
if (mounted) {
setState(() => _writing = false);
}
}
}
}
class _StatusGlyph extends StatelessWidget {
const _StatusGlyph({required this.status});
final ConnectionStatus status;
@override
Widget build(BuildContext context) {
final color = switch (status) {
ConnectionStatus.connected => Colors.green,
ConnectionStatus.connecting => Colors.amber,
ConnectionStatus.awaitingLogin => Colors.lightBlue,
ConnectionStatus.error => Colors.red,
ConnectionStatus.disconnected => Colors.grey,
};
return Tooltip(
message: status.label,
child: Icon(Icons.circle, color: color, size: 18),
);
}
}
class _InfoRow extends StatelessWidget {
const _InfoRow({required this.label, required this.value});
final String label;
final String value;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 160,
child: Text(label, style: Theme.of(context).textTheme.labelLarge),
),
Expanded(child: Text(value)),
],
),
);
}
}
class _SectionLabel extends StatelessWidget {
const _SectionLabel(this.text);
final String text;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Text(text, style: Theme.of(context).textTheme.titleMedium),
);
}
}
class _ErrorBanner extends StatelessWidget {
const _ErrorBanner({required this.message});
final String message;
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return DecoratedBox(
decoration: BoxDecoration(
color: colors.errorContainer,
borderRadius: BorderRadius.circular(8),
),
child: Padding(
padding: const EdgeInsets.all(12),
child: Row(
children: [
Icon(Icons.error_outline, color: colors.onErrorContainer),
const SizedBox(width: 12),
Expanded(
child: Text(
message,
style: TextStyle(color: colors.onErrorContainer),
),
),
],
),
),
);
}
}
class _LoginBanner extends StatelessWidget {
const _LoginBanner({required this.pending});
final PendingLogin pending;
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return DecoratedBox(
decoration: BoxDecoration(
color: colors.tertiaryContainer,
borderRadius: BorderRadius.circular(8),
),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Sign in to continue',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: colors.onTertiaryContainer,
),
),
const SizedBox(height: 8),
Text(
'A browser window opened to complete sign-in. '
'If it did not, open the URL below.',
style: TextStyle(color: colors.onTertiaryContainer),
),
const SizedBox(height: 12),
SelectableText(
pending.verificationUri,
style: TextStyle(color: colors.onTertiaryContainer),
),
const SizedBox(height: 4),
Text(
'Code: ${pending.userCode}',
style: TextStyle(color: colors.onTertiaryContainer),
),
const SizedBox(height: 12),
Wrap(
spacing: 8,
children: [
FilledButton.tonalIcon(
onPressed: () => _openUrl(pending.verificationUri),
icon: const Icon(Icons.open_in_new),
label: const Text('Open in browser'),
),
OutlinedButton.icon(
onPressed: () => _copy(context, pending.verificationUri),
icon: const Icon(Icons.copy),
label: const Text('Copy URL'),
),
],
),
],
),
),
);
}
Future<void> _openUrl(String url) async {
await openExternalUrl(url);
}
Future<void> _copy(BuildContext context, String url) async {
await Clipboard.setData(ClipboardData(text: url));
if (!context.mounted) {
return;
}
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('URL copied')),
);
}
}
class _NetworkTile extends StatelessWidget {
const _NetworkTile({
required this.route,
required this.exitNodeMode,
required this.busy,
required this.onChanged,
});
final NetworkRoute route;
final bool exitNodeMode;
final bool busy;
final ValueChanged<bool> onChanged;
@override
Widget build(BuildContext context) {
final subtitle = [
route.range,
if (route.domains.isNotEmpty) route.domains.join(', '),
].join(' ');
Widget leading;
if (busy) {
leading = const SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(strokeWidth: 2),
);
} else if (exitNodeMode) {
leading = IconButton(
icon: Icon(
route.selected
? Icons.radio_button_checked
: Icons.radio_button_unchecked,
),
onPressed: () => onChanged(!route.selected),
);
} else {
leading = Checkbox(
value: route.selected,
onChanged: (value) => onChanged(value ?? false),
);
}
return ListTile(
contentPadding: EdgeInsets.zero,
leading: leading,
title: Text(route.id),
subtitle: Text(subtitle),
trailing: route.isExitNode ? const Icon(Icons.public) : null,
onTap: busy ? null : () => onChanged(!route.selected),
);
}
}
class _ProfileTile extends StatelessWidget {
const _ProfileTile({required this.profile, this.onTap, this.trailing});
final ProfileInfo profile;
final VoidCallback? onTap;
final Widget? trailing;
@override
Widget build(BuildContext context) {
return ListTile(
contentPadding: EdgeInsets.zero,
leading: Icon(
profile.active ? Icons.check_circle : Icons.circle_outlined,
),
title: Text(profile.name),
subtitle: profile.email == null ? null : Text(profile.email!),
onTap: onTap,
trailing: trailing,
);
}
}

View File

@@ -0,0 +1,916 @@
import 'dart:async';
import 'dart:io';
import 'package:grpc/grpc.dart';
import 'generated/daemon.pbgrpc.dart' as daemon;
import 'models.dart';
import 'platform.dart';
const _userAgent = 'netbird-desktop-ui/development';
abstract class DaemonClient {
String get daemonAddr;
Stream<ClientSnapshot> watchSnapshot();
Stream<SystemNotification> watchEvents();
Stream<UpdateProgressEvent> watchUpdateRequests();
Future<void> connect();
Future<void> disconnect();
Future<void> bringUp();
Future<void> bringDown();
Future<DebugBundleResult> debugBundle({
required bool anonymize,
required bool systemInfo,
String? uploadUrl,
});
Future<DaemonLogLevel> getLogLevel();
Future<void> setLogLevel(DaemonLogLevel level);
Future<void> setSyncResponsePersistence(bool enabled);
Future<void> startCpuProfile();
Future<void> stopCpuProfile();
Future<TriggerUpdateResult> triggerUpdate();
Future<InstallerResult> getInstallerResult();
Future<void> updateSettings(ClientSettings updated);
Future<void> setNetworkSelection(String routeId, bool selected);
Future<void> setExitNode(String? routeId);
Future<void> switchProfile(String name);
Future<void> addProfile(String name);
Future<void> removeProfile(String name);
Future<void> logoutActive();
void dispose();
}
class GrpcDaemonClient implements DaemonClient {
GrpcDaemonClient({required this.daemonAddr}) {
_snapshot = ClientSnapshot.initial(daemonAddr);
_channel = _createChannel(daemonAddr);
_client = daemon.DaemonServiceClient(_channel);
}
@override
final String daemonAddr;
final _snapshots = StreamController<ClientSnapshot>.broadcast();
final _events = StreamController<SystemNotification>.broadcast();
final _updateRequests = StreamController<UpdateProgressEvent>.broadcast();
final _refreshInterval = const Duration(seconds: 2);
final _callTimeout = const Duration(seconds: 5);
final _ssoLoginTimeout = const Duration(minutes: 5);
final _installerPollTimeout = const Duration(minutes: 15);
late final ClientChannel _channel;
late final daemon.DaemonServiceClient _client;
late ClientSnapshot _snapshot;
Timer? _poller;
StreamSubscription<daemon.SystemEvent>? _eventSubscription;
var _started = false;
@override
Stream<ClientSnapshot> watchSnapshot() {
_start();
scheduleMicrotask(_emit);
return _snapshots.stream;
}
@override
Stream<SystemNotification> watchEvents() {
_start();
return _events.stream;
}
@override
Stream<UpdateProgressEvent> watchUpdateRequests() {
_start();
return _updateRequests.stream;
}
@override
Future<void> connect() async {
_setStatus(ConnectionStatus.connecting, clearError: true);
try {
await _runLoginFlow();
await _client.up(
daemon.UpRequest(username: _username()),
options: _options(timeout: const Duration(seconds: 30)),
);
} catch (error) {
_snapshot = _snapshot.copyWith(
status: ConnectionStatus.error,
errorMessage: _formatError(error),
clearPendingLogin: true,
);
_emit();
return;
} finally {
await _refresh();
}
}
@override
Future<void> disconnect() async {
await _runRpc(() async {
await _client.down(daemon.DownRequest(), options: _options());
});
}
@override
Future<void> bringUp() async {
await _client.up(
daemon.UpRequest(username: _username()),
options: _options(timeout: const Duration(seconds: 30)),
);
}
@override
Future<void> bringDown() async {
await _client.down(
daemon.DownRequest(),
options: _options(timeout: const Duration(seconds: 15)),
);
}
@override
Future<DebugBundleResult> debugBundle({
required bool anonymize,
required bool systemInfo,
String? uploadUrl,
}) async {
final request = daemon.DebugBundleRequest(
anonymize: anonymize,
systemInfo: systemInfo,
uploadURL: uploadUrl ?? '',
);
final response = await _client.debugBundle(
request,
options: _options(timeout: const Duration(minutes: 2)),
);
return DebugBundleResult(
path: response.path,
uploadedKey: response.uploadedKey,
uploadFailureReason: response.uploadFailureReason,
);
}
@override
Future<DaemonLogLevel> getLogLevel() async {
final response = await _client.getLogLevel(
daemon.GetLogLevelRequest(),
options: _options(),
);
return _mapLogLevelFromProto(response.level);
}
@override
Future<void> setLogLevel(DaemonLogLevel level) async {
await _client.setLogLevel(
daemon.SetLogLevelRequest(level: _mapLogLevelToProto(level)),
options: _options(),
);
}
@override
Future<void> setSyncResponsePersistence(bool enabled) async {
await _client.setSyncResponsePersistence(
daemon.SetSyncResponsePersistenceRequest(enabled: enabled),
options: _options(),
);
}
@override
Future<void> startCpuProfile() async {
await _client.startCPUProfile(
daemon.StartCPUProfileRequest(),
options: _options(),
);
}
@override
Future<void> stopCpuProfile() async {
await _client.stopCPUProfile(
daemon.StopCPUProfileRequest(),
options: _options(),
);
}
@override
Future<TriggerUpdateResult> triggerUpdate() async {
final response = await _client.triggerUpdate(
daemon.TriggerUpdateRequest(),
options: _options(timeout: const Duration(seconds: 30)),
);
return TriggerUpdateResult(
success: response.success,
errorMessage: response.errorMsg,
);
}
@override
Future<InstallerResult> getInstallerResult() async {
final response = await _client.getInstallerResult(
daemon.InstallerResultRequest(),
options: _options(timeout: _installerPollTimeout),
);
return InstallerResult(
success: response.success,
errorMessage: response.errorMsg,
);
}
@override
Future<void> updateSettings(ClientSettings updated) async {
await _runRpc(() async {
final activeProfile = _snapshot.activeProfile.name;
await _client.setConfig(
daemon.SetConfigRequest(
username: _username(),
profileName: activeProfile,
managementUrl: updated.managementUrl,
rosenpassEnabled: updated.quantumResistance,
serverSSHAllowed: updated.allowSsh,
disableAutoConnect: !updated.autoConnect,
disableNotifications: !updated.notifications,
lazyConnectionEnabled: updated.lazyConnection,
blockInbound: updated.blockInbound,
),
options: _options(timeout: const Duration(seconds: 10)),
);
});
}
@override
Future<void> setNetworkSelection(String routeId, bool selected) async {
await _runRpc(() async {
final request = daemon.SelectNetworksRequest(networkIDs: [routeId]);
if (selected) {
await _client.selectNetworks(request, options: _options());
} else {
await _client.deselectNetworks(request, options: _options());
}
});
}
@override
Future<void> setExitNode(String? routeId) async {
await _runRpc(() async {
final exitNodeIds = _snapshot.networks
.where((route) => route.isExitNode)
.map((route) => route.id)
.toList();
if (exitNodeIds.isNotEmpty) {
await _client.deselectNetworks(
daemon.SelectNetworksRequest(networkIDs: exitNodeIds),
options: _options(),
);
}
if (routeId != null) {
await _client.selectNetworks(
daemon.SelectNetworksRequest(networkIDs: [routeId]),
options: _options(),
);
}
});
}
@override
Future<void> switchProfile(String name) async {
await _runRpc(() async {
await _client.switchProfile(
daemon.SwitchProfileRequest(profileName: name, username: _username()),
options: _options(),
);
});
}
@override
Future<void> addProfile(String name) async {
await _runRpc(() async {
await _client.addProfile(
daemon.AddProfileRequest(profileName: name, username: _username()),
options: _options(),
);
});
}
@override
Future<void> removeProfile(String name) async {
await _runRpc(() async {
await _client.removeProfile(
daemon.RemoveProfileRequest(profileName: name, username: _username()),
options: _options(),
);
});
}
@override
Future<void> logoutActive() async {
await _runRpc(() async {
final active = _snapshot.activeProfile.name;
await _client.logout(
daemon.LogoutRequest(profileName: active, username: _username()),
options: _options(timeout: const Duration(seconds: 15)),
);
});
}
@override
void dispose() {
_poller?.cancel();
unawaited(_eventSubscription?.cancel() ?? Future<void>.value());
_events.close();
_updateRequests.close();
_snapshots.close();
unawaited(_channel.shutdown());
}
void _start() {
if (_started) {
return;
}
_started = true;
unawaited(_refresh());
_poller = Timer.periodic(_refreshInterval, (_) {
unawaited(_refresh());
});
_eventSubscription = _client
.subscribeEvents(daemon.SubscribeRequest(), options: _options())
.listen(
(event) {
_checkUpdateMetadata(event);
final notification = _mapSystemEvent(event);
if (notification != null && !_events.isClosed) {
_events.add(notification);
}
unawaited(_refresh());
},
onError: (_) {},
);
}
DaemonLogLevel _mapLogLevelFromProto(daemon.LogLevel level) {
return switch (level) {
daemon.LogLevel.PANIC => DaemonLogLevel.panic,
daemon.LogLevel.FATAL => DaemonLogLevel.fatal,
daemon.LogLevel.ERROR => DaemonLogLevel.error,
daemon.LogLevel.WARN => DaemonLogLevel.warn,
daemon.LogLevel.INFO => DaemonLogLevel.info,
daemon.LogLevel.DEBUG => DaemonLogLevel.debug,
daemon.LogLevel.TRACE => DaemonLogLevel.trace,
_ => DaemonLogLevel.unknown,
};
}
daemon.LogLevel _mapLogLevelToProto(DaemonLogLevel level) {
return switch (level) {
DaemonLogLevel.panic => daemon.LogLevel.PANIC,
DaemonLogLevel.fatal => daemon.LogLevel.FATAL,
DaemonLogLevel.error => daemon.LogLevel.ERROR,
DaemonLogLevel.warn => daemon.LogLevel.WARN,
DaemonLogLevel.info => daemon.LogLevel.INFO,
DaemonLogLevel.debug => daemon.LogLevel.DEBUG,
DaemonLogLevel.trace => daemon.LogLevel.TRACE,
DaemonLogLevel.unknown => daemon.LogLevel.UNKNOWN,
};
}
void _checkUpdateMetadata(daemon.SystemEvent event) {
final action = event.metadata['progress_window'];
if (action != 'show') {
return;
}
final version = event.metadata['version'] ?? 'unknown';
if (!_updateRequests.isClosed) {
_updateRequests.add(UpdateProgressEvent(version: version));
}
}
SystemNotification? _mapSystemEvent(daemon.SystemEvent event) {
final severity = switch (event.severity) {
daemon.SystemEvent_Severity.WARNING => NotificationSeverity.warning,
daemon.SystemEvent_Severity.ERROR => NotificationSeverity.error,
daemon.SystemEvent_Severity.CRITICAL => NotificationSeverity.critical,
_ => NotificationSeverity.info,
};
final category = switch (event.category) {
daemon.SystemEvent_Category.NETWORK => NotificationCategory.network,
daemon.SystemEvent_Category.DNS => NotificationCategory.dns,
daemon.SystemEvent_Category.AUTHENTICATION =>
NotificationCategory.authentication,
daemon.SystemEvent_Category.CONNECTIVITY =>
NotificationCategory.connectivity,
daemon.SystemEvent_Category.SYSTEM => NotificationCategory.system,
_ => NotificationCategory.system,
};
return SystemNotification(
severity: severity,
category: category,
message: event.message,
userMessage: event.userMessage,
id: event.metadata['id'],
);
}
Future<void> _runLoginFlow() async {
final loginResponse = await _client.login(
daemon.LoginRequest(
isUnixDesktopClient: Platform.isLinux,
profileName: _snapshot.activeProfile.name,
username: _username(),
hint: _snapshot.activeProfile.email,
),
options: _options(timeout: const Duration(seconds: 30)),
);
if (!loginResponse.needsSSOLogin) {
return;
}
_snapshot = _snapshot.copyWith(
status: ConnectionStatus.awaitingLogin,
pendingLogin: PendingLogin(
verificationUri: loginResponse.verificationURIComplete,
userCode: loginResponse.userCode,
),
);
_emit();
if (loginResponse.verificationURIComplete.isNotEmpty) {
await openExternalUrl(loginResponse.verificationURIComplete);
}
await _client.waitSSOLogin(
daemon.WaitSSOLoginRequest(userCode: loginResponse.userCode),
options: _options(timeout: _ssoLoginTimeout),
);
_snapshot = _snapshot.copyWith(
status: ConnectionStatus.connecting,
clearPendingLogin: true,
);
_emit();
}
Future<void> _runRpc(Future<void> Function() action) async {
try {
_snapshot = _snapshot.copyWith(clearError: true);
_emit();
await action();
} catch (error) {
_snapshot = _snapshot.copyWith(
status: ConnectionStatus.error,
errorMessage: _formatError(error),
);
_emit();
} finally {
await _refresh();
}
}
Future<void> _refresh() async {
try {
final status = await _client.status(
daemon.StatusRequest(),
options: _options(),
);
final activeProfile = await _loadActiveProfile();
final profiles = await _loadProfiles(activeProfile);
final networks = await _loadNetworks();
final settings = await _loadSettings(activeProfile);
final mappedStatus = _mapStatus(status.status);
final preserveAwaiting =
_snapshot.status == ConnectionStatus.awaitingLogin &&
mappedStatus != ConnectionStatus.connected;
_snapshot = ClientSnapshot(
daemonAddr: daemonAddr,
daemonVersion: status.daemonVersion.isEmpty
? 'unknown'
: status.daemonVersion,
status: preserveAwaiting ? ConnectionStatus.awaitingLogin : mappedStatus,
activeProfile: activeProfile,
profiles: profiles,
networks: networks,
settings: settings,
pendingLogin: preserveAwaiting ? _snapshot.pendingLogin : null,
);
} catch (error) {
_snapshot = _snapshot.copyWith(
status: ConnectionStatus.error,
errorMessage: _formatError(error),
);
}
_emit();
}
Future<ProfileInfo> _loadActiveProfile() async {
try {
final active = await _client.getActiveProfile(
daemon.GetActiveProfileRequest(),
options: _options(),
);
if (active.profileName.isNotEmpty) {
return ProfileInfo(
name: active.profileName,
email: _snapshot.activeProfile.email,
active: true,
);
}
} catch (_) {
// Keep the status pane usable even when optional profile RPCs fail.
}
return _snapshot.activeProfile;
}
Future<List<ProfileInfo>> _loadProfiles(ProfileInfo activeProfile) async {
try {
final response = await _client.listProfiles(
daemon.ListProfilesRequest(username: _username()),
options: _options(),
);
final profiles = response.profiles.map((profile) {
return ProfileInfo(name: profile.name, active: profile.isActive);
}).toList();
if (profiles.isNotEmpty) {
return profiles;
}
} catch (_) {
// Profile listing is not required for core connection status.
}
return [activeProfile];
}
Future<List<NetworkRoute>> _loadNetworks() async {
try {
final response = await _client.listNetworks(
daemon.ListNetworksRequest(),
options: _options(),
);
return _mapNetworks(response.routes);
} catch (_) {
return _snapshot.networks;
}
}
Future<ClientSettings> _loadSettings(ProfileInfo activeProfile) async {
try {
final config = await _client.getConfig(
daemon.GetConfigRequest(
profileName: activeProfile.name,
username: _username(),
),
options: _options(),
);
return ClientSettings(
managementUrl: config.managementUrl.isEmpty
? 'https://api.netbird.io'
: config.managementUrl,
interfaceName: config.interfaceName.isEmpty
? 'wt0'
: config.interfaceName,
wireguardPort: config.hasWireguardPort()
? config.wireguardPort.toInt()
: 51820,
mtu: config.hasMtu() ? config.mtu.toInt() : 1280,
autoConnect: !config.disableAutoConnect,
allowSsh: config.serverSSHAllowed,
quantumResistance: config.rosenpassEnabled,
notifications: !config.disableNotifications,
lazyConnection: config.lazyConnectionEnabled,
blockInbound: config.blockInbound,
);
} catch (_) {
return _snapshot.settings;
}
}
List<NetworkRoute> _mapNetworks(Iterable<daemon.Network> routes) {
final rangeCounts = <String, int>{};
for (final route in routes) {
if (route.domains.isEmpty) {
rangeCounts.update(
route.range,
(count) => count + 1,
ifAbsent: () => 1,
);
}
}
return routes.map((route) {
final resolvedIps = route.resolvedIPs.map((domain, ipList) {
return MapEntry(domain, ipList.ips.toList());
});
return NetworkRoute(
id: route.iD,
range: route.range,
selected: route.selected,
domains: route.domains.toList(),
resolvedIps: resolvedIps,
overlapping:
route.domains.isEmpty && (rangeCounts[route.range] ?? 0) > 1,
);
}).toList()
..sort((a, b) => a.id.toLowerCase().compareTo(b.id.toLowerCase()));
}
CallOptions _options({Duration? timeout}) {
return CallOptions(timeout: timeout ?? _callTimeout);
}
void _setStatus(
ConnectionStatus status, {
bool clearError = false,
bool clearPendingLogin = false,
}) {
_snapshot = _snapshot.copyWith(
status: status,
clearError: clearError,
clearPendingLogin: clearPendingLogin,
);
_emit();
}
void _emit() {
if (!_snapshots.isClosed) {
_snapshots.add(_snapshot);
}
}
}
class FakeDaemonClient implements DaemonClient {
FakeDaemonClient({required this.daemonAddr}) {
scheduleMicrotask(_emit);
}
@override
final String daemonAddr;
final _snapshots = StreamController<ClientSnapshot>.broadcast();
late ClientSnapshot _snapshot = ClientSnapshot.initial(daemonAddr).copyWith(
daemonVersion: 'development',
profiles: const [
ProfileInfo(name: 'default', email: 'user@example.com', active: true),
ProfileInfo(name: 'staging', active: false),
],
networks: const [
NetworkRoute(id: 'office', range: '10.10.0.0/16', selected: true),
NetworkRoute(id: 'prod', range: '10.20.0.0/16'),
NetworkRoute(id: 'exit-us', range: '0.0.0.0/0'),
],
);
@override
Stream<ClientSnapshot> watchSnapshot() {
scheduleMicrotask(_emit);
return _snapshots.stream;
}
@override
Stream<SystemNotification> watchEvents() =>
const Stream<SystemNotification>.empty();
@override
Stream<UpdateProgressEvent> watchUpdateRequests() =>
const Stream<UpdateProgressEvent>.empty();
@override
Future<void> connect() async {
_snapshot = _snapshot.copyWith(status: ConnectionStatus.connecting);
_emit();
await Future<void>.delayed(const Duration(milliseconds: 450));
_snapshot = _snapshot.copyWith(status: ConnectionStatus.connected);
_emit();
}
@override
Future<void> disconnect() async {
_snapshot = _snapshot.copyWith(status: ConnectionStatus.disconnected);
_emit();
}
@override
Future<void> bringUp() async {
_snapshot = _snapshot.copyWith(status: ConnectionStatus.connected);
_emit();
}
@override
Future<void> bringDown() async {
_snapshot = _snapshot.copyWith(status: ConnectionStatus.disconnected);
_emit();
}
@override
Future<DebugBundleResult> debugBundle({
required bool anonymize,
required bool systemInfo,
String? uploadUrl,
}) async {
await Future<void>.delayed(const Duration(milliseconds: 400));
return DebugBundleResult(
path: '/tmp/netbird-debug.tar.gz',
uploadedKey: uploadUrl == null ? '' : 'fake-upload-key',
);
}
@override
Future<DaemonLogLevel> getLogLevel() async => DaemonLogLevel.info;
@override
Future<void> setLogLevel(DaemonLogLevel level) async {}
@override
Future<void> setSyncResponsePersistence(bool enabled) async {}
@override
Future<void> startCpuProfile() async {}
@override
Future<void> stopCpuProfile() async {}
@override
Future<TriggerUpdateResult> triggerUpdate() async {
return const TriggerUpdateResult(success: true);
}
@override
Future<InstallerResult> getInstallerResult() async {
await Future<void>.delayed(const Duration(seconds: 2));
return const InstallerResult(success: true);
}
@override
Future<void> updateSettings(ClientSettings updated) async {
_snapshot = _snapshot.copyWith(settings: updated);
_emit();
}
@override
Future<void> setNetworkSelection(String routeId, bool selected) async {
final next = _snapshot.networks.map((route) {
if (route.id != routeId) {
return route;
}
return NetworkRoute(
id: route.id,
range: route.range,
domains: route.domains,
resolvedIps: route.resolvedIps,
overlapping: route.overlapping,
selected: selected,
);
}).toList();
_snapshot = _snapshot.copyWith(networks: next);
_emit();
}
@override
Future<void> setExitNode(String? routeId) async {
final next = _snapshot.networks.map((route) {
if (!route.isExitNode) {
return route;
}
return NetworkRoute(
id: route.id,
range: route.range,
domains: route.domains,
resolvedIps: route.resolvedIps,
overlapping: route.overlapping,
selected: route.id == routeId,
);
}).toList();
_snapshot = _snapshot.copyWith(networks: next);
_emit();
}
@override
Future<void> switchProfile(String name) async {
final profiles = _snapshot.profiles.map((profile) {
return ProfileInfo(
name: profile.name,
email: profile.email,
active: profile.name == name,
);
}).toList();
final active = profiles.firstWhere(
(profile) => profile.active,
orElse: () => _snapshot.activeProfile,
);
_snapshot = _snapshot.copyWith(profiles: profiles, activeProfile: active);
_emit();
}
@override
Future<void> addProfile(String name) async {
final profiles = [
..._snapshot.profiles,
ProfileInfo(name: name, active: false),
];
_snapshot = _snapshot.copyWith(profiles: profiles);
_emit();
}
@override
Future<void> removeProfile(String name) async {
final profiles = _snapshot.profiles
.where((profile) => profile.name != name)
.toList();
_snapshot = _snapshot.copyWith(profiles: profiles);
_emit();
}
@override
Future<void> logoutActive() async {
_snapshot = _snapshot.copyWith(status: ConnectionStatus.disconnected);
_emit();
}
@override
void dispose() {
_snapshots.close();
}
void _emit() {
if (!_snapshots.isClosed) {
_snapshots.add(_snapshot);
}
}
}
ClientChannel _createChannel(String daemonAddr) {
final options = ChannelOptions(
credentials: const ChannelCredentials.insecure(),
userAgent: _userAgent,
connectTimeout: const Duration(seconds: 3),
);
if (daemonAddr.startsWith('unix://')) {
final path = daemonAddr.substring('unix://'.length);
return ClientChannel(
InternetAddress(path, type: InternetAddressType.unix),
port: 0,
options: options,
);
}
final uri = daemonAddr.contains('://')
? Uri.parse(daemonAddr)
: Uri.parse('tcp://$daemonAddr');
final host = uri.host.isEmpty ? '127.0.0.1' : uri.host;
final port = uri.hasPort ? uri.port : 41731;
return ClientChannel(host, port: port, options: options);
}
ConnectionStatus _mapStatus(String status) {
return switch (status) {
'Connected' => ConnectionStatus.connected,
'Connecting' => ConnectionStatus.connecting,
'Idle' || 'SessionExpired' => ConnectionStatus.disconnected,
_ => ConnectionStatus.error,
};
}
String _username() {
if (Platform.isWindows) {
final username = Platform.environment['USERNAME'] ?? '';
final domain = Platform.environment['USERDOMAIN'] ?? '';
if (domain.isNotEmpty && username.isNotEmpty) {
return '$domain\\$username';
}
return username;
}
return Platform.environment['USER'] ?? Platform.environment['LOGNAME'] ?? '';
}
String _formatError(Object error) {
if (error is GrpcError) {
return error.message ?? error.toString();
}
return error.toString();
}

View File

@@ -0,0 +1,460 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'daemon_client.dart';
import 'models.dart';
import 'platform.dart';
const _defaultUploadUrl = 'https://upload.netbird.io/';
class DebugScreen extends StatefulWidget {
const DebugScreen({required this.client, super.key});
final DaemonClient client;
@override
State<DebugScreen> createState() => _DebugScreenState();
}
class _DebugScreenState extends State<DebugScreen> {
final _uploadUrlController =
TextEditingController(text: _defaultUploadUrl);
final _durationController = TextEditingController(text: '1');
bool _anonymize = false;
bool _systemInfo = true;
bool _upload = true;
bool _runWithTrace = true;
bool _busy = false;
String _status = '';
double? _progress;
@override
void dispose() {
_uploadUrlController.dispose();
_durationController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Debug', style: Theme.of(context).textTheme.headlineSmall),
const SizedBox(height: 16),
Text(
'Create a debug bundle to help troubleshoot issues with NetBird.',
style: Theme.of(context).textTheme.bodyMedium,
),
const SizedBox(height: 24),
Expanded(
child: ListView(
children: [
CheckboxListTile(
contentPadding: EdgeInsets.zero,
value: _anonymize,
onChanged: _busy
? null
: (value) => setState(() => _anonymize = value ?? false),
title: const Text(
'Anonymize sensitive information (public IPs, domains, ...)',
),
),
CheckboxListTile(
contentPadding: EdgeInsets.zero,
value: _systemInfo,
onChanged: _busy
? null
: (value) => setState(() => _systemInfo = value ?? false),
title: const Text(
'Include system information (routes, interfaces, ...)',
),
),
CheckboxListTile(
contentPadding: EdgeInsets.zero,
value: _upload,
onChanged: _busy
? null
: (value) => setState(() => _upload = value ?? false),
title: const Text('Upload bundle automatically after creation'),
),
if (_upload)
Padding(
padding: const EdgeInsets.only(left: 32, bottom: 8, top: 4),
child: TextField(
controller: _uploadUrlController,
enabled: !_busy,
decoration: const InputDecoration(
labelText: 'Debug upload URL',
border: OutlineInputBorder(),
),
),
),
const Divider(height: 32),
CheckboxListTile(
contentPadding: EdgeInsets.zero,
value: _runWithTrace,
onChanged: _busy
? null
: (value) =>
setState(() => _runWithTrace = value ?? false),
title: const Text(
'Run with trace logs before creating bundle',
),
),
if (_runWithTrace)
Padding(
padding: const EdgeInsets.only(left: 32, top: 4),
child: Row(
children: [
const Text('Run for'),
const SizedBox(width: 12),
SizedBox(
width: 80,
child: TextField(
controller: _durationController,
enabled: !_busy,
keyboardType: TextInputType.number,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
],
decoration: const InputDecoration(
isDense: true,
),
),
),
const SizedBox(width: 8),
Text(_durationLabel()),
],
),
),
if (_runWithTrace)
const Padding(
padding: EdgeInsets.only(left: 32, top: 8),
child: Text(
'Note: NetBird will be brought up and down during collection.',
style: TextStyle(fontStyle: FontStyle.italic),
),
),
const SizedBox(height: 24),
if (_status.isNotEmpty)
Padding(
padding: const EdgeInsets.only(bottom: 12),
child: Text(_status),
),
if (_progress != null)
Padding(
padding: const EdgeInsets.only(bottom: 16),
child: LinearProgressIndicator(value: _progress),
),
Align(
alignment: Alignment.centerLeft,
child: FilledButton.icon(
onPressed: _busy ? null : _onCreate,
icon: _busy
? const SizedBox(
width: 18,
height: 18,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Icon(Icons.archive_outlined),
label: const Text('Create Debug Bundle'),
),
),
],
),
),
],
),
);
}
String _durationLabel() {
final value = int.tryParse(_durationController.text) ?? 0;
return value == 1 ? 'minute' : 'minutes';
}
Future<void> _onCreate() async {
final uploadUrl = _upload ? _uploadUrlController.text.trim() : null;
if (_upload && (uploadUrl == null || uploadUrl.isEmpty)) {
setState(() => _status = 'Upload URL is required when upload is enabled');
return;
}
Duration? traceDuration;
if (_runWithTrace) {
final minutes = int.tryParse(_durationController.text);
if (minutes == null || minutes < 1) {
setState(() => _status = 'Duration must be a number ≥ 1');
return;
}
traceDuration = Duration(minutes: minutes);
}
setState(() {
_busy = true;
_status = '';
_progress = null;
});
try {
DebugBundleResult result;
if (traceDuration != null) {
result = await _runWithTraceLogs(
duration: traceDuration,
uploadUrl: uploadUrl,
);
} else {
setState(() => _status = 'Creating debug bundle...');
result = await widget.client.debugBundle(
anonymize: _anonymize,
systemInfo: _systemInfo,
uploadUrl: uploadUrl,
);
}
if (!mounted) {
return;
}
setState(() => _status = 'Bundle created successfully');
await _showResultDialog(result);
} catch (error) {
if (!mounted) {
return;
}
setState(() {
_status = 'Error: $error';
_progress = null;
});
} finally {
if (mounted) {
setState(() => _busy = false);
}
}
}
Future<DebugBundleResult> _runWithTraceLogs({
required Duration duration,
required String? uploadUrl,
}) async {
final initialLevel = await widget.client.getLogLevel();
final wasTrace = initialLevel == DaemonLogLevel.trace;
var levelChanged = false;
var persistenceEnabled = false;
var cpuProfileStarted = false;
try {
if (!wasTrace) {
await widget.client.setLogLevel(DaemonLogLevel.trace);
levelChanged = true;
}
try {
await widget.client.bringDown();
} catch (_) {
// Already down is fine; daemon returns OK either way.
}
await Future<void>.delayed(const Duration(seconds: 1));
try {
await widget.client.setSyncResponsePersistence(true);
persistenceEnabled = true;
} catch (_) {
// Persistence is best-effort — the bundle still works without it.
}
await widget.client.bringUp();
await Future<void>.delayed(const Duration(seconds: 3));
try {
await widget.client.startCpuProfile();
cpuProfileStarted = true;
} catch (_) {
// CPU profiling is optional.
}
await _trackProgress(duration);
if (cpuProfileStarted) {
try {
await widget.client.stopCpuProfile();
} catch (_) {}
}
if (!mounted) {
return const DebugBundleResult(path: '');
}
setState(() {
_status = 'Creating debug bundle with collected logs...';
_progress = null;
});
return await widget.client.debugBundle(
anonymize: _anonymize,
systemInfo: _systemInfo,
uploadUrl: uploadUrl,
);
} finally {
if (levelChanged) {
try {
await widget.client.setLogLevel(initialLevel);
} catch (_) {}
}
if (persistenceEnabled) {
try {
await widget.client.setSyncResponsePersistence(false);
} catch (_) {}
}
}
}
Future<void> _trackProgress(Duration total) async {
final start = DateTime.now();
final end = start.add(total);
setState(() {
_progress = 0;
_status = 'Running with trace logs... ${_formatRemaining(total)} remaining';
});
while (DateTime.now().isBefore(end)) {
await Future<void>.delayed(const Duration(milliseconds: 500));
if (!mounted) {
return;
}
final elapsed = DateTime.now().difference(start);
final fraction = (elapsed.inMilliseconds / total.inMilliseconds).clamp(
0.0,
1.0,
);
final remaining = end.difference(DateTime.now());
setState(() {
_progress = fraction;
_status =
'Running with trace logs... ${_formatRemaining(remaining < Duration.zero ? Duration.zero : remaining)} remaining';
});
}
}
String _formatRemaining(Duration d) {
final hours = d.inHours.toString().padLeft(2, '0');
final minutes = (d.inMinutes % 60).toString().padLeft(2, '0');
final seconds = (d.inSeconds % 60).toString().padLeft(2, '0');
return '$hours:$minutes:$seconds';
}
Future<void> _showResultDialog(DebugBundleResult result) async {
if (!mounted) {
return;
}
await showDialog<void>(
context: context,
builder: (context) => _DebugResultDialog(result: result),
);
}
}
class _DebugResultDialog extends StatelessWidget {
const _DebugResultDialog({required this.result});
final DebugBundleResult result;
@override
Widget build(BuildContext context) {
final folder = _parentFolder(result.path);
String title;
Widget body;
if (result.uploadFailed) {
title = 'Upload Failed';
body = Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text('Bundle upload failed:\n${result.uploadFailureReason}'),
const SizedBox(height: 12),
SelectableText('Local copy: ${result.path}'),
],
);
} else if (result.uploaded) {
title = 'Upload Successful';
body = Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
const Text('Bundle uploaded successfully.'),
const SizedBox(height: 12),
const Text('Upload key:'),
SelectableText(result.uploadedKey),
const SizedBox(height: 12),
SelectableText('Local copy: ${result.path}'),
],
);
} else {
title = 'Debug Bundle Created';
body = Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text('Bundle created locally at:\n${result.path}'),
const SizedBox(height: 8),
const Text(
'Administrator privileges may be required to access the file.',
style: TextStyle(fontStyle: FontStyle.italic),
),
],
);
}
return AlertDialog(
title: Text(title),
content: SingleChildScrollView(child: body),
actions: [
if (result.uploaded)
TextButton.icon(
onPressed: () async {
await Clipboard.setData(
ClipboardData(text: result.uploadedKey),
);
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Upload key copied')),
);
}
},
icon: const Icon(Icons.copy),
label: const Text('Copy key'),
),
TextButton.icon(
onPressed: result.path.isEmpty
? null
: () => openExternalUrl(result.path),
icon: const Icon(Icons.description_outlined),
label: const Text('Open file'),
),
TextButton.icon(
onPressed: folder.isEmpty ? null : () => openExternalUrl(folder),
icon: const Icon(Icons.folder_open),
label: const Text('Open folder'),
),
FilledButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Close'),
),
],
);
}
String _parentFolder(String path) {
if (path.isEmpty) {
return '';
}
final lastSlash = path.lastIndexOf(RegExp(r'[/\\]'));
return lastSlash <= 0 ? '' : path.substring(0, lastSlash);
}
}

View File

@@ -0,0 +1,434 @@
import 'dart:async';
import 'dart:io';
import 'package:local_notifier/local_notifier.dart';
import 'package:tray_manager/tray_manager.dart';
import 'package:window_manager/window_manager.dart';
import 'daemon_client.dart';
import 'models.dart';
import 'platform.dart';
const uiVersion = '0.1.0';
const _githubUrl = 'https://github.com/netbirdio/netbird';
const _downloadUrl = 'https://netbird.io/download/';
class TabIndex {
static const status = 0;
static const networks = 1;
static const profiles = 2;
static const settings = 3;
static const debug = 4;
}
/// Owns native desktop integration: window lifecycle (hide on close), system
/// tray icon and menu, and OS-level notifications driven by daemon events.
class DesktopIntegration with TrayListener, WindowListener {
DesktopIntegration({required this.client});
final DaemonClient client;
final _tabRequests = StreamController<int>.broadcast();
StreamSubscription<ClientSnapshot>? _snapshotSub;
StreamSubscription<SystemNotification>? _eventSub;
ClientSnapshot? _lastSnapshot;
String? _lastMenuKey;
bool _disposed = false;
bool _settingsBusy = false;
Stream<int> get tabRequests => _tabRequests.stream;
static const _trayMenuConnect = 'connect';
static const _trayMenuDisconnect = 'disconnect';
static const _trayMenuShow = 'show';
static const _trayMenuQuit = 'quit';
static const _trayMenuAllowSSH = 'settings.allow_ssh';
static const _trayMenuAutoConnect = 'settings.auto_connect';
static const _trayMenuQuantum = 'settings.quantum';
static const _trayMenuLazy = 'settings.lazy';
static const _trayMenuBlockInbound = 'settings.block_inbound';
static const _trayMenuNotifications = 'settings.notifications';
static const _trayMenuAdvancedSettings = 'open.settings';
static const _trayMenuDebugBundle = 'open.debug';
static const _trayMenuNetworks = 'open.networks';
static const _trayMenuManageProfiles = 'open.profiles';
static const _trayMenuLogout = 'profile.logout';
static const _trayMenuGithub = 'about.github';
static const _trayMenuDownload = 'about.download';
static const _profileSwitchPrefix = 'profile.switch:';
Future<void> initialize() async {
await localNotifier.setup(appName: 'NetBird');
await windowManager.setPreventClose(true);
windowManager.addListener(this);
trayManager.addListener(this);
await _applyTrayIcon(ConnectionStatus.disconnected);
await trayManager.setToolTip('NetBird');
await _refreshTrayMenu(null);
_snapshotSub = client.watchSnapshot().listen(_onSnapshot);
_eventSub = client.watchEvents().listen(_onEvent);
}
Future<void> dispose() async {
if (_disposed) {
return;
}
_disposed = true;
await _snapshotSub?.cancel();
await _eventSub?.cancel();
await _tabRequests.close();
windowManager.removeListener(this);
trayManager.removeListener(this);
await trayManager.destroy();
}
@override
void onWindowClose() {
unawaited(_handleWindowClose());
}
Future<void> _handleWindowClose() async {
final prevent = await windowManager.isPreventClose();
if (prevent) {
await windowManager.hide();
}
}
@override
void onTrayIconMouseDown() {
if (Platform.isMacOS) {
unawaited(trayManager.popUpContextMenu());
} else {
unawaited(_showWindow());
}
}
@override
void onTrayIconRightMouseDown() {
unawaited(trayManager.popUpContextMenu());
}
@override
void onTrayMenuItemClick(MenuItem menuItem) {
final key = menuItem.key;
if (key == null) {
return;
}
if (key.startsWith(_profileSwitchPrefix)) {
final name = key.substring(_profileSwitchPrefix.length);
unawaited(_switchProfile(name));
return;
}
switch (key) {
case _trayMenuConnect:
unawaited(client.connect());
case _trayMenuDisconnect:
unawaited(client.disconnect());
case _trayMenuShow:
unawaited(_showWindow());
case _trayMenuQuit:
unawaited(_quit());
case _trayMenuAllowSSH:
unawaited(_toggleSetting((s) => s.copyWith(allowSsh: !s.allowSsh)));
case _trayMenuAutoConnect:
unawaited(
_toggleSetting((s) => s.copyWith(autoConnect: !s.autoConnect)),
);
case _trayMenuQuantum:
unawaited(
_toggleSetting(
(s) => s.copyWith(quantumResistance: !s.quantumResistance),
),
);
case _trayMenuLazy:
unawaited(
_toggleSetting(
(s) => s.copyWith(lazyConnection: !s.lazyConnection),
),
);
case _trayMenuBlockInbound:
unawaited(
_toggleSetting(
(s) => s.copyWith(blockInbound: !s.blockInbound),
),
);
case _trayMenuNotifications:
unawaited(
_toggleSetting(
(s) => s.copyWith(notifications: !s.notifications),
),
);
case _trayMenuAdvancedSettings:
unawaited(_openTab(TabIndex.settings));
case _trayMenuDebugBundle:
unawaited(_openTab(TabIndex.debug));
case _trayMenuNetworks:
unawaited(_openTab(TabIndex.networks));
case _trayMenuManageProfiles:
unawaited(_openTab(TabIndex.profiles));
case _trayMenuLogout:
unawaited(client.logoutActive());
case _trayMenuGithub:
unawaited(openExternalUrl(_githubUrl));
case _trayMenuDownload:
unawaited(openExternalUrl(_downloadUrl));
}
}
Future<void> _openTab(int index) async {
if (!_tabRequests.isClosed) {
_tabRequests.add(index);
}
await _showWindow();
}
Future<void> _toggleSetting(
ClientSettings Function(ClientSettings) mutate,
) async {
if (_settingsBusy) {
return;
}
final snapshot = _lastSnapshot;
if (snapshot == null) {
return;
}
_settingsBusy = true;
try {
await client.updateSettings(mutate(snapshot.settings));
} finally {
_settingsBusy = false;
}
}
Future<void> _switchProfile(String name) async {
final snapshot = _lastSnapshot;
if (snapshot == null || snapshot.activeProfile.name == name) {
return;
}
await client.switchProfile(name);
}
Future<void> _showWindow() async {
await windowManager.show();
await windowManager.focus();
}
Future<void> _quit() async {
await dispose();
await windowManager.setPreventClose(false);
await windowManager.destroy();
}
void _onSnapshot(ClientSnapshot snapshot) {
final previous = _lastSnapshot;
_lastSnapshot = snapshot;
if (previous == null || previous.status != snapshot.status) {
unawaited(_applyTrayIcon(snapshot.status));
unawaited(trayManager.setToolTip('NetBird — ${snapshot.status.label}'));
}
unawaited(_refreshTrayMenu(snapshot));
}
void _onEvent(SystemNotification event) {
if (event.userMessage.isEmpty) {
return;
}
final notificationsEnabled =
_lastSnapshot?.settings.notifications ?? true;
final critical = event.severity == NotificationSeverity.critical;
if (!notificationsEnabled && !critical) {
return;
}
final title = '${_severityPrefix(event.severity)} [${event.category.label}]';
final body = event.id == null
? event.userMessage
: '${event.userMessage} (id: ${event.id})';
unawaited(
LocalNotification(title: title, body: body).show(),
);
}
Future<void> _applyTrayIcon(ConnectionStatus status) async {
final basename = switch (status) {
ConnectionStatus.connected => 'connected',
ConnectionStatus.connecting ||
ConnectionStatus.awaitingLogin => 'connecting',
ConnectionStatus.error => 'error',
ConnectionStatus.disconnected => 'disconnected',
};
final asset = Platform.isMacOS
? 'assets/tray/$basename-macos.png'
: 'assets/tray/$basename.png';
await trayManager.setIcon(asset, isTemplate: Platform.isMacOS);
}
Future<void> _refreshTrayMenu(ClientSnapshot? snapshot) async {
final key = _menuKey(snapshot);
if (key == _lastMenuKey) {
return;
}
_lastMenuKey = key;
final connected = snapshot?.status == ConnectionStatus.connected;
final connecting = snapshot?.status == ConnectionStatus.connecting ||
snapshot?.status == ConnectionStatus.awaitingLogin;
final statusLabel =
snapshot?.status.label ?? ConnectionStatus.disconnected.label;
final settings = snapshot?.settings ?? const ClientSettings();
final activeName = snapshot?.activeProfile.name ?? 'unknown';
final email = snapshot?.activeProfile.email;
final daemonVersion = snapshot?.daemonVersion ?? 'unknown';
final profileItems = <MenuItem>[];
final profiles = snapshot?.profiles ?? const <ProfileInfo>[];
for (final profile in profiles) {
profileItems.add(
MenuItem.checkbox(
key: '$_profileSwitchPrefix${profile.name}',
label: profile.name,
checked: profile.active,
),
);
}
if (profileItems.isNotEmpty) {
profileItems.add(MenuItem.separator());
}
profileItems
..add(MenuItem(key: _trayMenuManageProfiles, label: 'Manage Profiles'))
..add(
MenuItem(
key: _trayMenuLogout,
label: 'Deregister',
disabled: !connected,
),
);
await trayManager.setContextMenu(
Menu(
items: [
MenuItem(label: statusLabel, disabled: true),
MenuItem.submenu(
label: 'Profile: $activeName',
submenu: Menu(items: profileItems),
),
if (email != null && email.isNotEmpty)
MenuItem(label: '($email)', disabled: true),
MenuItem.separator(),
MenuItem(
key: _trayMenuConnect,
label: 'Connect',
disabled: connected || connecting,
),
MenuItem(
key: _trayMenuDisconnect,
label: 'Disconnect',
disabled: !connected,
),
MenuItem.separator(),
MenuItem.submenu(
label: 'Settings',
submenu: Menu(
items: [
MenuItem.checkbox(
key: _trayMenuAllowSSH,
label: 'Allow SSH',
checked: settings.allowSsh,
),
MenuItem.checkbox(
key: _trayMenuAutoConnect,
label: 'Connect on Startup',
checked: settings.autoConnect,
),
MenuItem.checkbox(
key: _trayMenuQuantum,
label: 'Enable Quantum-Resistance',
checked: settings.quantumResistance,
),
MenuItem.checkbox(
key: _trayMenuLazy,
label: 'Enable Lazy Connections',
checked: settings.lazyConnection,
),
MenuItem.checkbox(
key: _trayMenuBlockInbound,
label: 'Block Inbound Connections',
checked: settings.blockInbound,
),
MenuItem.checkbox(
key: _trayMenuNotifications,
label: 'Notifications',
checked: settings.notifications,
),
MenuItem.separator(),
MenuItem(
key: _trayMenuAdvancedSettings,
label: 'Advanced Settings',
),
MenuItem(
key: _trayMenuDebugBundle,
label: 'Create Debug Bundle',
),
],
),
),
MenuItem(key: _trayMenuNetworks, label: 'Networks'),
MenuItem.separator(),
MenuItem.submenu(
label: 'About',
submenu: Menu(
items: [
MenuItem(key: _trayMenuGithub, label: 'GitHub'),
MenuItem(label: 'GUI: $uiVersion', disabled: true),
MenuItem(label: 'Daemon: $daemonVersion', disabled: true),
MenuItem(
key: _trayMenuDownload,
label: 'Download latest version',
),
],
),
),
MenuItem.separator(),
MenuItem(key: _trayMenuShow, label: 'Show window'),
MenuItem(key: _trayMenuQuit, label: 'Quit'),
],
),
);
}
String _menuKey(ClientSnapshot? snapshot) {
if (snapshot == null) {
return 'null';
}
final s = snapshot.settings;
final profiles = snapshot.profiles
.map((p) => '${p.name}:${p.active}:${p.email ?? ''}')
.join(',');
return [
snapshot.status.name,
snapshot.activeProfile.name,
snapshot.activeProfile.email ?? '',
snapshot.daemonVersion,
profiles,
s.allowSsh,
s.autoConnect,
s.quantumResistance,
s.lazyConnection,
s.blockInbound,
s.notifications,
].join('|');
}
String _severityPrefix(NotificationSeverity severity) {
return switch (severity) {
NotificationSeverity.critical => 'Critical',
NotificationSeverity.error => 'Error',
NotificationSeverity.warning => 'Warning',
NotificationSeverity.info => 'Info',
};
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,153 @@
// This is a generated file - do not edit.
//
// Generated from daemon.proto.
// @dart = 3.3
// ignore_for_file: annotate_overrides, camel_case_types, comment_references
// ignore_for_file: constant_identifier_names
// ignore_for_file: curly_braces_in_flow_control_structures
// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes
// ignore_for_file: non_constant_identifier_names, prefer_relative_imports
import 'dart:core' as $core;
import 'package:protobuf/protobuf.dart' as $pb;
class LogLevel extends $pb.ProtobufEnum {
static const LogLevel UNKNOWN =
LogLevel._(0, _omitEnumNames ? '' : 'UNKNOWN');
static const LogLevel PANIC = LogLevel._(1, _omitEnumNames ? '' : 'PANIC');
static const LogLevel FATAL = LogLevel._(2, _omitEnumNames ? '' : 'FATAL');
static const LogLevel ERROR = LogLevel._(3, _omitEnumNames ? '' : 'ERROR');
static const LogLevel WARN = LogLevel._(4, _omitEnumNames ? '' : 'WARN');
static const LogLevel INFO = LogLevel._(5, _omitEnumNames ? '' : 'INFO');
static const LogLevel DEBUG = LogLevel._(6, _omitEnumNames ? '' : 'DEBUG');
static const LogLevel TRACE = LogLevel._(7, _omitEnumNames ? '' : 'TRACE');
static const $core.List<LogLevel> values = <LogLevel>[
UNKNOWN,
PANIC,
FATAL,
ERROR,
WARN,
INFO,
DEBUG,
TRACE,
];
static final $core.List<LogLevel?> _byValue =
$pb.ProtobufEnum.$_initByValueList(values, 7);
static LogLevel? valueOf($core.int value) =>
value < 0 || value >= _byValue.length ? null : _byValue[value];
const LogLevel._(super.value, super.name);
}
class ExposeProtocol extends $pb.ProtobufEnum {
static const ExposeProtocol EXPOSE_HTTP =
ExposeProtocol._(0, _omitEnumNames ? '' : 'EXPOSE_HTTP');
static const ExposeProtocol EXPOSE_HTTPS =
ExposeProtocol._(1, _omitEnumNames ? '' : 'EXPOSE_HTTPS');
static const ExposeProtocol EXPOSE_TCP =
ExposeProtocol._(2, _omitEnumNames ? '' : 'EXPOSE_TCP');
static const ExposeProtocol EXPOSE_UDP =
ExposeProtocol._(3, _omitEnumNames ? '' : 'EXPOSE_UDP');
static const ExposeProtocol EXPOSE_TLS =
ExposeProtocol._(4, _omitEnumNames ? '' : 'EXPOSE_TLS');
static const $core.List<ExposeProtocol> values = <ExposeProtocol>[
EXPOSE_HTTP,
EXPOSE_HTTPS,
EXPOSE_TCP,
EXPOSE_UDP,
EXPOSE_TLS,
];
static final $core.List<ExposeProtocol?> _byValue =
$pb.ProtobufEnum.$_initByValueList(values, 4);
static ExposeProtocol? valueOf($core.int value) =>
value < 0 || value >= _byValue.length ? null : _byValue[value];
const ExposeProtocol._(super.value, super.name);
}
/// avoid collision with loglevel enum
class OSLifecycleRequest_CycleType extends $pb.ProtobufEnum {
static const OSLifecycleRequest_CycleType UNKNOWN =
OSLifecycleRequest_CycleType._(0, _omitEnumNames ? '' : 'UNKNOWN');
static const OSLifecycleRequest_CycleType SLEEP =
OSLifecycleRequest_CycleType._(1, _omitEnumNames ? '' : 'SLEEP');
static const OSLifecycleRequest_CycleType WAKEUP =
OSLifecycleRequest_CycleType._(2, _omitEnumNames ? '' : 'WAKEUP');
static const $core.List<OSLifecycleRequest_CycleType> values =
<OSLifecycleRequest_CycleType>[
UNKNOWN,
SLEEP,
WAKEUP,
];
static final $core.List<OSLifecycleRequest_CycleType?> _byValue =
$pb.ProtobufEnum.$_initByValueList(values, 2);
static OSLifecycleRequest_CycleType? valueOf($core.int value) =>
value < 0 || value >= _byValue.length ? null : _byValue[value];
const OSLifecycleRequest_CycleType._(super.value, super.name);
}
class SystemEvent_Severity extends $pb.ProtobufEnum {
static const SystemEvent_Severity INFO =
SystemEvent_Severity._(0, _omitEnumNames ? '' : 'INFO');
static const SystemEvent_Severity WARNING =
SystemEvent_Severity._(1, _omitEnumNames ? '' : 'WARNING');
static const SystemEvent_Severity ERROR =
SystemEvent_Severity._(2, _omitEnumNames ? '' : 'ERROR');
static const SystemEvent_Severity CRITICAL =
SystemEvent_Severity._(3, _omitEnumNames ? '' : 'CRITICAL');
static const $core.List<SystemEvent_Severity> values = <SystemEvent_Severity>[
INFO,
WARNING,
ERROR,
CRITICAL,
];
static final $core.List<SystemEvent_Severity?> _byValue =
$pb.ProtobufEnum.$_initByValueList(values, 3);
static SystemEvent_Severity? valueOf($core.int value) =>
value < 0 || value >= _byValue.length ? null : _byValue[value];
const SystemEvent_Severity._(super.value, super.name);
}
class SystemEvent_Category extends $pb.ProtobufEnum {
static const SystemEvent_Category NETWORK =
SystemEvent_Category._(0, _omitEnumNames ? '' : 'NETWORK');
static const SystemEvent_Category DNS =
SystemEvent_Category._(1, _omitEnumNames ? '' : 'DNS');
static const SystemEvent_Category AUTHENTICATION =
SystemEvent_Category._(2, _omitEnumNames ? '' : 'AUTHENTICATION');
static const SystemEvent_Category CONNECTIVITY =
SystemEvent_Category._(3, _omitEnumNames ? '' : 'CONNECTIVITY');
static const SystemEvent_Category SYSTEM =
SystemEvent_Category._(4, _omitEnumNames ? '' : 'SYSTEM');
static const $core.List<SystemEvent_Category> values = <SystemEvent_Category>[
NETWORK,
DNS,
AUTHENTICATION,
CONNECTIVITY,
SYSTEM,
];
static final $core.List<SystemEvent_Category?> _byValue =
$pb.ProtobufEnum.$_initByValueList(values, 4);
static SystemEvent_Category? valueOf($core.int value) =>
value < 0 || value >= _byValue.length ? null : _byValue[value];
const SystemEvent_Category._(super.value, super.name);
}
const $core.bool _omitEnumNames =
$core.bool.fromEnvironment('protobuf.omit_enum_names');

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,257 @@
enum ConnectionStatus {
disconnected,
connecting,
awaitingLogin,
connected,
error;
String get label {
return switch (this) {
ConnectionStatus.disconnected => 'Disconnected',
ConnectionStatus.connecting => 'Connecting',
ConnectionStatus.awaitingLogin => 'Awaiting login',
ConnectionStatus.connected => 'Connected',
ConnectionStatus.error => 'Error',
};
}
}
enum NetworkFilter {
all,
overlapping,
exitNode;
bool matches(NetworkRoute route) {
return switch (this) {
NetworkFilter.all => true,
NetworkFilter.overlapping => route.overlapping,
NetworkFilter.exitNode => route.isExitNode,
};
}
}
class ClientSnapshot {
const ClientSnapshot({
required this.daemonAddr,
required this.daemonVersion,
required this.status,
required this.activeProfile,
required this.profiles,
required this.networks,
required this.settings,
this.errorMessage,
this.pendingLogin,
});
factory ClientSnapshot.initial(String daemonAddr) {
return ClientSnapshot(
daemonAddr: daemonAddr,
daemonVersion: 'unknown',
status: ConnectionStatus.disconnected,
activeProfile: const ProfileInfo(name: 'default', active: true),
profiles: const [ProfileInfo(name: 'default', active: true)],
networks: const [],
settings: const ClientSettings(),
);
}
final String daemonAddr;
final String daemonVersion;
final ConnectionStatus status;
final ProfileInfo activeProfile;
final List<ProfileInfo> profiles;
final List<NetworkRoute> networks;
final ClientSettings settings;
final String? errorMessage;
final PendingLogin? pendingLogin;
ClientSnapshot copyWith({
String? daemonAddr,
String? daemonVersion,
ConnectionStatus? status,
ProfileInfo? activeProfile,
List<ProfileInfo>? profiles,
List<NetworkRoute>? networks,
ClientSettings? settings,
String? errorMessage,
PendingLogin? pendingLogin,
bool clearError = false,
bool clearPendingLogin = false,
}) {
return ClientSnapshot(
daemonAddr: daemonAddr ?? this.daemonAddr,
daemonVersion: daemonVersion ?? this.daemonVersion,
status: status ?? this.status,
activeProfile: activeProfile ?? this.activeProfile,
profiles: profiles ?? this.profiles,
networks: networks ?? this.networks,
settings: settings ?? this.settings,
errorMessage: clearError ? null : errorMessage ?? this.errorMessage,
pendingLogin: clearPendingLogin
? null
: pendingLogin ?? this.pendingLogin,
);
}
}
class PendingLogin {
const PendingLogin({
required this.verificationUri,
required this.userCode,
});
final String verificationUri;
final String userCode;
}
class ProfileInfo {
const ProfileInfo({required this.name, required this.active, this.email});
final String name;
final String? email;
final bool active;
}
class NetworkRoute {
const NetworkRoute({
required this.id,
required this.range,
this.domains = const [],
this.resolvedIps = const {},
this.selected = false,
this.overlapping = false,
});
final String id;
final String range;
final List<String> domains;
final Map<String, List<String>> resolvedIps;
final bool selected;
final bool overlapping;
bool get isExitNode => range == '0.0.0.0/0';
}
enum DaemonLogLevel { unknown, panic, fatal, error, warn, info, debug, trace }
class DebugBundleResult {
const DebugBundleResult({
required this.path,
this.uploadedKey = '',
this.uploadFailureReason = '',
});
final String path;
final String uploadedKey;
final String uploadFailureReason;
bool get uploaded => uploadedKey.isNotEmpty && uploadFailureReason.isEmpty;
bool get uploadFailed => uploadFailureReason.isNotEmpty;
}
class TriggerUpdateResult {
const TriggerUpdateResult({required this.success, this.errorMessage = ''});
final bool success;
final String errorMessage;
}
class InstallerResult {
const InstallerResult({required this.success, this.errorMessage = ''});
final bool success;
final String errorMessage;
}
class UpdateProgressEvent {
const UpdateProgressEvent({required this.version});
final String version;
}
enum NotificationSeverity { info, warning, error, critical }
enum NotificationCategory {
network,
dns,
authentication,
connectivity,
system;
String get label {
return switch (this) {
NotificationCategory.network => 'Network',
NotificationCategory.dns => 'DNS',
NotificationCategory.authentication => 'Authentication',
NotificationCategory.connectivity => 'Connectivity',
NotificationCategory.system => 'System',
};
}
}
class SystemNotification {
const SystemNotification({
required this.severity,
required this.category,
required this.message,
required this.userMessage,
this.id,
});
final NotificationSeverity severity;
final NotificationCategory category;
final String message;
final String userMessage;
final String? id;
}
class ClientSettings {
const ClientSettings({
this.managementUrl = 'https://api.netbird.io',
this.interfaceName = 'wt0',
this.wireguardPort = 51820,
this.mtu = 1280,
this.autoConnect = true,
this.allowSsh = false,
this.quantumResistance = false,
this.notifications = true,
this.lazyConnection = false,
this.blockInbound = false,
});
final String managementUrl;
final String interfaceName;
final int wireguardPort;
final int mtu;
final bool autoConnect;
final bool allowSsh;
final bool quantumResistance;
final bool notifications;
final bool lazyConnection;
final bool blockInbound;
ClientSettings copyWith({
String? managementUrl,
String? interfaceName,
int? wireguardPort,
int? mtu,
bool? autoConnect,
bool? allowSsh,
bool? quantumResistance,
bool? notifications,
bool? lazyConnection,
bool? blockInbound,
}) {
return ClientSettings(
managementUrl: managementUrl ?? this.managementUrl,
interfaceName: interfaceName ?? this.interfaceName,
wireguardPort: wireguardPort ?? this.wireguardPort,
mtu: mtu ?? this.mtu,
autoConnect: autoConnect ?? this.autoConnect,
allowSsh: allowSsh ?? this.allowSsh,
quantumResistance: quantumResistance ?? this.quantumResistance,
notifications: notifications ?? this.notifications,
lazyConnection: lazyConnection ?? this.lazyConnection,
blockInbound: blockInbound ?? this.blockInbound,
);
}
}

View File

@@ -0,0 +1,22 @@
import 'dart:io';
/// Opens a URL in the user's default browser. Returns false if the platform
/// helper exits non-zero or is missing. Mirrors the Go UI's `openURL` logic.
Future<bool> openExternalUrl(String url) async {
try {
final ProcessResult result;
if (Platform.isMacOS) {
result = await Process.run('open', [url]);
} else if (Platform.isWindows) {
result = await Process.run('rundll32', [
'url.dll,FileProtocolHandler',
url,
]);
} else {
result = await Process.run('xdg-open', [url]);
}
return result.exitCode == 0;
} catch (_) {
return false;
}
}

View File

@@ -0,0 +1,140 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'daemon_client.dart';
import 'models.dart';
const _allowCloseAfter = Duration(seconds: 10);
const _dotInterval = Duration(seconds: 1);
/// Shows a modal dialog while the daemon installs an update. Polls
/// `GetInstallerResult` and resolves when the daemon finishes or fails.
Future<void> showUpdateProgressDialog({
required BuildContext context,
required DaemonClient client,
required UpdateProgressEvent event,
}) {
return showDialog<void>(
context: context,
barrierDismissible: false,
builder: (context) => _UpdateProgressDialog(client: client, event: event),
);
}
class _UpdateProgressDialog extends StatefulWidget {
const _UpdateProgressDialog({required this.client, required this.event});
final DaemonClient client;
final UpdateProgressEvent event;
@override
State<_UpdateProgressDialog> createState() => _UpdateProgressDialogState();
}
class _UpdateProgressDialogState extends State<_UpdateProgressDialog> {
Timer? _dotTimer;
Timer? _allowCloseTimer;
int _dots = 0;
bool _canClose = false;
String _status = 'Updating';
String? _error;
bool _resolved = false;
@override
void initState() {
super.initState();
_dotTimer = Timer.periodic(_dotInterval, (_) => _tick());
_allowCloseTimer = Timer(_allowCloseAfter, () {
if (mounted) {
setState(() => _canClose = true);
}
});
unawaited(_pollInstaller());
}
@override
void dispose() {
_dotTimer?.cancel();
_allowCloseTimer?.cancel();
super.dispose();
}
void _tick() {
if (!mounted) {
return;
}
setState(() {
_dots = (_dots + 1) % 4;
_status = 'Updating${'.' * _dots}';
});
}
Future<void> _pollInstaller() async {
try {
final result = await widget.client.getInstallerResult();
if (!mounted) {
return;
}
if (result.success) {
Navigator.of(context).pop();
return;
}
setState(() {
_resolved = true;
_canClose = true;
_status = 'Update failed';
_error = result.errorMessage.isEmpty
? 'Unknown error'
: result.errorMessage;
});
} catch (error) {
if (!mounted) {
return;
}
setState(() {
_resolved = true;
_canClose = true;
_status = 'Update timed out';
_error = error.toString();
});
}
}
@override
Widget build(BuildContext context) {
return PopScope(
canPop: _canClose,
child: AlertDialog(
title: const Text('Updating client'),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Your client version is older than the auto-update version set in '
'Management.\nUpdating client to ${widget.event.version}.',
),
const SizedBox(height: 16),
if (!_resolved) const LinearProgressIndicator(),
const SizedBox(height: 12),
Text(_status),
if (_error != null) ...[
const SizedBox(height: 12),
Text(
_error!,
style: TextStyle(color: Theme.of(context).colorScheme.error),
),
],
],
),
actions: [
TextButton(
onPressed: _canClose ? () => Navigator.of(context).pop() : null,
child: const Text('Close'),
),
],
),
);
}
}

1
client/flutter_ui/linux/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
flutter/ephemeral

View File

@@ -0,0 +1,128 @@
# Project-level configuration.
cmake_minimum_required(VERSION 3.13)
project(runner LANGUAGES CXX)
# The name of the executable created for the application. Change this to change
# the on-disk name of your application.
set(BINARY_NAME "netbird_flutter_ui")
# The unique GTK application identifier for this application. See:
# https://wiki.gnome.org/HowDoI/ChooseApplicationID
set(APPLICATION_ID "io.netbird.netbird_flutter_ui")
# Explicitly opt in to modern CMake behaviors to avoid warnings with recent
# versions of CMake.
cmake_policy(SET CMP0063 NEW)
# Load bundled libraries from the lib/ directory relative to the binary.
set(CMAKE_INSTALL_RPATH "$ORIGIN/lib")
# Root filesystem for cross-building.
if(FLUTTER_TARGET_PLATFORM_SYSROOT)
set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT})
set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT})
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
endif()
# Define build configuration options.
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
set(CMAKE_BUILD_TYPE "Debug" CACHE
STRING "Flutter build mode" FORCE)
set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS
"Debug" "Profile" "Release")
endif()
# Compilation settings that should be applied to most targets.
#
# Be cautious about adding new options here, as plugins use this function by
# default. In most cases, you should add new options to specific targets instead
# of modifying this function.
function(APPLY_STANDARD_SETTINGS TARGET)
target_compile_features(${TARGET} PUBLIC cxx_std_14)
target_compile_options(${TARGET} PRIVATE -Wall -Werror)
target_compile_options(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:-O3>")
target_compile_definitions(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:NDEBUG>")
endfunction()
# Flutter library and tool build rules.
set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter")
add_subdirectory(${FLUTTER_MANAGED_DIR})
# System-level dependencies.
find_package(PkgConfig REQUIRED)
pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
# Application build; see runner/CMakeLists.txt.
add_subdirectory("runner")
# Run the Flutter tool portions of the build. This must not be removed.
add_dependencies(${BINARY_NAME} flutter_assemble)
# Only the install-generated bundle's copy of the executable will launch
# correctly, since the resources must in the right relative locations. To avoid
# people trying to run the unbundled copy, put it in a subdirectory instead of
# the default top-level location.
set_target_properties(${BINARY_NAME}
PROPERTIES
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run"
)
# Generated plugin build rules, which manage building the plugins and adding
# them to the application.
include(flutter/generated_plugins.cmake)
# === Installation ===
# By default, "installing" just makes a relocatable bundle in the build
# directory.
set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle")
if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE)
endif()
# Start with a clean build bundle directory every time.
install(CODE "
file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\")
" COMPONENT Runtime)
set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data")
set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib")
install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}"
COMPONENT Runtime)
install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
COMPONENT Runtime)
install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES})
install(FILES "${bundled_library}"
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
endforeach(bundled_library)
# Copy the native assets provided by the build.dart from all packages.
set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/linux/")
install(DIRECTORY "${NATIVE_ASSETS_DIR}"
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
# Fully re-copy the assets directory on each build to avoid having stale files
# from a previous install.
set(FLUTTER_ASSET_DIR_NAME "flutter_assets")
install(CODE "
file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\")
" COMPONENT Runtime)
install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}"
DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime)
# Install the AOT library on non-Debug builds only.
if(NOT CMAKE_BUILD_TYPE MATCHES "Debug")
install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
endif()

View File

@@ -0,0 +1,88 @@
# This file controls Flutter-level build steps. It should not be edited.
cmake_minimum_required(VERSION 3.10)
set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral")
# Configuration provided via flutter tool.
include(${EPHEMERAL_DIR}/generated_config.cmake)
# TODO: Move the rest of this into files in ephemeral. See
# https://github.com/flutter/flutter/issues/57146.
# Serves the same purpose as list(TRANSFORM ... PREPEND ...),
# which isn't available in 3.10.
function(list_prepend LIST_NAME PREFIX)
set(NEW_LIST "")
foreach(element ${${LIST_NAME}})
list(APPEND NEW_LIST "${PREFIX}${element}")
endforeach(element)
set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE)
endfunction()
# === Flutter Library ===
# System-level dependencies.
find_package(PkgConfig REQUIRED)
pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0)
pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0)
set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so")
# Published to parent scope for install step.
set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE)
set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE)
set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE)
set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE)
list(APPEND FLUTTER_LIBRARY_HEADERS
"fl_basic_message_channel.h"
"fl_binary_codec.h"
"fl_binary_messenger.h"
"fl_dart_project.h"
"fl_engine.h"
"fl_json_message_codec.h"
"fl_json_method_codec.h"
"fl_message_codec.h"
"fl_method_call.h"
"fl_method_channel.h"
"fl_method_codec.h"
"fl_method_response.h"
"fl_plugin_registrar.h"
"fl_plugin_registry.h"
"fl_standard_message_codec.h"
"fl_standard_method_codec.h"
"fl_string_codec.h"
"fl_value.h"
"fl_view.h"
"flutter_linux.h"
)
list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/")
add_library(flutter INTERFACE)
target_include_directories(flutter INTERFACE
"${EPHEMERAL_DIR}"
)
target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}")
target_link_libraries(flutter INTERFACE
PkgConfig::GTK
PkgConfig::GLIB
PkgConfig::GIO
)
add_dependencies(flutter flutter_assemble)
# === Flutter tool backend ===
# _phony_ is a non-existent file to force this command to run every time,
# since currently there's no way to get a full input/output list from the
# flutter tool.
add_custom_command(
OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS}
${CMAKE_CURRENT_BINARY_DIR}/_phony_
COMMAND ${CMAKE_COMMAND} -E env
${FLUTTER_TOOL_ENVIRONMENT}
"${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh"
${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE}
VERBATIM
)
add_custom_target(flutter_assemble DEPENDS
"${FLUTTER_LIBRARY}"
${FLUTTER_LIBRARY_HEADERS}
)

View File

@@ -0,0 +1,27 @@
//
// Generated file. Do not edit.
//
// clang-format off
#include "generated_plugin_registrant.h"
#include <local_notifier/local_notifier_plugin.h>
#include <screen_retriever_linux/screen_retriever_linux_plugin.h>
#include <tray_manager/tray_manager_plugin.h>
#include <window_manager/window_manager_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) local_notifier_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "LocalNotifierPlugin");
local_notifier_plugin_register_with_registrar(local_notifier_registrar);
g_autoptr(FlPluginRegistrar) screen_retriever_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "ScreenRetrieverLinuxPlugin");
screen_retriever_linux_plugin_register_with_registrar(screen_retriever_linux_registrar);
g_autoptr(FlPluginRegistrar) tray_manager_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "TrayManagerPlugin");
tray_manager_plugin_register_with_registrar(tray_manager_registrar);
g_autoptr(FlPluginRegistrar) window_manager_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "WindowManagerPlugin");
window_manager_plugin_register_with_registrar(window_manager_registrar);
}

View File

@@ -0,0 +1,15 @@
//
// Generated file. Do not edit.
//
// clang-format off
#ifndef GENERATED_PLUGIN_REGISTRANT_
#define GENERATED_PLUGIN_REGISTRANT_
#include <flutter_linux/flutter_linux.h>
// Registers Flutter plugins.
void fl_register_plugins(FlPluginRegistry* registry);
#endif // GENERATED_PLUGIN_REGISTRANT_

View File

@@ -0,0 +1,27 @@
#
# Generated file, do not edit.
#
list(APPEND FLUTTER_PLUGIN_LIST
local_notifier
screen_retriever_linux
tray_manager
window_manager
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST
)
set(PLUGIN_BUNDLED_LIBRARIES)
foreach(plugin ${FLUTTER_PLUGIN_LIST})
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin})
target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin)
list(APPEND PLUGIN_BUNDLED_LIBRARIES $<TARGET_FILE:${plugin}_plugin>)
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries})
endforeach(plugin)
foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST})
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin})
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries})
endforeach(ffi_plugin)

View File

@@ -0,0 +1,26 @@
cmake_minimum_required(VERSION 3.13)
project(runner LANGUAGES CXX)
# Define the application target. To change its name, change BINARY_NAME in the
# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer
# work.
#
# Any new source files that you add to the application should be added here.
add_executable(${BINARY_NAME}
"main.cc"
"my_application.cc"
"${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc"
)
# Apply the standard set of build settings. This can be removed for applications
# that need different build settings.
apply_standard_settings(${BINARY_NAME})
# Add preprocessor definitions for the application ID.
add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}")
# Add dependency libraries. Add any application-specific dependencies here.
target_link_libraries(${BINARY_NAME} PRIVATE flutter)
target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK)
target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}")

View File

@@ -0,0 +1,6 @@
#include "my_application.h"
int main(int argc, char** argv) {
g_autoptr(MyApplication) app = my_application_new();
return g_application_run(G_APPLICATION(app), argc, argv);
}

View File

@@ -0,0 +1,148 @@
#include "my_application.h"
#include <flutter_linux/flutter_linux.h>
#ifdef GDK_WINDOWING_X11
#include <gdk/gdkx.h>
#endif
#include "flutter/generated_plugin_registrant.h"
struct _MyApplication {
GtkApplication parent_instance;
char** dart_entrypoint_arguments;
};
G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION)
// Called when first Flutter frame received.
static void first_frame_cb(MyApplication* self, FlView* view) {
gtk_widget_show(gtk_widget_get_toplevel(GTK_WIDGET(view)));
}
// Implements GApplication::activate.
static void my_application_activate(GApplication* application) {
MyApplication* self = MY_APPLICATION(application);
GtkWindow* window =
GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application)));
// Use a header bar when running in GNOME as this is the common style used
// by applications and is the setup most users will be using (e.g. Ubuntu
// desktop).
// If running on X and not using GNOME then just use a traditional title bar
// in case the window manager does more exotic layout, e.g. tiling.
// If running on Wayland assume the header bar will work (may need changing
// if future cases occur).
gboolean use_header_bar = TRUE;
#ifdef GDK_WINDOWING_X11
GdkScreen* screen = gtk_window_get_screen(window);
if (GDK_IS_X11_SCREEN(screen)) {
const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen);
if (g_strcmp0(wm_name, "GNOME Shell") != 0) {
use_header_bar = FALSE;
}
}
#endif
if (use_header_bar) {
GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new());
gtk_widget_show(GTK_WIDGET(header_bar));
gtk_header_bar_set_title(header_bar, "netbird_flutter_ui");
gtk_header_bar_set_show_close_button(header_bar, TRUE);
gtk_window_set_titlebar(window, GTK_WIDGET(header_bar));
} else {
gtk_window_set_title(window, "netbird_flutter_ui");
}
gtk_window_set_default_size(window, 1280, 720);
g_autoptr(FlDartProject) project = fl_dart_project_new();
fl_dart_project_set_dart_entrypoint_arguments(
project, self->dart_entrypoint_arguments);
FlView* view = fl_view_new(project);
GdkRGBA background_color;
// Background defaults to black, override it here if necessary, e.g. #00000000
// for transparent.
gdk_rgba_parse(&background_color, "#000000");
fl_view_set_background_color(view, &background_color);
gtk_widget_show(GTK_WIDGET(view));
gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view));
// Show the window when Flutter renders.
// Requires the view to be realized so we can start rendering.
g_signal_connect_swapped(view, "first-frame", G_CALLBACK(first_frame_cb),
self);
gtk_widget_realize(GTK_WIDGET(view));
fl_register_plugins(FL_PLUGIN_REGISTRY(view));
gtk_widget_grab_focus(GTK_WIDGET(view));
}
// Implements GApplication::local_command_line.
static gboolean my_application_local_command_line(GApplication* application,
gchar*** arguments,
int* exit_status) {
MyApplication* self = MY_APPLICATION(application);
// Strip out the first argument as it is the binary name.
self->dart_entrypoint_arguments = g_strdupv(*arguments + 1);
g_autoptr(GError) error = nullptr;
if (!g_application_register(application, nullptr, &error)) {
g_warning("Failed to register: %s", error->message);
*exit_status = 1;
return TRUE;
}
g_application_activate(application);
*exit_status = 0;
return TRUE;
}
// Implements GApplication::startup.
static void my_application_startup(GApplication* application) {
// MyApplication* self = MY_APPLICATION(object);
// Perform any actions required at application startup.
G_APPLICATION_CLASS(my_application_parent_class)->startup(application);
}
// Implements GApplication::shutdown.
static void my_application_shutdown(GApplication* application) {
// MyApplication* self = MY_APPLICATION(object);
// Perform any actions required at application shutdown.
G_APPLICATION_CLASS(my_application_parent_class)->shutdown(application);
}
// Implements GObject::dispose.
static void my_application_dispose(GObject* object) {
MyApplication* self = MY_APPLICATION(object);
g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev);
G_OBJECT_CLASS(my_application_parent_class)->dispose(object);
}
static void my_application_class_init(MyApplicationClass* klass) {
G_APPLICATION_CLASS(klass)->activate = my_application_activate;
G_APPLICATION_CLASS(klass)->local_command_line =
my_application_local_command_line;
G_APPLICATION_CLASS(klass)->startup = my_application_startup;
G_APPLICATION_CLASS(klass)->shutdown = my_application_shutdown;
G_OBJECT_CLASS(klass)->dispose = my_application_dispose;
}
static void my_application_init(MyApplication* self) {}
MyApplication* my_application_new() {
// Set the program name to the application ID, which helps various systems
// like GTK and desktop environments map this running application to its
// corresponding .desktop file. This ensures better integration by allowing
// the application to be recognized beyond its binary name.
g_set_prgname(APPLICATION_ID);
return MY_APPLICATION(g_object_new(my_application_get_type(),
"application-id", APPLICATION_ID, "flags",
G_APPLICATION_NON_UNIQUE, nullptr));
}

View File

@@ -0,0 +1,21 @@
#ifndef FLUTTER_MY_APPLICATION_H_
#define FLUTTER_MY_APPLICATION_H_
#include <gtk/gtk.h>
G_DECLARE_FINAL_TYPE(MyApplication,
my_application,
MY,
APPLICATION,
GtkApplication)
/**
* my_application_new:
*
* Creates a new Flutter-based application.
*
* Returns: a new #MyApplication.
*/
MyApplication* my_application_new();
#endif // FLUTTER_MY_APPLICATION_H_

7
client/flutter_ui/macos/.gitignore vendored Normal file
View File

@@ -0,0 +1,7 @@
# Flutter-related
**/Flutter/ephemeral/
**/Pods/
# Xcode-related
**/dgph
**/xcuserdata/

View File

@@ -0,0 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
#include "ephemeral/Flutter-Generated.xcconfig"

View File

@@ -0,0 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
#include "ephemeral/Flutter-Generated.xcconfig"

View File

@@ -0,0 +1,18 @@
//
// Generated file. Do not edit.
//
import FlutterMacOS
import Foundation
import local_notifier
import screen_retriever_macos
import tray_manager
import window_manager
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
LocalNotifierPlugin.register(with: registry.registrar(forPlugin: "LocalNotifierPlugin"))
ScreenRetrieverMacosPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverMacosPlugin"))
TrayManagerPlugin.register(with: registry.registrar(forPlugin: "TrayManagerPlugin"))
WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin"))
}

View File

@@ -0,0 +1,42 @@
platform :osx, '10.15'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
project 'Runner', {
'Debug' => :debug,
'Profile' => :release,
'Release' => :release,
}
def flutter_root
generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__)
unless File.exist?(generated_xcode_build_settings_path)
raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first"
end
File.foreach(generated_xcode_build_settings_path) do |line|
matches = line.match(/FLUTTER_ROOT\=(.*)/)
return matches[1].strip if matches
end
raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\""
end
require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
flutter_macos_podfile_setup
target 'Runner' do
use_frameworks!
flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__))
target 'RunnerTests' do
inherit! :search_paths
end
end
post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_macos_build_settings(target)
end
end

View File

@@ -0,0 +1,40 @@
PODS:
- FlutterMacOS (1.0.0)
- local_notifier (0.1.0):
- FlutterMacOS
- screen_retriever_macos (0.0.1):
- FlutterMacOS
- tray_manager (0.0.1):
- FlutterMacOS
- window_manager (0.5.0):
- FlutterMacOS
DEPENDENCIES:
- FlutterMacOS (from `Flutter/ephemeral`)
- local_notifier (from `Flutter/ephemeral/.symlinks/plugins/local_notifier/macos`)
- screen_retriever_macos (from `Flutter/ephemeral/.symlinks/plugins/screen_retriever_macos/macos`)
- tray_manager (from `Flutter/ephemeral/.symlinks/plugins/tray_manager/macos`)
- window_manager (from `Flutter/ephemeral/.symlinks/plugins/window_manager/macos`)
EXTERNAL SOURCES:
FlutterMacOS:
:path: Flutter/ephemeral
local_notifier:
:path: Flutter/ephemeral/.symlinks/plugins/local_notifier/macos
screen_retriever_macos:
:path: Flutter/ephemeral/.symlinks/plugins/screen_retriever_macos/macos
tray_manager:
:path: Flutter/ephemeral/.symlinks/plugins/tray_manager/macos
window_manager:
:path: Flutter/ephemeral/.symlinks/plugins/window_manager/macos
SPEC CHECKSUMS:
FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1
local_notifier: ebf072651e35ae5e47280ad52e2707375cb2ae4e
screen_retriever_macos: 452e51764a9e1cdb74b3c541238795849f21557f
tray_manager: a104b5c81b578d83f3c3d0f40a997c8b10810166
window_manager: b729e31d38fb04905235df9ea896128991cad99e
PODFILE CHECKSUM: 54d867c82ac51cbd61b565781b9fada492027009
COCOAPODS: 1.16.2

View File

@@ -0,0 +1,801 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 54;
objects = {
/* Begin PBXAggregateTarget section */
33CC111A2044C6BA0003C045 /* Flutter Assemble */ = {
isa = PBXAggregateTarget;
buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */;
buildPhases = (
33CC111E2044C6BF0003C045 /* ShellScript */,
);
dependencies = (
);
name = "Flutter Assemble";
productName = FLX;
};
/* End PBXAggregateTarget section */
/* Begin PBXBuildFile section */
331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; };
335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; };
33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; };
33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; };
33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; };
33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; };
5F10F38F17483368E6B26C16 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2D1C698E330CDD6D9457E84F /* Pods_RunnerTests.framework */; };
6E2193E107D1C306C0B38295 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AA24562430C7E3798566E220 /* Pods_Runner.framework */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
331C80D9294CF71000263BE5 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 33CC10E52044A3C60003C045 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 33CC10EC2044A3C60003C045;
remoteInfo = Runner;
};
33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 33CC10E52044A3C60003C045 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 33CC111A2044C6BA0003C045;
remoteInfo = FLX;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
33CC110E2044A8840003C045 /* Bundle Framework */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
);
name = "Bundle Framework";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
14CA49126DC810A7FD8021C0 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = "<group>"; };
2D1C698E330CDD6D9457E84F /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = "<group>"; };
335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = "<group>"; };
33CC10ED2044A3C60003C045 /* netbird_flutter_ui.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = netbird_flutter_ui.app; sourceTree = BUILT_PRODUCTS_DIR; };
33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = "<group>"; };
33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = "<group>"; };
33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = "<group>"; };
33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = "<group>"; };
33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = "<group>"; };
33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = "<group>"; };
33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = "<group>"; };
33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = "<group>"; };
33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = "<group>"; };
33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = "<group>"; };
3B081925C026B73446CD514F /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
5344037698CB477EF6AE75A3 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = "<group>"; };
97BFF106FF1D50C0EF3C4AF6 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = "<group>"; };
AA24562430C7E3798566E220 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
E69F59E3113C82C71F7A2757 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
EB350F2E61DA77DD3D20E0EB /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
331C80D2294CF70F00263BE5 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
5F10F38F17483368E6B26C16 /* Pods_RunnerTests.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
33CC10EA2044A3C60003C045 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
6E2193E107D1C306C0B38295 /* Pods_Runner.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
16123F31EB7196617B509F9C /* Pods */ = {
isa = PBXGroup;
children = (
5344037698CB477EF6AE75A3 /* Pods-Runner.debug.xcconfig */,
E69F59E3113C82C71F7A2757 /* Pods-Runner.release.xcconfig */,
EB350F2E61DA77DD3D20E0EB /* Pods-Runner.profile.xcconfig */,
97BFF106FF1D50C0EF3C4AF6 /* Pods-RunnerTests.debug.xcconfig */,
3B081925C026B73446CD514F /* Pods-RunnerTests.release.xcconfig */,
14CA49126DC810A7FD8021C0 /* Pods-RunnerTests.profile.xcconfig */,
);
name = Pods;
path = Pods;
sourceTree = "<group>";
};
331C80D6294CF71000263BE5 /* RunnerTests */ = {
isa = PBXGroup;
children = (
331C80D7294CF71000263BE5 /* RunnerTests.swift */,
);
path = RunnerTests;
sourceTree = "<group>";
};
33BA886A226E78AF003329D5 /* Configs */ = {
isa = PBXGroup;
children = (
33E5194F232828860026EE4D /* AppInfo.xcconfig */,
9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
333000ED22D3DE5D00554162 /* Warnings.xcconfig */,
);
path = Configs;
sourceTree = "<group>";
};
33CC10E42044A3C60003C045 = {
isa = PBXGroup;
children = (
33FAB671232836740065AC1E /* Runner */,
33CEB47122A05771004F2AC0 /* Flutter */,
331C80D6294CF71000263BE5 /* RunnerTests */,
33CC10EE2044A3C60003C045 /* Products */,
D73912EC22F37F3D000D13A0 /* Frameworks */,
16123F31EB7196617B509F9C /* Pods */,
);
sourceTree = "<group>";
};
33CC10EE2044A3C60003C045 /* Products */ = {
isa = PBXGroup;
children = (
33CC10ED2044A3C60003C045 /* netbird_flutter_ui.app */,
331C80D5294CF71000263BE5 /* RunnerTests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
33CC11242044D66E0003C045 /* Resources */ = {
isa = PBXGroup;
children = (
33CC10F22044A3C60003C045 /* Assets.xcassets */,
33CC10F42044A3C60003C045 /* MainMenu.xib */,
33CC10F72044A3C60003C045 /* Info.plist */,
);
name = Resources;
path = ..;
sourceTree = "<group>";
};
33CEB47122A05771004F2AC0 /* Flutter */ = {
isa = PBXGroup;
children = (
335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */,
33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */,
33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */,
33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */,
);
path = Flutter;
sourceTree = "<group>";
};
33FAB671232836740065AC1E /* Runner */ = {
isa = PBXGroup;
children = (
33CC10F02044A3C60003C045 /* AppDelegate.swift */,
33CC11122044BFA00003C045 /* MainFlutterWindow.swift */,
33E51913231747F40026EE4D /* DebugProfile.entitlements */,
33E51914231749380026EE4D /* Release.entitlements */,
33CC11242044D66E0003C045 /* Resources */,
33BA886A226E78AF003329D5 /* Configs */,
);
path = Runner;
sourceTree = "<group>";
};
D73912EC22F37F3D000D13A0 /* Frameworks */ = {
isa = PBXGroup;
children = (
AA24562430C7E3798566E220 /* Pods_Runner.framework */,
2D1C698E330CDD6D9457E84F /* Pods_RunnerTests.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
331C80D4294CF70F00263BE5 /* RunnerTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
buildPhases = (
13F875F4B0174355038870C8 /* [CP] Check Pods Manifest.lock */,
331C80D1294CF70F00263BE5 /* Sources */,
331C80D2294CF70F00263BE5 /* Frameworks */,
331C80D3294CF70F00263BE5 /* Resources */,
);
buildRules = (
);
dependencies = (
331C80DA294CF71000263BE5 /* PBXTargetDependency */,
);
name = RunnerTests;
productName = RunnerTests;
productReference = 331C80D5294CF71000263BE5 /* RunnerTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
33CC10EC2044A3C60003C045 /* Runner */ = {
isa = PBXNativeTarget;
buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
6D776BDDAB33DFA32528CFE2 /* [CP] Check Pods Manifest.lock */,
33CC10E92044A3C60003C045 /* Sources */,
33CC10EA2044A3C60003C045 /* Frameworks */,
33CC10EB2044A3C60003C045 /* Resources */,
33CC110E2044A8840003C045 /* Bundle Framework */,
3399D490228B24CF009A79C7 /* ShellScript */,
DF9F03510A6543FA652C823E /* [CP] Embed Pods Frameworks */,
);
buildRules = (
);
dependencies = (
33CC11202044C79F0003C045 /* PBXTargetDependency */,
);
name = Runner;
productName = Runner;
productReference = 33CC10ED2044A3C60003C045 /* netbird_flutter_ui.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
33CC10E52044A3C60003C045 /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = YES;
LastSwiftUpdateCheck = 0920;
LastUpgradeCheck = 1510;
ORGANIZATIONNAME = "";
TargetAttributes = {
331C80D4294CF70F00263BE5 = {
CreatedOnToolsVersion = 14.0;
TestTargetID = 33CC10EC2044A3C60003C045;
};
33CC10EC2044A3C60003C045 = {
CreatedOnToolsVersion = 9.2;
LastSwiftMigration = 1100;
ProvisioningStyle = Automatic;
SystemCapabilities = {
com.apple.Sandbox = {
enabled = 1;
};
};
};
33CC111A2044C6BA0003C045 = {
CreatedOnToolsVersion = 9.2;
ProvisioningStyle = Manual;
};
};
};
buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */;
compatibilityVersion = "Xcode 9.3";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 33CC10E42044A3C60003C045;
productRefGroup = 33CC10EE2044A3C60003C045 /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
33CC10EC2044A3C60003C045 /* Runner */,
331C80D4294CF70F00263BE5 /* RunnerTests */,
33CC111A2044C6BA0003C045 /* Flutter Assemble */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
331C80D3294CF70F00263BE5 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
33CC10EB2044A3C60003C045 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */,
33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
13F875F4B0174355038870C8 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
3399D490228B24CF009A79C7 /* ShellScript */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
);
outputFileListPaths = (
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n";
};
33CC111E2044C6BF0003C045 /* ShellScript */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
Flutter/ephemeral/FlutterInputs.xcfilelist,
);
inputPaths = (
Flutter/ephemeral/tripwire,
);
outputFileListPaths = (
Flutter/ephemeral/FlutterOutputs.xcfilelist,
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire";
};
6D776BDDAB33DFA32528CFE2 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
DF9F03510A6543FA652C823E /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
331C80D1294CF70F00263BE5 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
33CC10E92044A3C60003C045 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */,
33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */,
335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
331C80DA294CF71000263BE5 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 33CC10EC2044A3C60003C045 /* Runner */;
targetProxy = 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */;
};
33CC11202044C79F0003C045 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */;
targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
33CC10F42044A3C60003C045 /* MainMenu.xib */ = {
isa = PBXVariantGroup;
children = (
33CC10F52044A3C60003C045 /* Base */,
);
name = MainMenu.xib;
path = Runner;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
331C80DB294CF71000263BE5 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 97BFF106FF1D50C0EF3C4AF6 /* Pods-RunnerTests.debug.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = io.netbird.netbirdFlutterUi.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/netbird_flutter_ui.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/netbird_flutter_ui";
};
name = Debug;
};
331C80DC294CF71000263BE5 /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 3B081925C026B73446CD514F /* Pods-RunnerTests.release.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = io.netbird.netbirdFlutterUi.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/netbird_flutter_ui.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/netbird_flutter_ui";
};
name = Release;
};
331C80DD294CF71000263BE5 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 14CA49126DC810A7FD8021C0 /* Pods-RunnerTests.profile.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = io.netbird.netbirdFlutterUi.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/netbird_flutter_ui.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/netbird_flutter_ui";
};
name = Profile;
};
338D0CE9231458BD00FA5F75 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CODE_SIGN_IDENTITY = "-";
COPY_PHASE_STRIP = NO;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.15;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = macosx;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
};
name = Profile;
};
338D0CEA231458BD00FA5F75 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_VERSION = 5.0;
};
name = Profile;
};
338D0CEB231458BD00FA5F75 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_STYLE = Manual;
PRODUCT_NAME = "$(TARGET_NAME)";
};
name = Profile;
};
33CC10F92044A3C60003C045 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CODE_SIGN_IDENTITY = "-";
COPY_PHASE_STRIP = NO;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.15;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = macosx;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
};
name = Debug;
};
33CC10FA2044A3C60003C045 /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CODE_SIGN_IDENTITY = "-";
COPY_PHASE_STRIP = NO;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.15;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = macosx;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
};
name = Release;
};
33CC10FC2044A3C60003C045 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
};
name = Debug;
};
33CC10FD2044A3C60003C045 /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_VERSION = 5.0;
};
name = Release;
};
33CC111C2044C6BA0003C045 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_STYLE = Manual;
PRODUCT_NAME = "$(TARGET_NAME)";
};
name = Debug;
};
33CC111D2044C6BA0003C045 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_STYLE = Automatic;
PRODUCT_NAME = "$(TARGET_NAME)";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
331C80DB294CF71000263BE5 /* Debug */,
331C80DC294CF71000263BE5 /* Release */,
331C80DD294CF71000263BE5 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
33CC10F92044A3C60003C045 /* Debug */,
33CC10FA2044A3C60003C045 /* Release */,
338D0CE9231458BD00FA5F75 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
33CC10FC2044A3C60003C045 /* Debug */,
33CC10FD2044A3C60003C045 /* Release */,
338D0CEA231458BD00FA5F75 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = {
isa = XCConfigurationList;
buildConfigurations = (
33CC111C2044C6BA0003C045 /* Debug */,
33CC111D2044C6BA0003C045 /* Release */,
338D0CEB231458BD00FA5F75 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 33CC10E52044A3C60003C045 /* Project object */;
}

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@@ -0,0 +1,99 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1510"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
BuildableName = "netbird_flutter_ui.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
BuildableName = "netbird_flutter_ui.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</MacroExpansion>
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "331C80D4294CF70F00263BE5"
BuildableName = "RunnerTests.xctest"
BlueprintName = "RunnerTests"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
enableGPUValidationMode = "1"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
BuildableName = "netbird_flutter_ui.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Profile"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
BuildableName = "netbird_flutter_ui.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
<FileRef
location = "group:Pods/Pods.xcodeproj">
</FileRef>
</Workspace>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@@ -0,0 +1,13 @@
import Cocoa
import FlutterMacOS
@main
class AppDelegate: FlutterAppDelegate {
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
return true
}
override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
return true
}
}

View File

@@ -0,0 +1,68 @@
{
"images" : [
{
"size" : "16x16",
"idiom" : "mac",
"filename" : "app_icon_16.png",
"scale" : "1x"
},
{
"size" : "16x16",
"idiom" : "mac",
"filename" : "app_icon_32.png",
"scale" : "2x"
},
{
"size" : "32x32",
"idiom" : "mac",
"filename" : "app_icon_32.png",
"scale" : "1x"
},
{
"size" : "32x32",
"idiom" : "mac",
"filename" : "app_icon_64.png",
"scale" : "2x"
},
{
"size" : "128x128",
"idiom" : "mac",
"filename" : "app_icon_128.png",
"scale" : "1x"
},
{
"size" : "128x128",
"idiom" : "mac",
"filename" : "app_icon_256.png",
"scale" : "2x"
},
{
"size" : "256x256",
"idiom" : "mac",
"filename" : "app_icon_256.png",
"scale" : "1x"
},
{
"size" : "256x256",
"idiom" : "mac",
"filename" : "app_icon_512.png",
"scale" : "2x"
},
{
"size" : "512x512",
"idiom" : "mac",
"filename" : "app_icon_512.png",
"scale" : "1x"
},
{
"size" : "512x512",
"idiom" : "mac",
"filename" : "app_icon_1024.png",
"scale" : "2x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 520 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@@ -0,0 +1,343 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="14490.70" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="14490.70"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<customObject id="-2" userLabel="File's Owner" customClass="NSApplication">
<connections>
<outlet property="delegate" destination="Voe-Tx-rLC" id="GzC-gU-4Uq"/>
</connections>
</customObject>
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
<customObject id="Voe-Tx-rLC" customClass="AppDelegate" customModule="Runner" customModuleProvider="target">
<connections>
<outlet property="applicationMenu" destination="uQy-DD-JDr" id="XBo-yE-nKs"/>
<outlet property="mainFlutterWindow" destination="QvC-M9-y7g" id="gIp-Ho-8D9"/>
</connections>
</customObject>
<customObject id="YLy-65-1bz" customClass="NSFontManager"/>
<menu title="Main Menu" systemMenu="main" id="AYu-sK-qS6">
<items>
<menuItem title="APP_NAME" id="1Xt-HY-uBw">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="APP_NAME" systemMenu="apple" id="uQy-DD-JDr">
<items>
<menuItem title="About APP_NAME" id="5kV-Vb-QxS">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="orderFrontStandardAboutPanel:" target="-1" id="Exp-CZ-Vem"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="VOq-y0-SEH"/>
<menuItem title="Preferences…" keyEquivalent="," id="BOF-NM-1cW"/>
<menuItem isSeparatorItem="YES" id="wFC-TO-SCJ"/>
<menuItem title="Services" id="NMo-om-nkz">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Services" systemMenu="services" id="hz9-B4-Xy5"/>
</menuItem>
<menuItem isSeparatorItem="YES" id="4je-JR-u6R"/>
<menuItem title="Hide APP_NAME" keyEquivalent="h" id="Olw-nP-bQN">
<connections>
<action selector="hide:" target="-1" id="PnN-Uc-m68"/>
</connections>
</menuItem>
<menuItem title="Hide Others" keyEquivalent="h" id="Vdr-fp-XzO">
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
<connections>
<action selector="hideOtherApplications:" target="-1" id="VT4-aY-XCT"/>
</connections>
</menuItem>
<menuItem title="Show All" id="Kd2-mp-pUS">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="unhideAllApplications:" target="-1" id="Dhg-Le-xox"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="kCx-OE-vgT"/>
<menuItem title="Quit APP_NAME" keyEquivalent="q" id="4sb-4s-VLi">
<connections>
<action selector="terminate:" target="-1" id="Te7-pn-YzF"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Edit" id="5QF-Oa-p0T">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Edit" id="W48-6f-4Dl">
<items>
<menuItem title="Undo" keyEquivalent="z" id="dRJ-4n-Yzg">
<connections>
<action selector="undo:" target="-1" id="M6e-cu-g7V"/>
</connections>
</menuItem>
<menuItem title="Redo" keyEquivalent="Z" id="6dh-zS-Vam">
<connections>
<action selector="redo:" target="-1" id="oIA-Rs-6OD"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="WRV-NI-Exz"/>
<menuItem title="Cut" keyEquivalent="x" id="uRl-iY-unG">
<connections>
<action selector="cut:" target="-1" id="YJe-68-I9s"/>
</connections>
</menuItem>
<menuItem title="Copy" keyEquivalent="c" id="x3v-GG-iWU">
<connections>
<action selector="copy:" target="-1" id="G1f-GL-Joy"/>
</connections>
</menuItem>
<menuItem title="Paste" keyEquivalent="v" id="gVA-U4-sdL">
<connections>
<action selector="paste:" target="-1" id="UvS-8e-Qdg"/>
</connections>
</menuItem>
<menuItem title="Paste and Match Style" keyEquivalent="V" id="WeT-3V-zwk">
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
<connections>
<action selector="pasteAsPlainText:" target="-1" id="cEh-KX-wJQ"/>
</connections>
</menuItem>
<menuItem title="Delete" id="pa3-QI-u2k">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="delete:" target="-1" id="0Mk-Ml-PaM"/>
</connections>
</menuItem>
<menuItem title="Select All" keyEquivalent="a" id="Ruw-6m-B2m">
<connections>
<action selector="selectAll:" target="-1" id="VNm-Mi-diN"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="uyl-h8-XO2"/>
<menuItem title="Find" id="4EN-yA-p0u">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Find" id="1b7-l0-nxx">
<items>
<menuItem title="Find…" tag="1" keyEquivalent="f" id="Xz5-n4-O0W">
<connections>
<action selector="performFindPanelAction:" target="-1" id="cD7-Qs-BN4"/>
</connections>
</menuItem>
<menuItem title="Find and Replace…" tag="12" keyEquivalent="f" id="YEy-JH-Tfz">
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
<connections>
<action selector="performFindPanelAction:" target="-1" id="WD3-Gg-5AJ"/>
</connections>
</menuItem>
<menuItem title="Find Next" tag="2" keyEquivalent="g" id="q09-fT-Sye">
<connections>
<action selector="performFindPanelAction:" target="-1" id="NDo-RZ-v9R"/>
</connections>
</menuItem>
<menuItem title="Find Previous" tag="3" keyEquivalent="G" id="OwM-mh-QMV">
<connections>
<action selector="performFindPanelAction:" target="-1" id="HOh-sY-3ay"/>
</connections>
</menuItem>
<menuItem title="Use Selection for Find" tag="7" keyEquivalent="e" id="buJ-ug-pKt">
<connections>
<action selector="performFindPanelAction:" target="-1" id="U76-nv-p5D"/>
</connections>
</menuItem>
<menuItem title="Jump to Selection" keyEquivalent="j" id="S0p-oC-mLd">
<connections>
<action selector="centerSelectionInVisibleArea:" target="-1" id="IOG-6D-g5B"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Spelling and Grammar" id="Dv1-io-Yv7">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Spelling" id="3IN-sU-3Bg">
<items>
<menuItem title="Show Spelling and Grammar" keyEquivalent=":" id="HFo-cy-zxI">
<connections>
<action selector="showGuessPanel:" target="-1" id="vFj-Ks-hy3"/>
</connections>
</menuItem>
<menuItem title="Check Document Now" keyEquivalent=";" id="hz2-CU-CR7">
<connections>
<action selector="checkSpelling:" target="-1" id="fz7-VC-reM"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="bNw-od-mp5"/>
<menuItem title="Check Spelling While Typing" id="rbD-Rh-wIN">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleContinuousSpellChecking:" target="-1" id="7w6-Qz-0kB"/>
</connections>
</menuItem>
<menuItem title="Check Grammar With Spelling" id="mK6-2p-4JG">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleGrammarChecking:" target="-1" id="muD-Qn-j4w"/>
</connections>
</menuItem>
<menuItem title="Correct Spelling Automatically" id="78Y-hA-62v">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleAutomaticSpellingCorrection:" target="-1" id="2lM-Qi-WAP"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Substitutions" id="9ic-FL-obx">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Substitutions" id="FeM-D8-WVr">
<items>
<menuItem title="Show Substitutions" id="z6F-FW-3nz">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="orderFrontSubstitutionsPanel:" target="-1" id="oku-mr-iSq"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="gPx-C9-uUO"/>
<menuItem title="Smart Copy/Paste" id="9yt-4B-nSM">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleSmartInsertDelete:" target="-1" id="3IJ-Se-DZD"/>
</connections>
</menuItem>
<menuItem title="Smart Quotes" id="hQb-2v-fYv">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleAutomaticQuoteSubstitution:" target="-1" id="ptq-xd-QOA"/>
</connections>
</menuItem>
<menuItem title="Smart Dashes" id="rgM-f4-ycn">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleAutomaticDashSubstitution:" target="-1" id="oCt-pO-9gS"/>
</connections>
</menuItem>
<menuItem title="Smart Links" id="cwL-P1-jid">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleAutomaticLinkDetection:" target="-1" id="Gip-E3-Fov"/>
</connections>
</menuItem>
<menuItem title="Data Detectors" id="tRr-pd-1PS">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleAutomaticDataDetection:" target="-1" id="R1I-Nq-Kbl"/>
</connections>
</menuItem>
<menuItem title="Text Replacement" id="HFQ-gK-NFA">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleAutomaticTextReplacement:" target="-1" id="DvP-Fe-Py6"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Transformations" id="2oI-Rn-ZJC">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Transformations" id="c8a-y6-VQd">
<items>
<menuItem title="Make Upper Case" id="vmV-6d-7jI">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="uppercaseWord:" target="-1" id="sPh-Tk-edu"/>
</connections>
</menuItem>
<menuItem title="Make Lower Case" id="d9M-CD-aMd">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="lowercaseWord:" target="-1" id="iUZ-b5-hil"/>
</connections>
</menuItem>
<menuItem title="Capitalize" id="UEZ-Bs-lqG">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="capitalizeWord:" target="-1" id="26H-TL-nsh"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Speech" id="xrE-MZ-jX0">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Speech" id="3rS-ZA-NoH">
<items>
<menuItem title="Start Speaking" id="Ynk-f8-cLZ">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="startSpeaking:" target="-1" id="654-Ng-kyl"/>
</connections>
</menuItem>
<menuItem title="Stop Speaking" id="Oyz-dy-DGm">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="stopSpeaking:" target="-1" id="dX8-6p-jy9"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="View" id="H8h-7b-M4v">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="View" id="HyV-fh-RgO">
<items>
<menuItem title="Enter Full Screen" keyEquivalent="f" id="4J7-dP-txa">
<modifierMask key="keyEquivalentModifierMask" control="YES" command="YES"/>
<connections>
<action selector="toggleFullScreen:" target="-1" id="dU3-MA-1Rq"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Window" id="aUF-d1-5bR">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Window" systemMenu="window" id="Td7-aD-5lo">
<items>
<menuItem title="Minimize" keyEquivalent="m" id="OY7-WF-poV">
<connections>
<action selector="performMiniaturize:" target="-1" id="VwT-WD-YPe"/>
</connections>
</menuItem>
<menuItem title="Zoom" id="R4o-n2-Eq4">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="performZoom:" target="-1" id="DIl-cC-cCs"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="eu3-7i-yIM"/>
<menuItem title="Bring All to Front" id="LE2-aR-0XJ">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="arrangeInFront:" target="-1" id="DRN-fu-gQh"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Help" id="EPT-qC-fAb">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Help" systemMenu="help" id="rJ0-wn-3NY"/>
</menuItem>
</items>
<point key="canvasLocation" x="142" y="-258"/>
</menu>
<window title="APP_NAME" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" animationBehavior="default" id="QvC-M9-y7g" customClass="MainFlutterWindow" customModule="Runner" customModuleProvider="target">
<windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES" resizable="YES"/>
<rect key="contentRect" x="335" y="390" width="800" height="600"/>
<rect key="screenRect" x="0.0" y="0.0" width="2560" height="1577"/>
<view key="contentView" wantsLayer="YES" id="EiT-Mj-1SZ">
<rect key="frame" x="0.0" y="0.0" width="800" height="600"/>
<autoresizingMask key="autoresizingMask"/>
</view>
</window>
</objects>
</document>

View File

@@ -0,0 +1,14 @@
// Application-level settings for the Runner target.
//
// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the
// future. If not, the values below would default to using the project name when this becomes a
// 'flutter create' template.
// The application's name. By default this is also the title of the Flutter window.
PRODUCT_NAME = netbird_flutter_ui
// The application's bundle identifier
PRODUCT_BUNDLE_IDENTIFIER = io.netbird.netbirdFlutterUi
// The copyright displayed in application information
PRODUCT_COPYRIGHT = Copyright © 2026 io.netbird. All rights reserved.

View File

@@ -0,0 +1,2 @@
#include "../../Flutter/Flutter-Debug.xcconfig"
#include "Warnings.xcconfig"

View File

@@ -0,0 +1,2 @@
#include "../../Flutter/Flutter-Release.xcconfig"
#include "Warnings.xcconfig"

View File

@@ -0,0 +1,13 @@
WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings
GCC_WARN_UNDECLARED_SELECTOR = YES
CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES
CLANG_WARN_PRAGMA_PACK = YES
CLANG_WARN_STRICT_PROTOTYPES = YES
CLANG_WARN_COMMA = YES
GCC_WARN_STRICT_SELECTOR_MATCH = YES
CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES
GCC_WARN_SHADOW = YES
CLANG_WARN_UNREACHABLE_CODE = YES

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.network.server</key>
<true/>
</dict>
</plist>

View File

@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIconFile</key>
<string></string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSMinimumSystemVersion</key>
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
<key>NSHumanReadableCopyright</key>
<string>$(PRODUCT_COPYRIGHT)</string>
<key>NSMainNibFile</key>
<string>MainMenu</string>
<key>NSPrincipalClass</key>
<string>NSApplication</string>
</dict>
</plist>

View File

@@ -0,0 +1,15 @@
import Cocoa
import FlutterMacOS
class MainFlutterWindow: NSWindow {
override func awakeFromNib() {
let flutterViewController = FlutterViewController()
let windowFrame = self.frame
self.contentViewController = flutterViewController
self.setFrame(windowFrame, display: true)
RegisterGeneratedPlugins(registry: flutterViewController)
super.awakeFromNib()
}
}

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.network.server</key>
<true/>
</dict>
</plist>

View File

@@ -0,0 +1,12 @@
import Cocoa
import FlutterMacOS
import XCTest
class RunnerTests: XCTestCase {
func testExample() {
// If you add code to the Runner application, consider adding tests here.
// See https://developer.apple.com/documentation/xctest for more information about using XCTest.
}
}

View File

@@ -0,0 +1,413 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
async:
dependency: transitive
description:
name: async
sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37
url: "https://pub.dev"
source: hosted
version: "2.13.1"
boolean_selector:
dependency: transitive
description:
name: boolean_selector
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
characters:
dependency: transitive
description:
name: characters
sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
url: "https://pub.dev"
source: hosted
version: "1.4.1"
clock:
dependency: transitive
description:
name: clock
sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
url: "https://pub.dev"
source: hosted
version: "1.1.2"
collection:
dependency: transitive
description:
name: collection
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
url: "https://pub.dev"
source: hosted
version: "1.19.1"
crypto:
dependency: transitive
description:
name: crypto
sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
url: "https://pub.dev"
source: hosted
version: "3.0.7"
fake_async:
dependency: transitive
description:
name: fake_async
sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
url: "https://pub.dev"
source: hosted
version: "1.3.3"
fixnum:
dependency: "direct main"
description:
name: fixnum
sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be
url: "https://pub.dev"
source: hosted
version: "1.1.1"
flutter:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_test:
dependency: "direct dev"
description: flutter
source: sdk
version: "0.0.0"
google_cloud:
dependency: transitive
description:
name: google_cloud
sha256: fbcde933b2d8600c3cdb2328f8f4c47628ec29a39e9cef85dee535c7868993c4
url: "https://pub.dev"
source: hosted
version: "0.4.1"
google_identity_services_web:
dependency: transitive
description:
name: google_identity_services_web
sha256: "5d187c46dc59e02646e10fe82665fc3884a9b71bc1c90c2b8b749316d33ee454"
url: "https://pub.dev"
source: hosted
version: "0.3.3+1"
googleapis_auth:
dependency: transitive
description:
name: googleapis_auth
sha256: "661738b763d3e524de69df53bf4e03943e4e01e98265cebcc6684871b06a5379"
url: "https://pub.dev"
source: hosted
version: "2.3.0"
grpc:
dependency: "direct main"
description:
name: grpc
sha256: "86be3a7d39ad865b214a7370021ac80e68939238b507730de6d97fc662cb2723"
url: "https://pub.dev"
source: hosted
version: "5.1.0"
http:
dependency: transitive
description:
name: http
sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412"
url: "https://pub.dev"
source: hosted
version: "1.6.0"
http2:
dependency: transitive
description:
name: http2
sha256: "382d3aefc5bd6dc68c6b892d7664f29b5beb3251611ae946a98d35158a82bbfa"
url: "https://pub.dev"
source: hosted
version: "2.3.1"
http_parser:
dependency: transitive
description:
name: http_parser
sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
url: "https://pub.dev"
source: hosted
version: "4.1.2"
io:
dependency: transitive
description:
name: io
sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b
url: "https://pub.dev"
source: hosted
version: "1.0.5"
json_annotation:
dependency: transitive
description:
name: json_annotation
sha256: cb09e7dac6210041fad964ed7fbee004f14258b4eca4040f72d1234062ace4c8
url: "https://pub.dev"
source: hosted
version: "4.11.0"
leak_tracker:
dependency: transitive
description:
name: leak_tracker
sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
url: "https://pub.dev"
source: hosted
version: "11.0.2"
leak_tracker_flutter_testing:
dependency: transitive
description:
name: leak_tracker_flutter_testing
sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
url: "https://pub.dev"
source: hosted
version: "3.0.10"
leak_tracker_testing:
dependency: transitive
description:
name: leak_tracker_testing
sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
lints:
dependency: "direct dev"
description:
name: lints
sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df"
url: "https://pub.dev"
source: hosted
version: "6.1.0"
local_notifier:
dependency: "direct main"
description:
name: local_notifier
sha256: f6cfc933c6fbc961f4e52b5c880f68e41b2d3cd29aad557cc654fd211093a025
url: "https://pub.dev"
source: hosted
version: "0.1.6"
matcher:
dependency: transitive
description:
name: matcher
sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
url: "https://pub.dev"
source: hosted
version: "0.12.19"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
url: "https://pub.dev"
source: hosted
version: "0.13.0"
menu_base:
dependency: transitive
description:
name: menu_base
sha256: "820368014a171bd1241030278e6c2617354f492f5c703d7b7d4570a6b8b84405"
url: "https://pub.dev"
source: hosted
version: "0.1.1"
meta:
dependency: transitive
description:
name: meta
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
url: "https://pub.dev"
source: hosted
version: "1.17.0"
path:
dependency: transitive
description:
name: path
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
url: "https://pub.dev"
source: hosted
version: "1.9.1"
plugin_platform_interface:
dependency: transitive
description:
name: plugin_platform_interface
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
url: "https://pub.dev"
source: hosted
version: "2.1.8"
protobuf:
dependency: "direct main"
description:
name: protobuf
sha256: "75ec242d22e950bdcc79ee38dd520ce4ee0bc491d7fadc4ea47694604d22bf06"
url: "https://pub.dev"
source: hosted
version: "6.0.0"
screen_retriever:
dependency: transitive
description:
name: screen_retriever
sha256: "570dbc8e4f70bac451e0efc9c9bb19fa2d6799a11e6ef04f946d7886d2e23d0c"
url: "https://pub.dev"
source: hosted
version: "0.2.0"
screen_retriever_linux:
dependency: transitive
description:
name: screen_retriever_linux
sha256: f7f8120c92ef0784e58491ab664d01efda79a922b025ff286e29aa123ea3dd18
url: "https://pub.dev"
source: hosted
version: "0.2.0"
screen_retriever_macos:
dependency: transitive
description:
name: screen_retriever_macos
sha256: "71f956e65c97315dd661d71f828708bd97b6d358e776f1a30d5aa7d22d78a149"
url: "https://pub.dev"
source: hosted
version: "0.2.0"
screen_retriever_platform_interface:
dependency: transitive
description:
name: screen_retriever_platform_interface
sha256: ee197f4581ff0d5608587819af40490748e1e39e648d7680ecf95c05197240c0
url: "https://pub.dev"
source: hosted
version: "0.2.0"
screen_retriever_windows:
dependency: transitive
description:
name: screen_retriever_windows
sha256: "449ee257f03ca98a57288ee526a301a430a344a161f9202b4fcc38576716fe13"
url: "https://pub.dev"
source: hosted
version: "0.2.0"
shelf:
dependency: transitive
description:
name: shelf
sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12
url: "https://pub.dev"
source: hosted
version: "1.4.2"
shortid:
dependency: transitive
description:
name: shortid
sha256: d0b40e3dbb50497dad107e19c54ca7de0d1a274eb9b4404991e443dadb9ebedb
url: "https://pub.dev"
source: hosted
version: "0.1.2"
sky_engine:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
source_span:
dependency: transitive
description:
name: source_span
sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab"
url: "https://pub.dev"
source: hosted
version: "1.10.2"
stack_trace:
dependency: transitive
description:
name: stack_trace
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
url: "https://pub.dev"
source: hosted
version: "1.12.1"
stream_channel:
dependency: transitive
description:
name: stream_channel
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
url: "https://pub.dev"
source: hosted
version: "2.1.4"
string_scanner:
dependency: transitive
description:
name: string_scanner
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
url: "https://pub.dev"
source: hosted
version: "1.4.1"
term_glyph:
dependency: transitive
description:
name: term_glyph
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
url: "https://pub.dev"
source: hosted
version: "1.2.2"
test_api:
dependency: transitive
description:
name: test_api
sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
url: "https://pub.dev"
source: hosted
version: "0.7.10"
tray_manager:
dependency: "direct main"
description:
name: tray_manager
sha256: c5fd83b0ae4d80be6eaedfad87aaefab8787b333b8ebd064b0e442a81006035b
url: "https://pub.dev"
source: hosted
version: "0.5.2"
typed_data:
dependency: transitive
description:
name: typed_data
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
url: "https://pub.dev"
source: hosted
version: "1.4.0"
uuid:
dependency: transitive
description:
name: uuid
sha256: "1fef9e8e11e2991bb773070d4656b7bd5d850967a2456cfc83cf47925ba79489"
url: "https://pub.dev"
source: hosted
version: "4.5.3"
vector_math:
dependency: transitive
description:
name: vector_math
sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
url: "https://pub.dev"
source: hosted
version: "2.2.0"
vm_service:
dependency: transitive
description:
name: vm_service
sha256: "046d3928e16fa4dc46e8350415661755ab759d9fc97fc21b5ab295f71e4f0499"
url: "https://pub.dev"
source: hosted
version: "15.1.0"
web:
dependency: transitive
description:
name: web
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
url: "https://pub.dev"
source: hosted
version: "1.1.1"
window_manager:
dependency: "direct main"
description:
name: window_manager
sha256: "7eb6d6c4164ec08e1bf978d6e733f3cebe792e2a23fb07cbca25c2872bfdbdcd"
url: "https://pub.dev"
source: hosted
version: "0.5.1"
sdks:
dart: ">=3.9.0 <4.0.0"
flutter: ">=3.18.0-18.0.pre.54"

View File

@@ -0,0 +1,28 @@
name: netbird_flutter_ui
description: Experimental Flutter desktop UI for NetBird.
publish_to: none
version: 0.1.0
environment:
sdk: ^3.8.0
dependencies:
flutter:
sdk: flutter
fixnum: ^1.1.1
grpc: ^5.1.0
protobuf: ^6.0.0
tray_manager: ^0.5.0
window_manager: ^0.5.1
local_notifier: ^0.1.6
dev_dependencies:
flutter_test:
sdk: flutter
lints: ^6.0.0
flutter:
uses-material-design: true
assets:
- assets/tray/

View File

@@ -0,0 +1,19 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:netbird_flutter_ui/src/app_shell.dart';
import 'package:netbird_flutter_ui/src/daemon_client.dart';
void main() {
testWidgets('renders the status shell', (tester) async {
await tester.pumpWidget(
NetBirdFlutterApp(
client: FakeDaemonClient(daemonAddr: 'tcp://127.0.0.1:41731'),
),
);
await tester.pump();
expect(find.text('Status'), findsWidgets);
expect(find.text('Connect'), findsOneWidget);
expect(find.text('Disconnect'), findsOneWidget);
});
}

View File

@@ -0,0 +1,36 @@
#!/usr/bin/env bash
set -euo pipefail
project_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
tmp_dir="$(mktemp -d)"
cleanup() {
rm -rf "$tmp_dir"
}
trap cleanup EXIT
command -v flutter >/dev/null 2>&1 || {
echo "flutter is not installed"
exit 1
}
cp "$project_dir/pubspec.yaml" "$tmp_dir/pubspec.yaml"
cp "$project_dir/analysis_options.yaml" "$tmp_dir/analysis_options.yaml"
cp -R "$project_dir/lib" "$tmp_dir/lib"
cp -R "$project_dir/test" "$tmp_dir/test"
flutter create \
--platforms=windows,macos,linux \
--project-name=netbird_flutter_ui \
--org=io.netbird \
"$project_dir"
cp "$tmp_dir/pubspec.yaml" "$project_dir/pubspec.yaml"
cp "$tmp_dir/analysis_options.yaml" "$project_dir/analysis_options.yaml"
rm -rf "$project_dir/lib"
cp -R "$tmp_dir/lib" "$project_dir/lib"
rm -rf "$project_dir/test"
cp -R "$tmp_dir/test" "$project_dir/test"
cd "$project_dir"
flutter pub get

View File

@@ -0,0 +1,29 @@
#!/usr/bin/env bash
set -euo pipefail
project_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
repo_dir="$(cd "$project_dir/../.." && pwd)"
command -v protoc >/dev/null 2>&1 || {
echo "protoc is not installed"
exit 1
}
command -v dart >/dev/null 2>&1 || {
echo "dart is not installed"
exit 1
}
export PATH="$PATH:$HOME/.pub-cache/bin"
if ! command -v protoc-gen-dart >/dev/null 2>&1; then
dart pub global activate protoc_plugin
fi
mkdir -p "$project_dir/lib/src/generated"
protoc \
-I "$repo_dir/client/proto" \
--dart_out=grpc:"$project_dir/lib/src/generated" \
"$repo_dir/client/proto/daemon.proto"

17
client/flutter_ui/windows/.gitignore vendored Normal file
View File

@@ -0,0 +1,17 @@
flutter/ephemeral/
# Visual Studio user-specific files.
*.suo
*.user
*.userosscache
*.sln.docstates
# Visual Studio build-related files.
x64/
x86/
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!*.[Cc]ache/

View File

@@ -0,0 +1,108 @@
# Project-level configuration.
cmake_minimum_required(VERSION 3.14)
project(netbird_flutter_ui LANGUAGES CXX)
# The name of the executable created for the application. Change this to change
# the on-disk name of your application.
set(BINARY_NAME "netbird_flutter_ui")
# Explicitly opt in to modern CMake behaviors to avoid warnings with recent
# versions of CMake.
cmake_policy(VERSION 3.14...3.25)
# Define build configuration option.
get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG)
if(IS_MULTICONFIG)
set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release"
CACHE STRING "" FORCE)
else()
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
set(CMAKE_BUILD_TYPE "Debug" CACHE
STRING "Flutter build mode" FORCE)
set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS
"Debug" "Profile" "Release")
endif()
endif()
# Define settings for the Profile build mode.
set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}")
set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}")
set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}")
set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}")
# Use Unicode for all projects.
add_definitions(-DUNICODE -D_UNICODE)
# Compilation settings that should be applied to most targets.
#
# Be cautious about adding new options here, as plugins use this function by
# default. In most cases, you should add new options to specific targets instead
# of modifying this function.
function(APPLY_STANDARD_SETTINGS TARGET)
target_compile_features(${TARGET} PUBLIC cxx_std_17)
target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100")
target_compile_options(${TARGET} PRIVATE /EHsc)
target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0")
target_compile_definitions(${TARGET} PRIVATE "$<$<CONFIG:Debug>:_DEBUG>")
endfunction()
# Flutter library and tool build rules.
set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter")
add_subdirectory(${FLUTTER_MANAGED_DIR})
# Application build; see runner/CMakeLists.txt.
add_subdirectory("runner")
# Generated plugin build rules, which manage building the plugins and adding
# them to the application.
include(flutter/generated_plugins.cmake)
# === Installation ===
# Support files are copied into place next to the executable, so that it can
# run in place. This is done instead of making a separate bundle (as on Linux)
# so that building and running from within Visual Studio will work.
set(BUILD_BUNDLE_DIR "$<TARGET_FILE_DIR:${BINARY_NAME}>")
# Make the "install" step default, as it's required to run.
set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1)
if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE)
endif()
set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data")
set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}")
install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}"
COMPONENT Runtime)
install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
COMPONENT Runtime)
install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
if(PLUGIN_BUNDLED_LIBRARIES)
install(FILES "${PLUGIN_BUNDLED_LIBRARIES}"
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
endif()
# Copy the native assets provided by the build.dart from all packages.
set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/windows/")
install(DIRECTORY "${NATIVE_ASSETS_DIR}"
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
# Fully re-copy the assets directory on each build to avoid having stale files
# from a previous install.
set(FLUTTER_ASSET_DIR_NAME "flutter_assets")
install(CODE "
file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\")
" COMPONENT Runtime)
install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}"
DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime)
# Install the AOT library on non-Debug builds only.
install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
CONFIGURATIONS Profile;Release
COMPONENT Runtime)

View File

@@ -0,0 +1,109 @@
# This file controls Flutter-level build steps. It should not be edited.
cmake_minimum_required(VERSION 3.14)
set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral")
# Configuration provided via flutter tool.
include(${EPHEMERAL_DIR}/generated_config.cmake)
# TODO: Move the rest of this into files in ephemeral. See
# https://github.com/flutter/flutter/issues/57146.
set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper")
# Set fallback configurations for older versions of the flutter tool.
if (NOT DEFINED FLUTTER_TARGET_PLATFORM)
set(FLUTTER_TARGET_PLATFORM "windows-x64")
endif()
# === Flutter Library ===
set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll")
# Published to parent scope for install step.
set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE)
set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE)
set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE)
set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE)
list(APPEND FLUTTER_LIBRARY_HEADERS
"flutter_export.h"
"flutter_windows.h"
"flutter_messenger.h"
"flutter_plugin_registrar.h"
"flutter_texture_registrar.h"
)
list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/")
add_library(flutter INTERFACE)
target_include_directories(flutter INTERFACE
"${EPHEMERAL_DIR}"
)
target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib")
add_dependencies(flutter flutter_assemble)
# === Wrapper ===
list(APPEND CPP_WRAPPER_SOURCES_CORE
"core_implementations.cc"
"standard_codec.cc"
)
list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/")
list(APPEND CPP_WRAPPER_SOURCES_PLUGIN
"plugin_registrar.cc"
)
list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/")
list(APPEND CPP_WRAPPER_SOURCES_APP
"flutter_engine.cc"
"flutter_view_controller.cc"
)
list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/")
# Wrapper sources needed for a plugin.
add_library(flutter_wrapper_plugin STATIC
${CPP_WRAPPER_SOURCES_CORE}
${CPP_WRAPPER_SOURCES_PLUGIN}
)
apply_standard_settings(flutter_wrapper_plugin)
set_target_properties(flutter_wrapper_plugin PROPERTIES
POSITION_INDEPENDENT_CODE ON)
set_target_properties(flutter_wrapper_plugin PROPERTIES
CXX_VISIBILITY_PRESET hidden)
target_link_libraries(flutter_wrapper_plugin PUBLIC flutter)
target_include_directories(flutter_wrapper_plugin PUBLIC
"${WRAPPER_ROOT}/include"
)
add_dependencies(flutter_wrapper_plugin flutter_assemble)
# Wrapper sources needed for the runner.
add_library(flutter_wrapper_app STATIC
${CPP_WRAPPER_SOURCES_CORE}
${CPP_WRAPPER_SOURCES_APP}
)
apply_standard_settings(flutter_wrapper_app)
target_link_libraries(flutter_wrapper_app PUBLIC flutter)
target_include_directories(flutter_wrapper_app PUBLIC
"${WRAPPER_ROOT}/include"
)
add_dependencies(flutter_wrapper_app flutter_assemble)
# === Flutter tool backend ===
# _phony_ is a non-existent file to force this command to run every time,
# since currently there's no way to get a full input/output list from the
# flutter tool.
set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_")
set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE)
add_custom_command(
OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS}
${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN}
${CPP_WRAPPER_SOURCES_APP}
${PHONY_OUTPUT}
COMMAND ${CMAKE_COMMAND} -E env
${FLUTTER_TOOL_ENVIRONMENT}
"${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat"
${FLUTTER_TARGET_PLATFORM} $<CONFIG>
VERBATIM
)
add_custom_target(flutter_assemble DEPENDS
"${FLUTTER_LIBRARY}"
${FLUTTER_LIBRARY_HEADERS}
${CPP_WRAPPER_SOURCES_CORE}
${CPP_WRAPPER_SOURCES_PLUGIN}
${CPP_WRAPPER_SOURCES_APP}
)

View File

@@ -0,0 +1,23 @@
//
// Generated file. Do not edit.
//
// clang-format off
#include "generated_plugin_registrant.h"
#include <local_notifier/local_notifier_plugin.h>
#include <screen_retriever_windows/screen_retriever_windows_plugin_c_api.h>
#include <tray_manager/tray_manager_plugin.h>
#include <window_manager/window_manager_plugin.h>
void RegisterPlugins(flutter::PluginRegistry* registry) {
LocalNotifierPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("LocalNotifierPlugin"));
ScreenRetrieverWindowsPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("ScreenRetrieverWindowsPluginCApi"));
TrayManagerPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("TrayManagerPlugin"));
WindowManagerPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("WindowManagerPlugin"));
}

View File

@@ -0,0 +1,15 @@
//
// Generated file. Do not edit.
//
// clang-format off
#ifndef GENERATED_PLUGIN_REGISTRANT_
#define GENERATED_PLUGIN_REGISTRANT_
#include <flutter/plugin_registry.h>
// Registers Flutter plugins.
void RegisterPlugins(flutter::PluginRegistry* registry);
#endif // GENERATED_PLUGIN_REGISTRANT_

View File

@@ -0,0 +1,27 @@
#
# Generated file, do not edit.
#
list(APPEND FLUTTER_PLUGIN_LIST
local_notifier
screen_retriever_windows
tray_manager
window_manager
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST
)
set(PLUGIN_BUNDLED_LIBRARIES)
foreach(plugin ${FLUTTER_PLUGIN_LIST})
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin})
target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin)
list(APPEND PLUGIN_BUNDLED_LIBRARIES $<TARGET_FILE:${plugin}_plugin>)
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries})
endforeach(plugin)
foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST})
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin})
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries})
endforeach(ffi_plugin)

View File

@@ -0,0 +1,40 @@
cmake_minimum_required(VERSION 3.14)
project(runner LANGUAGES CXX)
# Define the application target. To change its name, change BINARY_NAME in the
# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer
# work.
#
# Any new source files that you add to the application should be added here.
add_executable(${BINARY_NAME} WIN32
"flutter_window.cpp"
"main.cpp"
"utils.cpp"
"win32_window.cpp"
"${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc"
"Runner.rc"
"runner.exe.manifest"
)
# Apply the standard set of build settings. This can be removed for applications
# that need different build settings.
apply_standard_settings(${BINARY_NAME})
# Add preprocessor definitions for the build version.
target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"")
target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}")
target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}")
target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}")
target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}")
# Disable Windows macros that collide with C++ standard library functions.
target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX")
# Add dependency libraries and include directories. Add any application-specific
# dependencies here.
target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app)
target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib")
target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}")
# Run the Flutter tool portions of the build. This must not be removed.
add_dependencies(${BINARY_NAME} flutter_assemble)

View File

@@ -0,0 +1,121 @@
// Microsoft Visual C++ generated resource script.
//
#pragma code_page(65001)
#include "resource.h"
#define APSTUDIO_READONLY_SYMBOLS
/////////////////////////////////////////////////////////////////////////////
//
// Generated from the TEXTINCLUDE 2 resource.
//
#include "winres.h"
/////////////////////////////////////////////////////////////////////////////
#undef APSTUDIO_READONLY_SYMBOLS
/////////////////////////////////////////////////////////////////////////////
// English (United States) resources
#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU)
LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
#ifdef APSTUDIO_INVOKED
/////////////////////////////////////////////////////////////////////////////
//
// TEXTINCLUDE
//
1 TEXTINCLUDE
BEGIN
"resource.h\0"
END
2 TEXTINCLUDE
BEGIN
"#include ""winres.h""\r\n"
"\0"
END
3 TEXTINCLUDE
BEGIN
"\r\n"
"\0"
END
#endif // APSTUDIO_INVOKED
/////////////////////////////////////////////////////////////////////////////
//
// Icon
//
// Icon with lowest ID value placed first to ensure application icon
// remains consistent on all systems.
IDI_APP_ICON ICON "resources\\app_icon.ico"
/////////////////////////////////////////////////////////////////////////////
//
// Version
//
#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD)
#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD
#else
#define VERSION_AS_NUMBER 1,0,0,0
#endif
#if defined(FLUTTER_VERSION)
#define VERSION_AS_STRING FLUTTER_VERSION
#else
#define VERSION_AS_STRING "1.0.0"
#endif
VS_VERSION_INFO VERSIONINFO
FILEVERSION VERSION_AS_NUMBER
PRODUCTVERSION VERSION_AS_NUMBER
FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
#ifdef _DEBUG
FILEFLAGS VS_FF_DEBUG
#else
FILEFLAGS 0x0L
#endif
FILEOS VOS__WINDOWS32
FILETYPE VFT_APP
FILESUBTYPE 0x0L
BEGIN
BLOCK "StringFileInfo"
BEGIN
BLOCK "040904e4"
BEGIN
VALUE "CompanyName", "io.netbird" "\0"
VALUE "FileDescription", "netbird_flutter_ui" "\0"
VALUE "FileVersion", VERSION_AS_STRING "\0"
VALUE "InternalName", "netbird_flutter_ui" "\0"
VALUE "LegalCopyright", "Copyright (C) 2026 io.netbird. All rights reserved." "\0"
VALUE "OriginalFilename", "netbird_flutter_ui.exe" "\0"
VALUE "ProductName", "netbird_flutter_ui" "\0"
VALUE "ProductVersion", VERSION_AS_STRING "\0"
END
END
BLOCK "VarFileInfo"
BEGIN
VALUE "Translation", 0x409, 1252
END
END
#endif // English (United States) resources
/////////////////////////////////////////////////////////////////////////////
#ifndef APSTUDIO_INVOKED
/////////////////////////////////////////////////////////////////////////////
//
// Generated from the TEXTINCLUDE 3 resource.
//
/////////////////////////////////////////////////////////////////////////////
#endif // not APSTUDIO_INVOKED

View File

@@ -0,0 +1,71 @@
#include "flutter_window.h"
#include <optional>
#include "flutter/generated_plugin_registrant.h"
FlutterWindow::FlutterWindow(const flutter::DartProject& project)
: project_(project) {}
FlutterWindow::~FlutterWindow() {}
bool FlutterWindow::OnCreate() {
if (!Win32Window::OnCreate()) {
return false;
}
RECT frame = GetClientArea();
// The size here must match the window dimensions to avoid unnecessary surface
// creation / destruction in the startup path.
flutter_controller_ = std::make_unique<flutter::FlutterViewController>(
frame.right - frame.left, frame.bottom - frame.top, project_);
// Ensure that basic setup of the controller was successful.
if (!flutter_controller_->engine() || !flutter_controller_->view()) {
return false;
}
RegisterPlugins(flutter_controller_->engine());
SetChildContent(flutter_controller_->view()->GetNativeWindow());
flutter_controller_->engine()->SetNextFrameCallback([&]() {
this->Show();
});
// Flutter can complete the first frame before the "show window" callback is
// registered. The following call ensures a frame is pending to ensure the
// window is shown. It is a no-op if the first frame hasn't completed yet.
flutter_controller_->ForceRedraw();
return true;
}
void FlutterWindow::OnDestroy() {
if (flutter_controller_) {
flutter_controller_ = nullptr;
}
Win32Window::OnDestroy();
}
LRESULT
FlutterWindow::MessageHandler(HWND hwnd, UINT const message,
WPARAM const wparam,
LPARAM const lparam) noexcept {
// Give Flutter, including plugins, an opportunity to handle window messages.
if (flutter_controller_) {
std::optional<LRESULT> result =
flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam,
lparam);
if (result) {
return *result;
}
}
switch (message) {
case WM_FONTCHANGE:
flutter_controller_->engine()->ReloadSystemFonts();
break;
}
return Win32Window::MessageHandler(hwnd, message, wparam, lparam);
}

View File

@@ -0,0 +1,33 @@
#ifndef RUNNER_FLUTTER_WINDOW_H_
#define RUNNER_FLUTTER_WINDOW_H_
#include <flutter/dart_project.h>
#include <flutter/flutter_view_controller.h>
#include <memory>
#include "win32_window.h"
// A window that does nothing but host a Flutter view.
class FlutterWindow : public Win32Window {
public:
// Creates a new FlutterWindow hosting a Flutter view running |project|.
explicit FlutterWindow(const flutter::DartProject& project);
virtual ~FlutterWindow();
protected:
// Win32Window:
bool OnCreate() override;
void OnDestroy() override;
LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam,
LPARAM const lparam) noexcept override;
private:
// The project to run.
flutter::DartProject project_;
// The Flutter instance hosted by this window.
std::unique_ptr<flutter::FlutterViewController> flutter_controller_;
};
#endif // RUNNER_FLUTTER_WINDOW_H_

View File

@@ -0,0 +1,43 @@
#include <flutter/dart_project.h>
#include <flutter/flutter_view_controller.h>
#include <windows.h>
#include "flutter_window.h"
#include "utils.h"
int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
_In_ wchar_t *command_line, _In_ int show_command) {
// Attach to console when present (e.g., 'flutter run') or create a
// new console when running with a debugger.
if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) {
CreateAndAttachConsole();
}
// Initialize COM, so that it is available for use in the library and/or
// plugins.
::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
flutter::DartProject project(L"data");
std::vector<std::string> command_line_arguments =
GetCommandLineArguments();
project.set_dart_entrypoint_arguments(std::move(command_line_arguments));
FlutterWindow window(project);
Win32Window::Point origin(10, 10);
Win32Window::Size size(1280, 720);
if (!window.Create(L"netbird_flutter_ui", origin, size)) {
return EXIT_FAILURE;
}
window.SetQuitOnClose(true);
::MSG msg;
while (::GetMessage(&msg, nullptr, 0, 0)) {
::TranslateMessage(&msg);
::DispatchMessage(&msg);
}
::CoUninitialize();
return EXIT_SUCCESS;
}

View File

@@ -0,0 +1,16 @@
//{{NO_DEPENDENCIES}}
// Microsoft Visual C++ generated include file.
// Used by Runner.rc
//
#define IDI_APP_ICON 101
// Next default values for new objects
//
#ifdef APSTUDIO_INVOKED
#ifndef APSTUDIO_READONLY_SYMBOLS
#define _APS_NEXT_RESOURCE_VALUE 102
#define _APS_NEXT_COMMAND_VALUE 40001
#define _APS_NEXT_CONTROL_VALUE 1001
#define _APS_NEXT_SYMED_VALUE 101
#endif
#endif

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
</windowsSettings>
</application>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- Windows 10 and Windows 11 -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
</application>
</compatibility>
</assembly>

View File

@@ -0,0 +1,65 @@
#include "utils.h"
#include <flutter_windows.h>
#include <io.h>
#include <stdio.h>
#include <windows.h>
#include <iostream>
void CreateAndAttachConsole() {
if (::AllocConsole()) {
FILE *unused;
if (freopen_s(&unused, "CONOUT$", "w", stdout)) {
_dup2(_fileno(stdout), 1);
}
if (freopen_s(&unused, "CONOUT$", "w", stderr)) {
_dup2(_fileno(stdout), 2);
}
std::ios::sync_with_stdio();
FlutterDesktopResyncOutputStreams();
}
}
std::vector<std::string> GetCommandLineArguments() {
// Convert the UTF-16 command line arguments to UTF-8 for the Engine to use.
int argc;
wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc);
if (argv == nullptr) {
return std::vector<std::string>();
}
std::vector<std::string> command_line_arguments;
// Skip the first argument as it's the binary name.
for (int i = 1; i < argc; i++) {
command_line_arguments.push_back(Utf8FromUtf16(argv[i]));
}
::LocalFree(argv);
return command_line_arguments;
}
std::string Utf8FromUtf16(const wchar_t* utf16_string) {
if (utf16_string == nullptr) {
return std::string();
}
unsigned int target_length = ::WideCharToMultiByte(
CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string,
-1, nullptr, 0, nullptr, nullptr)
-1; // remove the trailing null character
int input_length = (int)wcslen(utf16_string);
std::string utf8_string;
if (target_length == 0 || target_length > utf8_string.max_size()) {
return utf8_string;
}
utf8_string.resize(target_length);
int converted_length = ::WideCharToMultiByte(
CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string,
input_length, utf8_string.data(), target_length, nullptr, nullptr);
if (converted_length == 0) {
return std::string();
}
return utf8_string;
}

View File

@@ -0,0 +1,19 @@
#ifndef RUNNER_UTILS_H_
#define RUNNER_UTILS_H_
#include <string>
#include <vector>
// Creates a console for the process, and redirects stdout and stderr to
// it for both the runner and the Flutter library.
void CreateAndAttachConsole();
// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string
// encoded in UTF-8. Returns an empty std::string on failure.
std::string Utf8FromUtf16(const wchar_t* utf16_string);
// Gets the command line arguments passed in as a std::vector<std::string>,
// encoded in UTF-8. Returns an empty std::vector<std::string> on failure.
std::vector<std::string> GetCommandLineArguments();
#endif // RUNNER_UTILS_H_

View File

@@ -0,0 +1,288 @@
#include "win32_window.h"
#include <dwmapi.h>
#include <flutter_windows.h>
#include "resource.h"
namespace {
/// Window attribute that enables dark mode window decorations.
///
/// Redefined in case the developer's machine has a Windows SDK older than
/// version 10.0.22000.0.
/// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute
#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE
#define DWMWA_USE_IMMERSIVE_DARK_MODE 20
#endif
constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW";
/// Registry key for app theme preference.
///
/// A value of 0 indicates apps should use dark mode. A non-zero or missing
/// value indicates apps should use light mode.
constexpr const wchar_t kGetPreferredBrightnessRegKey[] =
L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize";
constexpr const wchar_t kGetPreferredBrightnessRegValue[] = L"AppsUseLightTheme";
// The number of Win32Window objects that currently exist.
static int g_active_window_count = 0;
using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd);
// Scale helper to convert logical scaler values to physical using passed in
// scale factor
int Scale(int source, double scale_factor) {
return static_cast<int>(source * scale_factor);
}
// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module.
// This API is only needed for PerMonitor V1 awareness mode.
void EnableFullDpiSupportIfAvailable(HWND hwnd) {
HMODULE user32_module = LoadLibraryA("User32.dll");
if (!user32_module) {
return;
}
auto enable_non_client_dpi_scaling =
reinterpret_cast<EnableNonClientDpiScaling*>(
GetProcAddress(user32_module, "EnableNonClientDpiScaling"));
if (enable_non_client_dpi_scaling != nullptr) {
enable_non_client_dpi_scaling(hwnd);
}
FreeLibrary(user32_module);
}
} // namespace
// Manages the Win32Window's window class registration.
class WindowClassRegistrar {
public:
~WindowClassRegistrar() = default;
// Returns the singleton registrar instance.
static WindowClassRegistrar* GetInstance() {
if (!instance_) {
instance_ = new WindowClassRegistrar();
}
return instance_;
}
// Returns the name of the window class, registering the class if it hasn't
// previously been registered.
const wchar_t* GetWindowClass();
// Unregisters the window class. Should only be called if there are no
// instances of the window.
void UnregisterWindowClass();
private:
WindowClassRegistrar() = default;
static WindowClassRegistrar* instance_;
bool class_registered_ = false;
};
WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr;
const wchar_t* WindowClassRegistrar::GetWindowClass() {
if (!class_registered_) {
WNDCLASS window_class{};
window_class.hCursor = LoadCursor(nullptr, IDC_ARROW);
window_class.lpszClassName = kWindowClassName;
window_class.style = CS_HREDRAW | CS_VREDRAW;
window_class.cbClsExtra = 0;
window_class.cbWndExtra = 0;
window_class.hInstance = GetModuleHandle(nullptr);
window_class.hIcon =
LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON));
window_class.hbrBackground = 0;
window_class.lpszMenuName = nullptr;
window_class.lpfnWndProc = Win32Window::WndProc;
RegisterClass(&window_class);
class_registered_ = true;
}
return kWindowClassName;
}
void WindowClassRegistrar::UnregisterWindowClass() {
UnregisterClass(kWindowClassName, nullptr);
class_registered_ = false;
}
Win32Window::Win32Window() {
++g_active_window_count;
}
Win32Window::~Win32Window() {
--g_active_window_count;
Destroy();
}
bool Win32Window::Create(const std::wstring& title,
const Point& origin,
const Size& size) {
Destroy();
const wchar_t* window_class =
WindowClassRegistrar::GetInstance()->GetWindowClass();
const POINT target_point = {static_cast<LONG>(origin.x),
static_cast<LONG>(origin.y)};
HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST);
UINT dpi = FlutterDesktopGetDpiForMonitor(monitor);
double scale_factor = dpi / 96.0;
HWND window = CreateWindow(
window_class, title.c_str(), WS_OVERLAPPEDWINDOW,
Scale(origin.x, scale_factor), Scale(origin.y, scale_factor),
Scale(size.width, scale_factor), Scale(size.height, scale_factor),
nullptr, nullptr, GetModuleHandle(nullptr), this);
if (!window) {
return false;
}
UpdateTheme(window);
return OnCreate();
}
bool Win32Window::Show() {
return ShowWindow(window_handle_, SW_SHOWNORMAL);
}
// static
LRESULT CALLBACK Win32Window::WndProc(HWND const window,
UINT const message,
WPARAM const wparam,
LPARAM const lparam) noexcept {
if (message == WM_NCCREATE) {
auto window_struct = reinterpret_cast<CREATESTRUCT*>(lparam);
SetWindowLongPtr(window, GWLP_USERDATA,
reinterpret_cast<LONG_PTR>(window_struct->lpCreateParams));
auto that = static_cast<Win32Window*>(window_struct->lpCreateParams);
EnableFullDpiSupportIfAvailable(window);
that->window_handle_ = window;
} else if (Win32Window* that = GetThisFromHandle(window)) {
return that->MessageHandler(window, message, wparam, lparam);
}
return DefWindowProc(window, message, wparam, lparam);
}
LRESULT
Win32Window::MessageHandler(HWND hwnd,
UINT const message,
WPARAM const wparam,
LPARAM const lparam) noexcept {
switch (message) {
case WM_DESTROY:
window_handle_ = nullptr;
Destroy();
if (quit_on_close_) {
PostQuitMessage(0);
}
return 0;
case WM_DPICHANGED: {
auto newRectSize = reinterpret_cast<RECT*>(lparam);
LONG newWidth = newRectSize->right - newRectSize->left;
LONG newHeight = newRectSize->bottom - newRectSize->top;
SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth,
newHeight, SWP_NOZORDER | SWP_NOACTIVATE);
return 0;
}
case WM_SIZE: {
RECT rect = GetClientArea();
if (child_content_ != nullptr) {
// Size and position the child window.
MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left,
rect.bottom - rect.top, TRUE);
}
return 0;
}
case WM_ACTIVATE:
if (child_content_ != nullptr) {
SetFocus(child_content_);
}
return 0;
case WM_DWMCOLORIZATIONCOLORCHANGED:
UpdateTheme(hwnd);
return 0;
}
return DefWindowProc(window_handle_, message, wparam, lparam);
}
void Win32Window::Destroy() {
OnDestroy();
if (window_handle_) {
DestroyWindow(window_handle_);
window_handle_ = nullptr;
}
if (g_active_window_count == 0) {
WindowClassRegistrar::GetInstance()->UnregisterWindowClass();
}
}
Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept {
return reinterpret_cast<Win32Window*>(
GetWindowLongPtr(window, GWLP_USERDATA));
}
void Win32Window::SetChildContent(HWND content) {
child_content_ = content;
SetParent(content, window_handle_);
RECT frame = GetClientArea();
MoveWindow(content, frame.left, frame.top, frame.right - frame.left,
frame.bottom - frame.top, true);
SetFocus(child_content_);
}
RECT Win32Window::GetClientArea() {
RECT frame;
GetClientRect(window_handle_, &frame);
return frame;
}
HWND Win32Window::GetHandle() {
return window_handle_;
}
void Win32Window::SetQuitOnClose(bool quit_on_close) {
quit_on_close_ = quit_on_close;
}
bool Win32Window::OnCreate() {
// No-op; provided for subclasses.
return true;
}
void Win32Window::OnDestroy() {
// No-op; provided for subclasses.
}
void Win32Window::UpdateTheme(HWND const window) {
DWORD light_mode;
DWORD light_mode_size = sizeof(light_mode);
LSTATUS result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey,
kGetPreferredBrightnessRegValue,
RRF_RT_REG_DWORD, nullptr, &light_mode,
&light_mode_size);
if (result == ERROR_SUCCESS) {
BOOL enable_dark_mode = light_mode == 0;
DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE,
&enable_dark_mode, sizeof(enable_dark_mode));
}
}

View File

@@ -0,0 +1,102 @@
#ifndef RUNNER_WIN32_WINDOW_H_
#define RUNNER_WIN32_WINDOW_H_
#include <windows.h>
#include <functional>
#include <memory>
#include <string>
// A class abstraction for a high DPI-aware Win32 Window. Intended to be
// inherited from by classes that wish to specialize with custom
// rendering and input handling
class Win32Window {
public:
struct Point {
unsigned int x;
unsigned int y;
Point(unsigned int x, unsigned int y) : x(x), y(y) {}
};
struct Size {
unsigned int width;
unsigned int height;
Size(unsigned int width, unsigned int height)
: width(width), height(height) {}
};
Win32Window();
virtual ~Win32Window();
// Creates a win32 window with |title| that is positioned and sized using
// |origin| and |size|. New windows are created on the default monitor. Window
// sizes are specified to the OS in physical pixels, hence to ensure a
// consistent size this function will scale the inputted width and height as
// as appropriate for the default monitor. The window is invisible until
// |Show| is called. Returns true if the window was created successfully.
bool Create(const std::wstring& title, const Point& origin, const Size& size);
// Show the current window. Returns true if the window was successfully shown.
bool Show();
// Release OS resources associated with window.
void Destroy();
// Inserts |content| into the window tree.
void SetChildContent(HWND content);
// Returns the backing Window handle to enable clients to set icon and other
// window properties. Returns nullptr if the window has been destroyed.
HWND GetHandle();
// If true, closing this window will quit the application.
void SetQuitOnClose(bool quit_on_close);
// Return a RECT representing the bounds of the current client area.
RECT GetClientArea();
protected:
// Processes and route salient window messages for mouse handling,
// size change and DPI. Delegates handling of these to member overloads that
// inheriting classes can handle.
virtual LRESULT MessageHandler(HWND window,
UINT const message,
WPARAM const wparam,
LPARAM const lparam) noexcept;
// Called when CreateAndShow is called, allowing subclass window-related
// setup. Subclasses should return false if setup fails.
virtual bool OnCreate();
// Called when Destroy is called.
virtual void OnDestroy();
private:
friend class WindowClassRegistrar;
// OS callback called by message pump. Handles the WM_NCCREATE message which
// is passed when the non-client area is being created and enables automatic
// non-client DPI scaling so that the non-client area automatically
// responds to changes in DPI. All other messages are handled by
// MessageHandler.
static LRESULT CALLBACK WndProc(HWND const window,
UINT const message,
WPARAM const wparam,
LPARAM const lparam) noexcept;
// Retrieves a class instance pointer for |window|
static Win32Window* GetThisFromHandle(HWND const window) noexcept;
// Update the window frame's theme to match the system theme.
static void UpdateTheme(HWND const window);
bool quit_on_close_ = false;
// window handle for top level window.
HWND window_handle_ = nullptr;
// window handle for hosted content.
HWND child_content_ = nullptr;
};
#endif // RUNNER_WIN32_WINDOW_H_