diff --git a/app/android/app/src/main/AndroidManifest.xml b/app/android/app/src/main/AndroidManifest.xml
index 241e2b87..b9449e88 100644
--- a/app/android/app/src/main/AndroidManifest.xml
+++ b/app/android/app/src/main/AndroidManifest.xml
@@ -41,9 +41,9 @@
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
+ android:name="io.flutter.embedding.android.EnableCutout"
+ android:value="true"
+ />
diff --git a/app/android/app/src/main/res/values-night/styles.xml b/app/android/app/src/main/res/values-night/styles.xml
index 06952be7..3c389d5f 100644
--- a/app/android/app/src/main/res/values-night/styles.xml
+++ b/app/android/app/src/main/res/values-night/styles.xml
@@ -5,6 +5,7 @@
- @drawable/launch_background
+ - shortEdges
diff --git a/app/android/app/src/main/res/values/styles.xml b/app/android/app/src/main/res/values/styles.xml
index cb1ef880..b69f5353 100644
--- a/app/android/app/src/main/res/values/styles.xml
+++ b/app/android/app/src/main/res/values/styles.xml
@@ -5,6 +5,7 @@
- @drawable/launch_background
+ - shortEdges
diff --git a/app/ios/Runner/AppDelegate.swift b/app/ios/Runner/AppDelegate.swift
index 10a47e7b..b725aba5 100644
--- a/app/ios/Runner/AppDelegate.swift
+++ b/app/ios/Runner/AppDelegate.swift
@@ -9,17 +9,25 @@ import Flutter
) -> Bool {
let controller : FlutterViewController = window?.rootViewController as! FlutterViewController
- let channel = FlutterMethodChannel(name: "ios-delegate-channel",
- binaryMessenger: controller.engine.binaryMessenger)
- channel.setMethodCallHandler({
- (call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
- if call.method == "isReduceMotionEnabled" {
- result(UIAccessibility.isReduceMotionEnabled)
- } else {
- result(FlutterMethodNotImplemented)
+
+ if let engine = controller.engine {
+ let channel = FlutterMethodChannel(
+ name: "ios-delegate-channel",
+ binaryMessenger: engine.binaryMessenger
+ )
+ channel.setMethodCallHandler { (call: FlutterMethodCall, result: @escaping FlutterResult) in
+ if call.method == "isReduceMotionEnabled" {
+ result(UIAccessibility.isReduceMotionEnabled)
+ } else {
+ result(FlutterMethodNotImplemented)
+ }
}
- })
+ } else {
+ // I couldn't get the iOS build to run without this check
+ print("Flutter engine is nil!")
+ }
+
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
-}
+}
\ No newline at end of file
diff --git a/app/lib/config/init.dart b/app/lib/config/init.dart
index 5dc12e33..c884afcf 100644
--- a/app/lib/config/init.dart
+++ b/app/lib/config/init.dart
@@ -1,6 +1,7 @@
import 'dart:async';
import 'dart:io';
+import 'package:bitsdojo_window/bitsdojo_window.dart';
import 'package:common/api_route_builder.dart';
import 'package:common/constants.dart';
import 'package:common/isolate.dart';
@@ -114,12 +115,14 @@ Future preInit(List args) async {
} else if (defaultTargetPlatform == TargetPlatform.macOS) {
startHidden = await isLaunchedAsLoginItem() && await getLaunchAtLoginMinimized();
}
-
- if (startHidden) {
- unawaited(hideToTray());
- } else {
- unawaited(showFromTray());
- }
+
+ doWhenWindowReady(() {
+ if (startHidden) {
+ unawaited(hideToTray());
+ } else {
+ unawaited(showFromTray());
+ }
+ });
if (defaultTargetPlatform == TargetPlatform.macOS) {
await setupStatusBar();
diff --git a/app/lib/pages/about/about_page.dart b/app/lib/pages/about/about_page.dart
index 643c3b2a..6b35e756 100644
--- a/app/lib/pages/about/about_page.dart
+++ b/app/lib/pages/about/about_page.dart
@@ -3,6 +3,7 @@ import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:localsend_app/gen/strings.g.dart';
import 'package:localsend_app/pages/debug/debug_page.dart';
+import 'package:localsend_app/widget/custom_basic_appbar.dart';
import 'package:localsend_app/widget/local_send_logo.dart';
import 'package:localsend_app/widget/responsive_list_view.dart';
import 'package:routerino/routerino.dart';
@@ -23,9 +24,7 @@ class AboutPage extends StatelessWidget {
Widget build(BuildContext context) {
final primaryColor = Theme.of(context).colorScheme.primary;
return Scaffold(
- appBar: AppBar(
- title: Text(t.aboutPage.title),
- ),
+ appBar: basicLocalSendAppbar(t.aboutPage.title),
body: ResponsiveListView(
padding: const EdgeInsets.symmetric(horizontal: 15),
children: [
diff --git a/app/lib/pages/changelog_page.dart b/app/lib/pages/changelog_page.dart
index e6d4acd3..7e1f64f2 100644
--- a/app/lib/pages/changelog_page.dart
+++ b/app/lib/pages/changelog_page.dart
@@ -4,6 +4,7 @@ import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:localsend_app/gen/assets.gen.dart';
import 'package:localsend_app/gen/strings.g.dart';
import 'package:localsend_app/util/ui/nav_bar_padding.dart';
+import 'package:localsend_app/widget/custom_basic_appbar.dart';
class ChangelogPage extends StatelessWidget {
const ChangelogPage();
@@ -11,9 +12,7 @@ class ChangelogPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
- appBar: AppBar(
- title: Text(t.changelogPage.title),
- ),
+ appBar: basicLocalSendAppbar(t.changelogPage.title),
body: FutureBuilder(
future: rootBundle.loadString(Assets.changelog), // ignore: discarded_futures
builder: (context, data) {
@@ -22,8 +21,8 @@ class ChangelogPage extends StatelessWidget {
}
return Markdown(
padding: EdgeInsets.only(
- left: 15,
- right: 15,
+ left: 15 + MediaQuery.of(context).padding.left,
+ right: 15 + MediaQuery.of(context).padding.right,
top: 15,
bottom: 15 + getNavBarPadding(context),
),
diff --git a/app/lib/pages/debug/debug_page.dart b/app/lib/pages/debug/debug_page.dart
index 5d33bdee..37c0900c 100644
--- a/app/lib/pages/debug/debug_page.dart
+++ b/app/lib/pages/debug/debug_page.dart
@@ -8,6 +8,7 @@ import 'package:localsend_app/pages/debug/security_debug_page.dart';
import 'package:localsend_app/provider/app_arguments_provider.dart';
import 'package:localsend_app/provider/persistence_provider.dart';
import 'package:localsend_app/util/shared_preferences/shared_preferences_file.dart';
+import 'package:localsend_app/widget/custom_basic_appbar.dart';
import 'package:localsend_app/widget/debug_entry.dart';
import 'package:refena_flutter/refena_flutter.dart';
import 'package:routerino/routerino.dart';
@@ -23,9 +24,7 @@ class DebugPage extends StatelessWidget {
final store = SharedPreferencesStorePlatform.instance;
return Scaffold(
- appBar: AppBar(
- title: const Text('Debugging'),
- ),
+ appBar: basicLocalSendAppbar('Debugging'),
body: ListView(
padding: const EdgeInsets.only(left: 20, right: 20, top: 10, bottom: 30),
children: [
diff --git a/app/lib/pages/debug/discovery_debug_page.dart b/app/lib/pages/debug/discovery_debug_page.dart
index f192eed7..0712bb25 100644
--- a/app/lib/pages/debug/discovery_debug_page.dart
+++ b/app/lib/pages/debug/discovery_debug_page.dart
@@ -3,6 +3,7 @@ import 'package:intl/intl.dart';
import 'package:localsend_app/provider/logging/discovery_logs_provider.dart';
import 'package:localsend_app/provider/network/nearby_devices_provider.dart';
import 'package:localsend_app/widget/copyable_text.dart';
+import 'package:localsend_app/widget/custom_basic_appbar.dart';
import 'package:localsend_app/widget/responsive_list_view.dart';
import 'package:refena_flutter/refena_flutter.dart';
@@ -16,9 +17,7 @@ class DiscoveryDebugPage extends StatelessWidget {
final ref = context.ref;
final logs = ref.watch(discoveryLoggerProvider);
return Scaffold(
- appBar: AppBar(
- title: const Text('Discovery Debugging'),
- ),
+ appBar: basicLocalSendAppbar('Discovery Debugging'),
body: ResponsiveListView(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 30),
children: [
diff --git a/app/lib/pages/debug/http_logs_page.dart b/app/lib/pages/debug/http_logs_page.dart
index c1230d6c..c4fc7752 100644
--- a/app/lib/pages/debug/http_logs_page.dart
+++ b/app/lib/pages/debug/http_logs_page.dart
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:localsend_app/provider/logging/http_logs_provider.dart';
import 'package:localsend_app/widget/copyable_text.dart';
+import 'package:localsend_app/widget/custom_basic_appbar.dart';
import 'package:localsend_app/widget/responsive_list_view.dart';
import 'package:refena_flutter/refena_flutter.dart';
@@ -14,9 +15,7 @@ class HttpLogsPage extends StatelessWidget {
Widget build(BuildContext context) {
final logs = context.ref.watch(httpLogsProvider);
return Scaffold(
- appBar: AppBar(
- title: const Text('HTTP Logs'),
- ),
+ appBar: basicLocalSendAppbar('HTTP Logs'),
body: ResponsiveListView(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 30),
children: [
diff --git a/app/lib/pages/debug/security_debug_page.dart b/app/lib/pages/debug/security_debug_page.dart
index 1ccf9a71..6df0c679 100644
--- a/app/lib/pages/debug/security_debug_page.dart
+++ b/app/lib/pages/debug/security_debug_page.dart
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:localsend_app/provider/security_provider.dart';
+import 'package:localsend_app/widget/custom_basic_appbar.dart';
import 'package:localsend_app/widget/debug_entry.dart';
import 'package:localsend_app/widget/responsive_list_view.dart';
import 'package:refena_flutter/refena_flutter.dart';
@@ -11,9 +12,7 @@ class SecurityDebugPage extends StatelessWidget {
Widget build(BuildContext context) {
final securityContext = context.ref.watch(securityProvider);
return Scaffold(
- appBar: AppBar(
- title: const Text('Security Debugging'),
- ),
+ appBar: basicLocalSendAppbar('Security Debugging'),
body: ResponsiveListView(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 30),
maxWidth: 700,
diff --git a/app/lib/pages/donation/donation_page.dart b/app/lib/pages/donation/donation_page.dart
index f17c21c1..78da0dbb 100644
--- a/app/lib/pages/donation/donation_page.dart
+++ b/app/lib/pages/donation/donation_page.dart
@@ -4,6 +4,7 @@ import 'package:localsend_app/model/state/purchase_state.dart';
import 'package:localsend_app/pages/donation/donation_page_vm.dart';
// [FOSS_REMOVE_START]
import 'package:localsend_app/provider/purchase_provider.dart';
+import 'package:localsend_app/widget/custom_basic_appbar.dart';
// [FOSS_REMOVE_END]
import 'package:localsend_app/widget/responsive_list_view.dart';
import 'package:refena_flutter/refena_flutter.dart';
@@ -21,9 +22,7 @@ class DonationPage extends StatelessWidget {
// [FOSS_REMOVE_END]
builder: (context, vm) {
return Scaffold(
- appBar: AppBar(
- title: Text(t.donationPage.title),
- ),
+ appBar: basicLocalSendAppbar(t.donationPage.title),
body: Stack(
children: [
ResponsiveListView(
diff --git a/app/lib/pages/home_page.dart b/app/lib/pages/home_page.dart
index 6e8536e4..43a20a13 100644
--- a/app/lib/pages/home_page.dart
+++ b/app/lib/pages/home_page.dart
@@ -1,5 +1,6 @@
import 'dart:io';
+import 'package:bitsdojo_window/bitsdojo_window.dart';
import 'package:desktop_drop/desktop_drop.dart';
import 'package:flutter/material.dart';
import 'package:localsend_app/config/init.dart';
@@ -11,6 +12,7 @@ import 'package:localsend_app/pages/tabs/send_tab.dart';
import 'package:localsend_app/pages/tabs/settings_tab.dart';
import 'package:localsend_app/provider/selection/selected_sending_files_provider.dart';
import 'package:localsend_app/util/native/cross_file_converters.dart';
+import 'package:localsend_app/util/native/platform_check.dart';
import 'package:localsend_app/widget/responsive_builder.dart';
import 'package:refena_flutter/refena_flutter.dart';
@@ -100,62 +102,78 @@ class _HomePageState extends State with Refena {
body: Row(
children: [
if (!sizingInformation.isMobile)
- NavigationRail(
- selectedIndex: vm.currentTab.index,
- onDestinationSelected: (index) => vm.changeTab(HomeTab.values[index]),
- extended: sizingInformation.isDesktop,
- backgroundColor: Theme.of(context).cardColorWithElevation,
- leading: sizingInformation.isDesktop
- ? const Column(
- children: [
- SizedBox(height: 20),
- Text(
- 'LocalSend',
- style: TextStyle(fontSize: 32, fontWeight: FontWeight.bold),
- textAlign: TextAlign.center,
- ),
- SizedBox(height: 20),
- ],
- )
- : null,
- destinations: HomeTab.values.map((tab) {
- return NavigationRailDestination(
- icon: Icon(tab.icon),
- label: Text(tab.label),
- );
- }).toList(),
+ Stack(
+ children: [
+ NavigationRail(
+ selectedIndex: vm.currentTab.index,
+ onDestinationSelected: (index) => vm.changeTab(HomeTab.values[index]),
+ extended: sizingInformation.isDesktop,
+ backgroundColor: Theme.of(context).cardColorWithElevation,
+ leading: sizingInformation.isDesktop
+ ? Column(
+ children: [
+ checkPlatform([TargetPlatform.macOS])
+ ? // considered adding some extra space so it looks more natural
+ SizedBox(height: 40)
+ : SizedBox(height: 20),
+ const Text(
+ 'LocalSend',
+ style: TextStyle(fontSize: 32, fontWeight: FontWeight.bold),
+ textAlign: TextAlign.center,
+ ),
+ SizedBox(height: 20),
+ ],
+ )
+ : checkPlatform([TargetPlatform.macOS])
+ ? SizedBox(
+ height: 20,
+ )
+ : null,
+ destinations: HomeTab.values.map((tab) {
+ return NavigationRailDestination(
+ icon: Icon(tab.icon),
+ label: Text(tab.label),
+ );
+ }).toList(),
+ ),
+ // makes the top draggable
+ Positioned(
+ top: 0,
+ left: 0,
+ right: 0,
+ height: 40,
+ child: MoveWindow(),
+ ),
+ ],
),
Expanded(
- child: SafeArea(
- left: sizingInformation.isMobile,
- child: Stack(
- children: [
- PageView(
- controller: vm.controller,
- physics: const NeverScrollableScrollPhysics(),
- children: const [
- ReceiveTab(),
- SendTab(),
- SettingsTab(),
- ],
- ),
- if (_dragAndDropIndicator)
- Container(
- width: double.infinity,
- decoration: BoxDecoration(
- color: Theme.of(context).scaffoldBackgroundColor,
- ),
- child: Column(
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- const Icon(Icons.file_download, size: 128),
- const SizedBox(height: 30),
- Text(t.sendTab.placeItems, style: Theme.of(context).textTheme.titleLarge),
- ],
- ),
+ child: Stack(
+ children: [
+ PageView(
+ controller: vm.controller,
+ physics: const NeverScrollableScrollPhysics(),
+ children: const [
+ SafeArea(child: ReceiveTab()),
+ SafeArea(child: SendTab()),
+ SettingsTab(),
+ ],
+ ),
+ if (_dragAndDropIndicator)
+ Container(
+ width: double.infinity,
+ decoration: BoxDecoration(
+ color: Theme.of(context).scaffoldBackgroundColor,
),
- ],
- ),
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ const Icon(Icons.file_download, size: 128),
+ const SizedBox(height: 30),
+ Text(t.sendTab.placeItems, style: Theme.of(context).textTheme.titleLarge),
+ ],
+ ),
+ ),
+ ],
),
),
],
diff --git a/app/lib/pages/language_page.dart b/app/lib/pages/language_page.dart
index 7038cfbd..470d4db4 100644
--- a/app/lib/pages/language_page.dart
+++ b/app/lib/pages/language_page.dart
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:localsend_app/gen/strings.g.dart';
import 'package:localsend_app/provider/settings_provider.dart';
+import 'package:localsend_app/widget/custom_basic_appbar.dart';
import 'package:localsend_app/widget/responsive_list_view.dart';
import 'package:refena_flutter/refena_flutter.dart';
@@ -27,9 +28,7 @@ class _LanguagePageState extends State {
final t = Translations.of(context);
final activeLocale = context.ref.watch(settingsProvider.select((s) => s.locale));
return Scaffold(
- appBar: AppBar(
- title: Text(t.settingsTab.general.language),
- ),
+ appBar: basicLocalSendAppbar(t.sendTab.selection.title),
body: ResponsiveListView(
padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 20),
children: [
diff --git a/app/lib/pages/progress_page.dart b/app/lib/pages/progress_page.dart
index 81d87b35..5facf702 100644
--- a/app/lib/pages/progress_page.dart
+++ b/app/lib/pages/progress_page.dart
@@ -1,6 +1,7 @@
import 'dart:async';
import 'dart:typed_data';
+import 'package:bitsdojo_window/bitsdojo_window.dart';
import 'package:common/model/dto/file_dto.dart';
import 'package:common/model/file_status.dart';
import 'package:common/model/session_status.dart';
@@ -19,6 +20,7 @@ import 'package:localsend_app/util/native/open_folder.dart';
import 'package:localsend_app/util/native/platform_check.dart';
import 'package:localsend_app/util/native/taskbar_helper.dart';
import 'package:localsend_app/util/ui/nav_bar_padding.dart';
+import 'package:localsend_app/widget/custom_basic_appbar.dart';
import 'package:localsend_app/widget/custom_progress_bar.dart';
import 'package:localsend_app/widget/dialogs/cancel_session_dialog.dart';
import 'package:localsend_app/widget/dialogs/error_dialog.dart';
@@ -229,11 +231,7 @@ class _ProgressPageState extends State with Refena {
},
canPop: false,
child: Scaffold(
- appBar: widget.showAppBar
- ? AppBar(
- title: Text(title),
- )
- : null,
+ appBar: widget.showAppBar ? basicLocalSendAppbar(title) : null,
body: Stack(
children: [
ListView.builder(
@@ -510,6 +508,15 @@ class _ProgressPageState extends State with Refena {
),
),
),
+ checkPlatform([TargetPlatform.macOS])
+ ? Positioned(
+ top: 0,
+ left: 0,
+ right: 0,
+ height: 40,
+ child: MoveWindow(),
+ )
+ : SizedBox(),
],
),
),
diff --git a/app/lib/pages/receive_history_page.dart b/app/lib/pages/receive_history_page.dart
index 6ac0b0aa..9c571135 100644
--- a/app/lib/pages/receive_history_page.dart
+++ b/app/lib/pages/receive_history_page.dart
@@ -13,6 +13,7 @@ import 'package:localsend_app/util/native/directories.dart';
import 'package:localsend_app/util/native/open_file.dart';
import 'package:localsend_app/util/native/open_folder.dart';
import 'package:localsend_app/util/native/platform_check.dart';
+import 'package:localsend_app/widget/custom_basic_appbar.dart';
import 'package:localsend_app/widget/dialogs/file_info_dialog.dart';
import 'package:localsend_app/widget/dialogs/history_clear_dialog.dart';
import 'package:localsend_app/widget/file_thumbnail.dart';
@@ -61,11 +62,8 @@ class ReceiveHistoryPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final entries = context.watch(receiveHistoryProvider);
-
return Scaffold(
- appBar: AppBar(
- title: Text(t.receiveHistoryPage.title),
- ),
+ appBar: basicLocalSendAppbar(t.receiveHistoryPage.title),
body: ResponsiveListView(
padding: const EdgeInsets.symmetric(vertical: 20),
children: [
diff --git a/app/lib/pages/selected_files_page.dart b/app/lib/pages/selected_files_page.dart
index d7f57bb9..bfbf0e1a 100644
--- a/app/lib/pages/selected_files_page.dart
+++ b/app/lib/pages/selected_files_page.dart
@@ -7,6 +7,7 @@ import 'package:localsend_app/provider/selection/selected_sending_files_provider
import 'package:localsend_app/util/file_size_helper.dart';
import 'package:localsend_app/util/native/open_file.dart';
import 'package:localsend_app/util/ui/nav_bar_padding.dart';
+import 'package:localsend_app/widget/custom_basic_appbar.dart';
import 'package:localsend_app/widget/dialogs/message_input_dialog.dart';
import 'package:localsend_app/widget/file_thumbnail.dart';
import 'package:localsend_app/widget/responsive_list_view.dart';
@@ -22,9 +23,7 @@ class SelectedFilesPage extends StatelessWidget {
final selectedFiles = ref.watch(selectedSendingFilesProvider);
return Scaffold(
- appBar: AppBar(
- title: Text(t.sendTab.selection.title),
- ),
+ appBar: basicLocalSendAppbar(t.sendTab.selection.title),
body: ResponsiveListView.single(
padding: const EdgeInsets.symmetric(horizontal: 15),
tabletPadding: const EdgeInsets.symmetric(horizontal: 15),
diff --git a/app/lib/pages/send_page.dart b/app/lib/pages/send_page.dart
index ff7f1908..24f4db7b 100644
--- a/app/lib/pages/send_page.dart
+++ b/app/lib/pages/send_page.dart
@@ -12,6 +12,7 @@ import 'package:localsend_app/util/favorites.dart';
import 'package:localsend_app/util/native/taskbar_helper.dart';
import 'package:localsend_app/widget/animations/initial_fade_transition.dart';
import 'package:localsend_app/widget/animations/initial_slide_transition.dart';
+import 'package:localsend_app/widget/custom_basic_appbar.dart';
import 'package:localsend_app/widget/dialogs/error_dialog.dart';
import 'package:localsend_app/widget/list_tile/device_list_tile.dart';
import 'package:localsend_app/widget/responsive_list_view.dart';
@@ -86,7 +87,7 @@ class _SendPageState extends State with Refena {
},
canPop: true,
child: Scaffold(
- appBar: widget.showAppBar ? AppBar() : null,
+ appBar: widget.showAppBar ? basicLocalSendAppbar('') : null,
body: SafeArea(
child: Center(
child: ConstrainedBox(
diff --git a/app/lib/pages/settings/network_interfaces_page.dart b/app/lib/pages/settings/network_interfaces_page.dart
index d0638aba..d31dd802 100644
--- a/app/lib/pages/settings/network_interfaces_page.dart
+++ b/app/lib/pages/settings/network_interfaces_page.dart
@@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
import 'package:local_hero/local_hero.dart';
import 'package:localsend_app/gen/strings.g.dart';
import 'package:localsend_app/provider/settings_provider.dart';
+import 'package:localsend_app/widget/custom_basic_appbar.dart';
import 'package:localsend_app/widget/dialogs/text_field_tv.dart';
import 'package:localsend_app/widget/labeled_checkbox.dart';
import 'package:localsend_app/widget/responsive_list_view.dart';
@@ -43,9 +44,7 @@ class _NetworkInterfacesPageState extends State {
? context.notifier(settingsProvider).setNetworkWhitelist
: context.notifier(settingsProvider).setNetworkBlacklist;
return Scaffold(
- appBar: AppBar(
- title: Text(t.networkInterfacesPage.title),
- ),
+ appBar: basicLocalSendAppbar(t.networkInterfacesPage.title),
body: LocalHeroScope(
duration: const Duration(milliseconds: 200),
curve: Curves.easeInOut,
diff --git a/app/lib/pages/tabs/receive_tab.dart b/app/lib/pages/tabs/receive_tab.dart
index 8ee20e0a..38f5f15b 100644
--- a/app/lib/pages/tabs/receive_tab.dart
+++ b/app/lib/pages/tabs/receive_tab.dart
@@ -1,3 +1,4 @@
+import 'package:bitsdojo_window/bitsdojo_window.dart';
import 'package:flutter/material.dart';
import 'package:localsend_app/gen/strings.g.dart';
import 'package:localsend_app/pages/home_page.dart';
@@ -6,6 +7,7 @@ import 'package:localsend_app/pages/receive_history_page.dart';
import 'package:localsend_app/pages/tabs/receive_tab_vm.dart';
import 'package:localsend_app/provider/animation_provider.dart';
import 'package:localsend_app/util/ip_helper.dart';
+import 'package:localsend_app/util/native/platform_check.dart';
import 'package:localsend_app/widget/animations/initial_fade_transition.dart';
import 'package:localsend_app/widget/column_list_view.dart';
import 'package:localsend_app/widget/custom_icon_button.dart';
@@ -30,6 +32,9 @@ class ReceiveTab extends StatelessWidget {
return Stack(
children: [
+ checkPlatform([TargetPlatform.macOS])
+ ? SizedBox(height: 50, child: MoveWindow())
+ : SizedBox(height: 0, width: 0), // makes the top part that's not occupied by another widget draggable
Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: ResponsiveListView.defaultMaxWidth),
diff --git a/app/lib/pages/tabs/send_tab.dart b/app/lib/pages/tabs/send_tab.dart
index 43ce7933..fab7d5ab 100644
--- a/app/lib/pages/tabs/send_tab.dart
+++ b/app/lib/pages/tabs/send_tab.dart
@@ -1,3 +1,4 @@
+import 'package:bitsdojo_window/bitsdojo_window.dart';
import 'package:collection/collection.dart';
import 'package:common/model/device.dart';
import 'package:common/model/session_status.dart';
@@ -49,210 +50,221 @@ class SendTab extends StatelessWidget {
final sizingInformation = SizingInformation(MediaQuery.sizeOf(context).width);
final buttonWidth = sizingInformation.isDesktop ? BigButton.desktopWidth : BigButton.mobileWidth;
final ref = context.ref;
- return ResponsiveListView(
- padding: EdgeInsets.zero,
+ return Stack(
children: [
- const SizedBox(height: 20),
- if (vm.selectedFiles.isEmpty) ...[
- Padding(
- padding: const EdgeInsets.symmetric(horizontal: _horizontalPadding),
- child: Text(
- t.sendTab.selection.title,
- style: Theme.of(context).textTheme.titleMedium,
- ),
- ),
- HorizontalClipListView(
- outerHorizontalPadding: 15,
- outerVerticalPadding: 10,
- childPadding: 10,
- minChildWidth: buttonWidth,
- children: _options.map((option) {
- return BigButton(
- icon: option.icon,
- label: option.label,
- filled: false,
- onTap: () async => ref.global.dispatchAsync(PickFileAction(
- option: option,
- context: context,
- )),
- );
- }).toList(),
- ),
- ] else ...[
- Card(
- margin: const EdgeInsets.only(bottom: 10, left: _horizontalPadding, right: _horizontalPadding),
- child: Padding(
- padding: const EdgeInsetsDirectional.only(start: 15, top: 5, bottom: 15),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Row(
- children: [
- Text(
- t.sendTab.selection.title,
- style: Theme.of(context).textTheme.titleMedium,
- ),
- const Spacer(),
- CustomIconButton(
- onPressed: () => ref.redux(selectedSendingFilesProvider).dispatch(ClearSelectionAction()),
- child: Icon(Icons.close, color: Theme.of(context).colorScheme.secondary),
- ),
- const SizedBox(width: 5),
- ],
- ),
- const SizedBox(height: 5),
- Text(t.sendTab.selection.files(files: vm.selectedFiles.length)),
- Text(t.sendTab.selection.size(size: vm.selectedFiles.fold(0, (prev, curr) => prev + curr.size).asReadableFileSize)),
- const SizedBox(height: 10),
- SizedBox(
- height: defaultThumbnailSize,
- child: ListView.builder(
- scrollDirection: Axis.horizontal,
- itemCount: vm.selectedFiles.length,
- itemBuilder: (context, index) {
- final file = vm.selectedFiles[index];
- return Padding(
- padding: const EdgeInsets.only(right: 10),
- child: SmartFileThumbnail.fromCrossFile(file),
- );
- },
- ),
- ),
- const SizedBox(height: 10),
- Row(
- mainAxisAlignment: MainAxisAlignment.end,
- children: [
- TextButton(
- style: TextButton.styleFrom(
- foregroundColor: Theme.of(context).colorScheme.onSurface,
- ),
- onPressed: () async {
- await context.push(() => const SelectedFilesPage());
- },
- child: Text(t.general.edit),
- ),
- const SizedBox(width: 15),
- ElevatedButton.icon(
- style: ElevatedButton.styleFrom(
- backgroundColor: Theme.of(context).colorScheme.primary,
- foregroundColor: Theme.of(context).colorScheme.onPrimary,
- ),
- onPressed: () async {
- if (_options.length == 1) {
- // open directly
- await ref.global.dispatchAsync(PickFileAction(
- option: _options.first,
- context: context,
- ));
- return;
- }
- await AddFileDialog.open(
- context: context,
- options: _options,
- );
- },
- icon: const Icon(Icons.add),
- label: Text(t.general.add),
- ),
- const SizedBox(width: 15),
- ],
- ),
- ],
- ),
- ),
- ),
- ],
- Row(
+ ResponsiveListView(
+ padding: EdgeInsets.zero,
children: [
- const SizedBox(width: _horizontalPadding),
- Flexible(
- child: Padding(
- padding: const EdgeInsets.symmetric(vertical: 10),
- child: Text(t.sendTab.nearbyDevices, style: Theme.of(context).textTheme.titleMedium),
+ const SizedBox(height: 20),
+ if (vm.selectedFiles.isEmpty) ...[
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: _horizontalPadding),
+ child: Text(
+ t.sendTab.selection.title,
+ style: Theme.of(context).textTheme.titleMedium,
+ ),
+ ),
+ HorizontalClipListView(
+ outerHorizontalPadding: 15,
+ outerVerticalPadding: 10,
+ childPadding: 10,
+ minChildWidth: buttonWidth,
+ children: _options.map((option) {
+ return BigButton(
+ icon: option.icon,
+ label: option.label,
+ filled: false,
+ onTap: () async => ref.global.dispatchAsync(PickFileAction(
+ option: option,
+ context: context,
+ )),
+ );
+ }).toList(),
+ ),
+ ] else ...[
+ Card(
+ margin: const EdgeInsets.only(bottom: 10, left: _horizontalPadding, right: _horizontalPadding),
+ child: Padding(
+ padding: const EdgeInsetsDirectional.only(start: 15, top: 5, bottom: 15),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Row(
+ children: [
+ Text(
+ t.sendTab.selection.title,
+ style: Theme.of(context).textTheme.titleMedium,
+ ),
+ const Spacer(),
+ CustomIconButton(
+ onPressed: () => ref.redux(selectedSendingFilesProvider).dispatch(ClearSelectionAction()),
+ child: Icon(Icons.close, color: Theme.of(context).colorScheme.secondary),
+ ),
+ const SizedBox(width: 5),
+ ],
+ ),
+ const SizedBox(height: 5),
+ Text(t.sendTab.selection.files(files: vm.selectedFiles.length)),
+ Text(t.sendTab.selection.size(size: vm.selectedFiles.fold(0, (prev, curr) => prev + curr.size).asReadableFileSize)),
+ const SizedBox(height: 10),
+ SizedBox(
+ height: defaultThumbnailSize,
+ child: ListView.builder(
+ scrollDirection: Axis.horizontal,
+ itemCount: vm.selectedFiles.length,
+ itemBuilder: (context, index) {
+ final file = vm.selectedFiles[index];
+ return Padding(
+ padding: const EdgeInsets.only(right: 10),
+ child: SmartFileThumbnail.fromCrossFile(file),
+ );
+ },
+ ),
+ ),
+ const SizedBox(height: 10),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.end,
+ children: [
+ TextButton(
+ style: TextButton.styleFrom(
+ foregroundColor: Theme.of(context).colorScheme.onSurface,
+ ),
+ onPressed: () async {
+ await context.push(() => const SelectedFilesPage());
+ },
+ child: Text(t.general.edit),
+ ),
+ const SizedBox(width: 15),
+ ElevatedButton.icon(
+ style: ElevatedButton.styleFrom(
+ backgroundColor: Theme.of(context).colorScheme.primary,
+ foregroundColor: Theme.of(context).colorScheme.onPrimary,
+ ),
+ onPressed: () async {
+ if (_options.length == 1) {
+ // open directly
+ await ref.global.dispatchAsync(PickFileAction(
+ option: _options.first,
+ context: context,
+ ));
+ return;
+ }
+ await AddFileDialog.open(
+ context: context,
+ options: _options,
+ );
+ },
+ icon: const Icon(Icons.add),
+ label: Text(t.general.add),
+ ),
+ const SizedBox(width: 15),
+ ],
+ ),
+ ],
+ ),
+ ),
+ ),
+ ],
+ Row(
+ children: [
+ const SizedBox(width: _horizontalPadding),
+ Flexible(
+ child: Padding(
+ padding: const EdgeInsets.symmetric(vertical: 10),
+ child: Text(t.sendTab.nearbyDevices, style: Theme.of(context).textTheme.titleMedium),
+ ),
+ ),
+ const SizedBox(width: 10),
+ _ScanButton(
+ ips: vm.localIps,
+ ),
+ Tooltip(
+ message: t.sendTab.manualSending,
+ child: CustomIconButton(
+ onPressed: () async => vm.onTapAddress(context),
+ child: const Icon(Icons.ads_click),
+ ),
+ ),
+ Tooltip(
+ message: t.dialogs.favoriteDialog.title,
+ child: CustomIconButton(
+ onPressed: () async => await vm.onTapFavorite(context),
+ child: const Icon(Icons.favorite),
+ ),
+ ),
+ _SendModeButton(
+ onSelect: (mode) async => vm.onTapSendMode(context, mode),
+ ),
+ ],
+ ),
+ if (vm.nearbyDevices.isEmpty)
+ const Padding(
+ padding: EdgeInsets.only(bottom: 10, left: _horizontalPadding, right: _horizontalPadding),
+ child: Opacity(
+ opacity: 0.3,
+ child: DevicePlaceholderListTile(),
+ ),
+ ),
+ ...vm.nearbyDevices.map((device) {
+ final favoriteEntry = vm.favoriteDevices.findDevice(device);
+ return Padding(
+ padding: const EdgeInsets.only(bottom: 10, left: _horizontalPadding, right: _horizontalPadding),
+ child: Hero(
+ tag: 'device-${device.ip}',
+ child: vm.sendMode == SendMode.multiple
+ ? _MultiSendDeviceListTile(
+ device: device,
+ isFavorite: favoriteEntry != null,
+ nameOverride: favoriteEntry?.alias,
+ vm: vm,
+ )
+ : DeviceListTile(
+ device: device,
+ isFavorite: favoriteEntry != null,
+ nameOverride: favoriteEntry?.alias,
+ onFavoriteTap: () async => await vm.onToggleFavorite(context, device),
+ onTap: () async => await vm.onTapDevice(context, device),
+ ),
+ ),
+ );
+ }),
+ const SizedBox(height: 10),
+ Center(
+ child: TextButton(
+ onPressed: () async {
+ await context.push(() => const TroubleshootPage());
+ },
+ child: Text(t.troubleshootPage.title),
),
),
- const SizedBox(width: 10),
- _ScanButton(
- ips: vm.localIps,
- ),
- Tooltip(
- message: t.sendTab.manualSending,
- child: CustomIconButton(
- onPressed: () async => vm.onTapAddress(context),
- child: const Icon(Icons.ads_click),
+ const SizedBox(height: 20),
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: _horizontalPadding),
+ child: Consumer(
+ builder: (context, ref) {
+ final animations = ref.watch(animationProvider);
+ return OpacitySlideshow(
+ durationMillis: 6000,
+ running: animations,
+ children: [
+ Text(t.sendTab.help, style: const TextStyle(color: Colors.grey), textAlign: TextAlign.center),
+ if (checkPlatformCanReceiveShareIntent())
+ Text(t.sendTab.shareIntentInfo, style: const TextStyle(color: Colors.grey), textAlign: TextAlign.center),
+ ],
+ );
+ },
),
),
- Tooltip(
- message: t.dialogs.favoriteDialog.title,
- child: CustomIconButton(
- onPressed: () async => await vm.onTapFavorite(context),
- child: const Icon(Icons.favorite),
- ),
- ),
- _SendModeButton(
- onSelect: (mode) async => vm.onTapSendMode(context, mode),
- ),
+ const SizedBox(height: 50),
],
),
- if (vm.nearbyDevices.isEmpty)
- const Padding(
- padding: EdgeInsets.only(bottom: 10, left: _horizontalPadding, right: _horizontalPadding),
- child: Opacity(
- opacity: 0.3,
- child: DevicePlaceholderListTile(),
- ),
- ),
- ...vm.nearbyDevices.map((device) {
- final favoriteEntry = vm.favoriteDevices.findDevice(device);
- return Padding(
- padding: const EdgeInsets.only(bottom: 10, left: _horizontalPadding, right: _horizontalPadding),
- child: Hero(
- tag: 'device-${device.ip}',
- child: vm.sendMode == SendMode.multiple
- ? _MultiSendDeviceListTile(
- device: device,
- isFavorite: favoriteEntry != null,
- nameOverride: favoriteEntry?.alias,
- vm: vm,
- )
- : DeviceListTile(
- device: device,
- isFavorite: favoriteEntry != null,
- nameOverride: favoriteEntry?.alias,
- onFavoriteTap: () async => await vm.onToggleFavorite(context, device),
- onTap: () async => await vm.onTapDevice(context, device),
- ),
- ),
- );
- }),
- const SizedBox(height: 10),
- Center(
- child: TextButton(
- onPressed: () async {
- await context.push(() => const TroubleshootPage());
- },
- child: Text(t.troubleshootPage.title),
- ),
- ),
- const SizedBox(height: 20),
- Padding(
- padding: const EdgeInsets.symmetric(horizontal: _horizontalPadding),
- child: Consumer(
- builder: (context, ref) {
- final animations = ref.watch(animationProvider);
- return OpacitySlideshow(
- durationMillis: 6000,
- running: animations,
- children: [
- Text(t.sendTab.help, style: const TextStyle(color: Colors.grey), textAlign: TextAlign.center),
- if (checkPlatformCanReceiveShareIntent())
- Text(t.sendTab.shareIntentInfo, style: const TextStyle(color: Colors.grey), textAlign: TextAlign.center),
- ],
- );
- },
- ),
- ),
- const SizedBox(height: 50),
+ // make the top draggable on Desktop
+ checkPlatform([TargetPlatform.macOS])
+ ? SizedBox(height: 50, child: MoveWindow())
+ : SizedBox(
+ height: 0,
+ width: 0,
+ ),
],
);
},
diff --git a/app/lib/pages/tabs/settings_tab.dart b/app/lib/pages/tabs/settings_tab.dart
index 6ca7f250..96c3805a 100644
--- a/app/lib/pages/tabs/settings_tab.dart
+++ b/app/lib/pages/tabs/settings_tab.dart
@@ -1,4 +1,6 @@
import 'dart:io';
+import 'dart:ui';
+import 'package:bitsdojo_window/bitsdojo_window.dart';
import 'package:common/constants.dart';
import 'package:common/model/device.dart';
import 'package:flutter/foundation.dart';
@@ -42,525 +44,552 @@ class SettingsTab extends StatelessWidget {
provider: settingsTabControllerProvider,
builder: (context, vm) {
final ref = context.ref;
- return ResponsiveListView(
- padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 40),
+ return Stack(
children: [
Padding(
- padding: const EdgeInsets.only(left: 8),
- child: Text(t.settingsTab.title, style: Theme.of(context).textTheme.titleLarge, textAlign: TextAlign.center),
- ),
- const SizedBox(height: 30),
- _SettingsSection(
- title: t.settingsTab.general.title,
- children: [
- _SettingsEntry(
- label: t.settingsTab.general.brightness,
- child: CustomDropdownButton(
- value: vm.settings.theme,
- items: vm.themeModes.map((theme) {
- return DropdownMenuItem(
- value: theme,
- alignment: Alignment.center,
- child: Text(theme.humanName),
- );
- }).toList(),
- onChanged: (theme) => vm.onChangeTheme(context, theme),
- ),
- ),
- _SettingsEntry(
- label: t.settingsTab.general.color,
- child: CustomDropdownButton(
- value: vm.settings.colorMode,
- items: vm.colorModes.map((colorMode) {
- return DropdownMenuItem(
- value: colorMode,
- alignment: Alignment.center,
- child: Text(colorMode.humanName),
- );
- }).toList(),
- onChanged: vm.onChangeColorMode,
- ),
- ),
- _ButtonEntry(
- label: t.settingsTab.general.language,
- buttonLabel: vm.settings.locale?.humanName ?? t.settingsTab.general.languageOptions.system,
- onTap: () => vm.onTapLanguage(context),
- ),
- if (checkPlatformIsDesktop()) ...[
- /// Wayland does window position handling, so there's no need for it. See [https://github.com/localsend/localsend/issues/544]
- if (vm.advanced && checkPlatformIsNotWaylandDesktop())
- _BooleanEntry(
- label: defaultTargetPlatform == TargetPlatform.windows
- ? t.settingsTab.general.saveWindowPlacementWindows
- : t.settingsTab.general.saveWindowPlacement,
- value: vm.settings.saveWindowPlacement,
- onChanged: (b) async {
- await ref.notifier(settingsProvider).setSaveWindowPlacement(b);
- },
- ),
- if (checkPlatformHasTray()) ...[
- _BooleanEntry(
- label: t.settingsTab.general.minimizeToTray,
- value: vm.settings.minimizeToTray,
- onChanged: (b) async {
- await ref.notifier(settingsProvider).setMinimizeToTray(b);
- },
- ),
- ],
- if (checkPlatformIsDesktop()) ...[
- _BooleanEntry(
- label: t.settingsTab.general.launchAtStartup,
- value: vm.autoStart,
- onChanged: (_) => vm.onToggleAutoStart(context),
- ),
- Visibility(
- visible: vm.autoStart,
- maintainAnimation: true,
- maintainState: true,
- child: AnimatedOpacity(
- opacity: vm.autoStart ? 1.0 : 0.0,
- duration: const Duration(milliseconds: 500),
- child: _BooleanEntry(
- label: t.settingsTab.general.launchMinimized,
- value: vm.autoStartLaunchHidden,
- onChanged: (_) => vm.onToggleAutoStartLaunchHidden(context),
- ),
- ),
- ),
- ],
- if (vm.advanced && checkPlatform([TargetPlatform.windows])) ...[
- _BooleanEntry(
- label: t.settingsTab.general.showInContextMenu,
- value: vm.showInContextMenu,
- onChanged: (_) => vm.onToggleShowInContextMenu(context),
- ),
- ],
- ],
- _BooleanEntry(
- label: t.settingsTab.general.animations,
- value: vm.settings.enableAnimations,
- onChanged: (b) async {
- await ref.notifier(settingsProvider).setEnableAnimations(b);
- },
- ),
- ],
- ),
- _SettingsSection(
- title: t.settingsTab.receive.title,
- children: [
- _BooleanEntry(
- label: t.settingsTab.receive.quickSave,
- value: vm.settings.quickSave,
- onChanged: (b) async {
- final old = vm.settings.quickSave;
- await ref.notifier(settingsProvider).setQuickSave(b);
- if (!old && b && context.mounted) {
- await QuickSaveNotice.open(context);
- }
- },
- ),
- _BooleanEntry(
- label: t.settingsTab.receive.quickSaveFromFavorites,
- value: vm.settings.quickSaveFromFavorites,
- onChanged: (b) async {
- final old = vm.settings.quickSaveFromFavorites;
- await ref.notifier(settingsProvider).setQuickSaveFromFavorites(b);
- if (!old && b && context.mounted) {
- await QuickSaveFromFavoritesNotice.open(context);
- }
- },
- ),
- _BooleanEntry(
- label: t.settingsTab.receive.requirePin,
- value: vm.settings.receivePin != null,
- onChanged: (b) async {
- final currentPIN = vm.settings.receivePin;
- if (currentPIN != null) {
- await ref.notifier(settingsProvider).setReceivePin(null);
- } else {
- final String? newPin = await showDialog(
- context: context,
- builder: (_) => const PinDialog(
- obscureText: false,
- generateRandom: false,
- ),
- );
-
- if (newPin != null && newPin.isNotEmpty) {
- await ref.notifier(settingsProvider).setReceivePin(newPin);
- }
- }
- },
- ),
- if (checkPlatformWithFileSystem())
- _SettingsEntry(
- label: t.settingsTab.receive.destination,
- child: TextButton(
- style: TextButton.styleFrom(
- backgroundColor: Theme.of(context).inputDecorationTheme.fillColor,
- shape: RoundedRectangleBorder(borderRadius: Theme.of(context).inputDecorationTheme.borderRadius),
- foregroundColor: Theme.of(context).colorScheme.onSurface,
- ),
- onPressed: () async {
- if (vm.settings.destination != null) {
- await ref.notifier(settingsProvider).setDestination(null);
- if (defaultTargetPlatform == TargetPlatform.macOS) {
- await removeExistingDestinationAccess();
- }
- return;
- }
-
- final directory = await pickDirectoryPath();
- if (directory != null) {
- if (defaultTargetPlatform == TargetPlatform.macOS) {
- await persistDestinationFolderAccess(directory);
- }
- await ref.notifier(settingsProvider).setDestination(directory);
- }
- },
- child: Padding(
- padding: const EdgeInsets.symmetric(vertical: 5),
- child: Text(vm.settings.destination ?? t.settingsTab.receive.downloads, style: Theme.of(context).textTheme.titleMedium),
- ),
- ),
- ),
- if (checkPlatformWithGallery())
- _BooleanEntry(
- label: t.settingsTab.receive.saveToGallery,
- value: vm.settings.saveToGallery,
- onChanged: (b) async {
- await ref.notifier(settingsProvider).setSaveToGallery(b);
- },
- ),
- _BooleanEntry(
- label: t.settingsTab.receive.autoFinish,
- value: vm.settings.autoFinish,
- onChanged: (b) async {
- await ref.notifier(settingsProvider).setAutoFinish(b);
- },
- ),
- _BooleanEntry(
- label: t.settingsTab.receive.saveToHistory,
- value: vm.settings.saveToHistory,
- onChanged: (b) async {
- await ref.notifier(settingsProvider).setSaveToHistory(b);
- },
- ),
- ],
- ),
- if (vm.advanced)
- _SettingsSection(
- title: t.settingsTab.send.title,
+ padding: EdgeInsets.only(
+ right: MediaQuery.of(context).padding.right), // So camera or 3-button navigation doesn't interfere on the right, rest is handled
+ child: ResponsiveListView(
+ padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 40),
children: [
- _BooleanEntry(
- label: t.settingsTab.send.shareViaLinkAutoAccept,
- value: vm.settings.shareViaLinkAutoAccept,
- onChanged: (b) async {
- await ref.notifier(settingsProvider).setShareViaLinkAutoAccept(b);
- },
- ),
- ],
- ),
- _SettingsSection(
- title: t.settingsTab.network.title,
- children: [
- AnimatedCrossFade(
- crossFadeState: vm.serverState != null &&
- (vm.serverState!.alias != vm.settings.alias ||
- vm.serverState!.port != vm.settings.port ||
- vm.serverState!.https != vm.settings.https)
- ? CrossFadeState.showSecond
- : CrossFadeState.showFirst,
- duration: const Duration(milliseconds: 200),
- alignment: Alignment.topLeft,
- firstChild: Container(),
- secondChild: Padding(
- padding: const EdgeInsets.only(bottom: 15),
- child: Text(t.settingsTab.network.needRestart, style: TextStyle(color: Theme.of(context).colorScheme.warning)),
- ),
- ),
- _SettingsEntry(
- label: '${t.settingsTab.network.server}${vm.serverState == null ? ' (${t.general.offline})' : ''}',
- child: DecoratedBox(
- decoration: BoxDecoration(
- color: Theme.of(context).inputDecorationTheme.fillColor,
- borderRadius: Theme.of(context).inputDecorationTheme.borderRadius,
- ),
- child: Row(
- mainAxisAlignment: MainAxisAlignment.spaceEvenly,
- children: [
- if (vm.serverState == null)
- Tooltip(
- message: t.general.start,
- child: TextButton(
- style: TextButton.styleFrom(foregroundColor: Theme.of(context).colorScheme.onSurface),
- onPressed: () => vm.onTapStartServer(context),
- child: const Icon(Icons.play_arrow),
- ),
- )
- else
- Tooltip(
- message: t.general.restart,
- child: TextButton(
- style: TextButton.styleFrom(foregroundColor: Theme.of(context).colorScheme.onSurface),
- onPressed: () => vm.onTapRestartServer(context),
- child: const Icon(Icons.refresh),
+ SizedBox(height: 30 + MediaQuery.of(context).padding.top),
+ _SettingsSection(
+ title: t.settingsTab.general.title,
+ children: [
+ _SettingsEntry(
+ label: t.settingsTab.general.brightness,
+ child: CustomDropdownButton(
+ value: vm.settings.theme,
+ items: vm.themeModes.map((theme) {
+ return DropdownMenuItem(
+ value: theme,
+ alignment: Alignment.center,
+ child: Text(theme.humanName),
+ );
+ }).toList(),
+ onChanged: (theme) => vm.onChangeTheme(context, theme),
+ ),
+ ),
+ _SettingsEntry(
+ label: t.settingsTab.general.color,
+ child: CustomDropdownButton(
+ value: vm.settings.colorMode,
+ items: vm.colorModes.map((colorMode) {
+ return DropdownMenuItem(
+ value: colorMode,
+ alignment: Alignment.center,
+ child: Text(colorMode.humanName),
+ );
+ }).toList(),
+ onChanged: vm.onChangeColorMode,
+ ),
+ ),
+ _ButtonEntry(
+ label: t.settingsTab.general.language,
+ buttonLabel: vm.settings.locale?.humanName ?? t.settingsTab.general.languageOptions.system,
+ onTap: () => vm.onTapLanguage(context),
+ ),
+ if (checkPlatformIsDesktop()) ...[
+ /// Wayland does window position handling, so there's no need for it. See [https://github.com/localsend/localsend/issues/544]
+ if (vm.advanced && checkPlatformIsNotWaylandDesktop())
+ _BooleanEntry(
+ label: defaultTargetPlatform == TargetPlatform.windows
+ ? t.settingsTab.general.saveWindowPlacementWindows
+ : t.settingsTab.general.saveWindowPlacement,
+ value: vm.settings.saveWindowPlacement,
+ onChanged: (b) async {
+ await ref.notifier(settingsProvider).setSaveWindowPlacement(b);
+ },
+ ),
+ if (checkPlatformHasTray()) ...[
+ _BooleanEntry(
+ label: t.settingsTab.general.minimizeToTray,
+ value: vm.settings.minimizeToTray,
+ onChanged: (b) async {
+ await ref.notifier(settingsProvider).setMinimizeToTray(b);
+ },
+ ),
+ ],
+ if (checkPlatformIsDesktop()) ...[
+ _BooleanEntry(
+ label: t.settingsTab.general.launchAtStartup,
+ value: vm.autoStart,
+ onChanged: (_) => vm.onToggleAutoStart(context),
+ ),
+ Visibility(
+ visible: vm.autoStart,
+ maintainAnimation: true,
+ maintainState: true,
+ child: AnimatedOpacity(
+ opacity: vm.autoStart ? 1.0 : 0.0,
+ duration: const Duration(milliseconds: 500),
+ child: _BooleanEntry(
+ label: t.settingsTab.general.launchMinimized,
+ value: vm.autoStartLaunchHidden,
+ onChanged: (_) => vm.onToggleAutoStartLaunchHidden(context),
+ ),
),
),
- Tooltip(
- message: t.general.stop,
+ ],
+ if (vm.advanced && checkPlatform([TargetPlatform.windows])) ...[
+ _BooleanEntry(
+ label: t.settingsTab.general.showInContextMenu,
+ value: vm.showInContextMenu,
+ onChanged: (_) => vm.onToggleShowInContextMenu(context),
+ ),
+ ],
+ ],
+ _BooleanEntry(
+ label: t.settingsTab.general.animations,
+ value: vm.settings.enableAnimations,
+ onChanged: (b) async {
+ await ref.notifier(settingsProvider).setEnableAnimations(b);
+ },
+ ),
+ ],
+ ),
+ _SettingsSection(
+ title: t.settingsTab.receive.title,
+ children: [
+ _BooleanEntry(
+ label: t.settingsTab.receive.quickSave,
+ value: vm.settings.quickSave,
+ onChanged: (b) async {
+ final old = vm.settings.quickSave;
+ await ref.notifier(settingsProvider).setQuickSave(b);
+ if (!old && b && context.mounted) {
+ await QuickSaveNotice.open(context);
+ }
+ },
+ ),
+ _BooleanEntry(
+ label: t.settingsTab.receive.quickSaveFromFavorites,
+ value: vm.settings.quickSaveFromFavorites,
+ onChanged: (b) async {
+ final old = vm.settings.quickSaveFromFavorites;
+ await ref.notifier(settingsProvider).setQuickSaveFromFavorites(b);
+ if (!old && b && context.mounted) {
+ await QuickSaveFromFavoritesNotice.open(context);
+ }
+ },
+ ),
+ _BooleanEntry(
+ label: t.settingsTab.receive.requirePin,
+ value: vm.settings.receivePin != null,
+ onChanged: (b) async {
+ final currentPIN = vm.settings.receivePin;
+ if (currentPIN != null) {
+ await ref.notifier(settingsProvider).setReceivePin(null);
+ } else {
+ final String? newPin = await showDialog(
+ context: context,
+ builder: (_) => const PinDialog(
+ obscureText: false,
+ generateRandom: false,
+ ),
+ );
+
+ if (newPin != null && newPin.isNotEmpty) {
+ await ref.notifier(settingsProvider).setReceivePin(newPin);
+ }
+ }
+ },
+ ),
+ if (checkPlatformWithFileSystem())
+ _SettingsEntry(
+ label: t.settingsTab.receive.destination,
child: TextButton(
- style: TextButton.styleFrom(foregroundColor: Theme.of(context).colorScheme.onSurface),
- onPressed: vm.serverState == null ? null : vm.onTapStopServer,
- child: const Icon(Icons.stop),
+ style: TextButton.styleFrom(
+ backgroundColor: Theme.of(context).inputDecorationTheme.fillColor,
+ shape: RoundedRectangleBorder(borderRadius: Theme.of(context).inputDecorationTheme.borderRadius),
+ foregroundColor: Theme.of(context).colorScheme.onSurface,
+ ),
+ onPressed: () async {
+ if (vm.settings.destination != null) {
+ await ref.notifier(settingsProvider).setDestination(null);
+ if (defaultTargetPlatform == TargetPlatform.macOS) {
+ await removeExistingDestinationAccess();
+ }
+ return;
+ }
+
+ final directory = await pickDirectoryPath();
+ if (directory != null) {
+ if (defaultTargetPlatform == TargetPlatform.macOS) {
+ await persistDestinationFolderAccess(directory);
+ }
+ await ref.notifier(settingsProvider).setDestination(directory);
+ }
+ },
+ child: Padding(
+ padding: const EdgeInsets.symmetric(vertical: 5),
+ child: Text(vm.settings.destination ?? t.settingsTab.receive.downloads, style: Theme.of(context).textTheme.titleMedium),
+ ),
),
),
+ if (checkPlatformWithGallery())
+ _BooleanEntry(
+ label: t.settingsTab.receive.saveToGallery,
+ value: vm.settings.saveToGallery,
+ onChanged: (b) async {
+ await ref.notifier(settingsProvider).setSaveToGallery(b);
+ },
+ ),
+ _BooleanEntry(
+ label: t.settingsTab.receive.autoFinish,
+ value: vm.settings.autoFinish,
+ onChanged: (b) async {
+ await ref.notifier(settingsProvider).setAutoFinish(b);
+ },
+ ),
+ _BooleanEntry(
+ label: t.settingsTab.receive.saveToHistory,
+ value: vm.settings.saveToHistory,
+ onChanged: (b) async {
+ await ref.notifier(settingsProvider).setSaveToHistory(b);
+ },
+ ),
+ ],
+ ),
+ if (vm.advanced)
+ _SettingsSection(
+ title: t.settingsTab.send.title,
+ children: [
+ _BooleanEntry(
+ label: t.settingsTab.send.shareViaLinkAutoAccept,
+ value: vm.settings.shareViaLinkAutoAccept,
+ onChanged: (b) async {
+ await ref.notifier(settingsProvider).setShareViaLinkAutoAccept(b);
+ },
+ ),
],
),
- ),
- ),
- _SettingsEntry(
- label: t.settingsTab.network.alias,
- child: TextFieldWithActions(
- name: t.settingsTab.network.alias,
- controller: vm.aliasController,
- onChanged: (s) async {
- await ref.notifier(settingsProvider).setAlias(s);
- },
- actions: [
- Tooltip(
- message: t.settingsTab.network.generateRandomAlias,
- child: IconButton(
- onPressed: () async {
- // Generates random alias
- final newAlias = generateRandomAlias();
-
- // Update the TextField with the new alias
- vm.aliasController.text = newAlias;
-
- // Persist the new alias using the settingsProvider
- await ref.notifier(settingsProvider).setAlias(newAlias);
- },
- icon: const Icon(Icons.casino),
+ _SettingsSection(
+ title: t.settingsTab.network.title,
+ children: [
+ AnimatedCrossFade(
+ crossFadeState: vm.serverState != null &&
+ (vm.serverState!.alias != vm.settings.alias ||
+ vm.serverState!.port != vm.settings.port ||
+ vm.serverState!.https != vm.settings.https)
+ ? CrossFadeState.showSecond
+ : CrossFadeState.showFirst,
+ duration: const Duration(milliseconds: 200),
+ alignment: Alignment.topLeft,
+ firstChild: Container(),
+ secondChild: Padding(
+ padding: const EdgeInsets.only(bottom: 15),
+ child: Text(t.settingsTab.network.needRestart, style: TextStyle(color: Theme.of(context).colorScheme.warning)),
),
),
- Tooltip(
- message: t.settingsTab.network.useSystemName,
- child: IconButton(
- onPressed: () async {
- // Uses dart.io to find the systems hostname
- final newAlias = Platform.localHostname;
-
- vm.aliasController.text = newAlias;
- await ref.notifier(settingsProvider).setAlias(newAlias);
+ _SettingsEntry(
+ label: '${t.settingsTab.network.server}${vm.serverState == null ? ' (${t.general.offline})' : ''}',
+ child: DecoratedBox(
+ decoration: BoxDecoration(
+ color: Theme.of(context).inputDecorationTheme.fillColor,
+ borderRadius: Theme.of(context).inputDecorationTheme.borderRadius,
+ ),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceEvenly,
+ children: [
+ if (vm.serverState == null)
+ Tooltip(
+ message: t.general.start,
+ child: TextButton(
+ style: TextButton.styleFrom(foregroundColor: Theme.of(context).colorScheme.onSurface),
+ onPressed: () => vm.onTapStartServer(context),
+ child: const Icon(Icons.play_arrow),
+ ),
+ )
+ else
+ Tooltip(
+ message: t.general.restart,
+ child: TextButton(
+ style: TextButton.styleFrom(foregroundColor: Theme.of(context).colorScheme.onSurface),
+ onPressed: () => vm.onTapRestartServer(context),
+ child: const Icon(Icons.refresh),
+ ),
+ ),
+ Tooltip(
+ message: t.general.stop,
+ child: TextButton(
+ style: TextButton.styleFrom(foregroundColor: Theme.of(context).colorScheme.onSurface),
+ onPressed: vm.serverState == null ? null : vm.onTapStopServer,
+ child: const Icon(Icons.stop),
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ _SettingsEntry(
+ label: t.settingsTab.network.alias,
+ child: TextFieldWithActions(
+ name: t.settingsTab.network.alias,
+ controller: vm.aliasController,
+ onChanged: (s) async {
+ await ref.notifier(settingsProvider).setAlias(s);
},
- icon: const Icon(Icons.desktop_windows_rounded),
+ actions: [
+ Tooltip(
+ message: t.settingsTab.network.generateRandomAlias,
+ child: IconButton(
+ onPressed: () async {
+ // Generates random alias
+ final newAlias = generateRandomAlias();
+
+ // Update the TextField with the new alias
+ vm.aliasController.text = newAlias;
+
+ // Persist the new alias using the settingsProvider
+ await ref.notifier(settingsProvider).setAlias(newAlias);
+ },
+ icon: const Icon(Icons.casino),
+ ),
+ ),
+ Tooltip(
+ message: t.settingsTab.network.useSystemName,
+ child: IconButton(
+ onPressed: () async {
+ // Uses dart.io to find the systems hostname
+ final newAlias = Platform.localHostname;
+
+ vm.aliasController.text = newAlias;
+ await ref.notifier(settingsProvider).setAlias(newAlias);
+ },
+ icon: const Icon(Icons.desktop_windows_rounded),
+ ),
+ ),
+ ],
+ ),
+ ),
+ if (vm.advanced)
+ _SettingsEntry(
+ label: t.settingsTab.network.deviceType,
+ child: CustomDropdownButton(
+ value: vm.deviceInfo.deviceType,
+ items: DeviceType.values.map((type) {
+ return DropdownMenuItem(
+ value: type,
+ alignment: Alignment.center,
+ child: Icon(type.icon),
+ );
+ }).toList(),
+ onChanged: (type) async {
+ await ref.notifier(settingsProvider).setDeviceType(type);
+ },
+ ),
+ ),
+ if (vm.advanced)
+ _SettingsEntry(
+ label: t.settingsTab.network.deviceModel,
+ child: TextFieldTv(
+ name: t.settingsTab.network.deviceModel,
+ controller: vm.deviceModelController,
+ onChanged: (s) async {
+ await ref.notifier(settingsProvider).setDeviceModel(s);
+ },
+ ),
+ ),
+ if (vm.advanced)
+ _SettingsEntry(
+ label: t.settingsTab.network.port,
+ child: TextFieldTv(
+ name: t.settingsTab.network.port,
+ controller: vm.portController,
+ onChanged: (s) async {
+ final port = int.tryParse(s);
+ if (port != null) {
+ await ref.notifier(settingsProvider).setPort(port);
+ }
+ },
+ ),
+ ),
+ if (vm.advanced)
+ _ButtonEntry(
+ label: t.settingsTab.network.network,
+ buttonLabel: switch (vm.settings.networkWhitelist != null || vm.settings.networkBlacklist != null) {
+ true => t.settingsTab.network.networkOptions.filtered,
+ false => t.settingsTab.network.networkOptions.all,
+ },
+ onTap: () async {
+ await context.push(() => const NetworkInterfacesPage());
+ },
+ ),
+ if (vm.advanced)
+ _SettingsEntry(
+ label: t.settingsTab.network.discoveryTimeout,
+ child: TextFieldTv(
+ name: t.settingsTab.network.discoveryTimeout,
+ controller: vm.timeoutController,
+ onChanged: (s) async {
+ final timeout = int.tryParse(s);
+ if (timeout != null) {
+ await ref.notifier(settingsProvider).setDiscoveryTimeout(timeout);
+ }
+ },
+ ),
+ ),
+ if (vm.advanced)
+ _BooleanEntry(
+ label: t.settingsTab.network.encryption,
+ value: vm.settings.https,
+ onChanged: (b) async {
+ final old = vm.settings.https;
+ await ref.notifier(settingsProvider).setHttps(b);
+ if (old && !b && context.mounted) {
+ await EncryptionDisabledNotice.open(context);
+ }
+ },
+ ),
+ if (vm.advanced)
+ _SettingsEntry(
+ label: t.settingsTab.network.multicastGroup,
+ child: TextFieldTv(
+ name: t.settingsTab.network.multicastGroup,
+ controller: vm.multicastController,
+ onChanged: (s) async {
+ await ref.notifier(settingsProvider).setMulticastGroup(s);
+ },
+ ),
+ ),
+ AnimatedCrossFade(
+ crossFadeState: vm.settings.port != defaultPort ? CrossFadeState.showSecond : CrossFadeState.showFirst,
+ duration: const Duration(milliseconds: 200),
+ alignment: Alignment.topLeft,
+ firstChild: Container(),
+ secondChild: Padding(
+ padding: const EdgeInsets.only(bottom: 15),
+ child: Text(
+ t.settingsTab.network.portWarning(defaultPort: defaultPort),
+ style: const TextStyle(color: Colors.grey),
+ ),
+ ),
+ ),
+ AnimatedCrossFade(
+ crossFadeState: vm.settings.multicastGroup != defaultMulticastGroup ? CrossFadeState.showSecond : CrossFadeState.showFirst,
+ duration: const Duration(milliseconds: 200),
+ alignment: Alignment.topLeft,
+ firstChild: Container(),
+ secondChild: Padding(
+ padding: const EdgeInsets.only(bottom: 15),
+ child: Text(
+ t.settingsTab.network.multicastGroupWarning(defaultMulticast: defaultMulticastGroup),
+ style: const TextStyle(color: Colors.grey),
+ ),
),
),
],
),
- ),
- if (vm.advanced)
- _SettingsEntry(
- label: t.settingsTab.network.deviceType,
- child: CustomDropdownButton(
- value: vm.deviceInfo.deviceType,
- items: DeviceType.values.map((type) {
- return DropdownMenuItem(
- value: type,
- alignment: Alignment.center,
- child: Icon(type.icon),
- );
- }).toList(),
- onChanged: (type) async {
- await ref.notifier(settingsProvider).setDeviceType(type);
- },
- ),
+ _SettingsSection(
+ title: t.settingsTab.other.title,
+ padding: const EdgeInsets.only(bottom: 0),
+ children: [
+ _ButtonEntry(
+ label: t.aboutPage.title,
+ buttonLabel: t.general.open,
+ onTap: () async {
+ await context.push(() => const AboutPage());
+ },
+ ),
+ _ButtonEntry(
+ label: t.settingsTab.other.support,
+ buttonLabel: t.settingsTab.other.donate,
+ onTap: () async {
+ await context.push(() => const DonationPage());
+ },
+ ),
+ _ButtonEntry(
+ label: t.settingsTab.other.privacyPolicy,
+ buttonLabel: t.general.open,
+ onTap: () async {
+ await launchUrl(
+ Uri.parse('https://localsend.org/privacy'),
+ mode: LaunchMode.externalApplication,
+ );
+ },
+ ),
+ if (checkPlatform([TargetPlatform.iOS, TargetPlatform.macOS]))
+ _ButtonEntry(
+ label: t.settingsTab.other.termsOfUse,
+ buttonLabel: t.general.open,
+ onTap: () async {
+ await launchUrl(
+ Uri.parse('https://www.apple.com/legal/internet-services/itunes/dev/stdeula/'),
+ mode: LaunchMode.externalApplication,
+ );
+ },
+ ),
+ ],
),
- if (vm.advanced)
- _SettingsEntry(
- label: t.settingsTab.network.deviceModel,
- child: TextFieldTv(
- name: t.settingsTab.network.deviceModel,
- controller: vm.deviceModelController,
- onChanged: (s) async {
- await ref.notifier(settingsProvider).setDeviceModel(s);
- },
- ),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.end,
+ children: [
+ LabeledCheckbox(
+ label: t.settingsTab.advancedSettings,
+ value: vm.advanced,
+ labelFirst: true,
+ onChanged: (b) async {
+ vm.onTapAdvanced(b == true);
+ await ref.notifier(settingsProvider).setAdvancedSettingsEnabled(b == true);
+ },
+ ),
+ const SizedBox(width: 10),
+ ],
),
- if (vm.advanced)
- _SettingsEntry(
- label: t.settingsTab.network.port,
- child: TextFieldTv(
- name: t.settingsTab.network.port,
- controller: vm.portController,
- onChanged: (s) async {
- final port = int.tryParse(s);
- if (port != null) {
- await ref.notifier(settingsProvider).setPort(port);
- }
- },
- ),
- ),
- if (vm.advanced)
- _ButtonEntry(
- label: t.settingsTab.network.network,
- buttonLabel: switch (vm.settings.networkWhitelist != null || vm.settings.networkBlacklist != null) {
- true => t.settingsTab.network.networkOptions.filtered,
- false => t.settingsTab.network.networkOptions.all,
- },
- onTap: () async {
- await context.push(() => const NetworkInterfacesPage());
- },
- ),
- if (vm.advanced)
- _SettingsEntry(
- label: t.settingsTab.network.discoveryTimeout,
- child: TextFieldTv(
- name: t.settingsTab.network.discoveryTimeout,
- controller: vm.timeoutController,
- onChanged: (s) async {
- final timeout = int.tryParse(s);
- if (timeout != null) {
- await ref.notifier(settingsProvider).setDiscoveryTimeout(timeout);
- }
- },
- ),
- ),
- if (vm.advanced)
- _BooleanEntry(
- label: t.settingsTab.network.encryption,
- value: vm.settings.https,
- onChanged: (b) async {
- final old = vm.settings.https;
- await ref.notifier(settingsProvider).setHttps(b);
- if (old && !b && context.mounted) {
- await EncryptionDisabledNotice.open(context);
- }
- },
- ),
- if (vm.advanced)
- _SettingsEntry(
- label: t.settingsTab.network.multicastGroup,
- child: TextFieldTv(
- name: t.settingsTab.network.multicastGroup,
- controller: vm.multicastController,
- onChanged: (s) async {
- await ref.notifier(settingsProvider).setMulticastGroup(s);
- },
- ),
- ),
- AnimatedCrossFade(
- crossFadeState: vm.settings.port != defaultPort ? CrossFadeState.showSecond : CrossFadeState.showFirst,
- duration: const Duration(milliseconds: 200),
- alignment: Alignment.topLeft,
- firstChild: Container(),
- secondChild: Padding(
- padding: const EdgeInsets.only(bottom: 15),
- child: Text(
- t.settingsTab.network.portWarning(defaultPort: defaultPort),
- style: const TextStyle(color: Colors.grey),
- ),
- ),
- ),
- AnimatedCrossFade(
- crossFadeState: vm.settings.multicastGroup != defaultMulticastGroup ? CrossFadeState.showSecond : CrossFadeState.showFirst,
- duration: const Duration(milliseconds: 200),
- alignment: Alignment.topLeft,
- firstChild: Container(),
- secondChild: Padding(
- padding: const EdgeInsets.only(bottom: 15),
- child: Text(
- t.settingsTab.network.multicastGroupWarning(defaultMulticast: defaultMulticastGroup),
- style: const TextStyle(color: Colors.grey),
- ),
- ),
- ),
- ],
- ),
- _SettingsSection(
- title: t.settingsTab.other.title,
- padding: const EdgeInsets.only(bottom: 0),
- children: [
- _ButtonEntry(
- label: t.aboutPage.title,
- buttonLabel: t.general.open,
- onTap: () async {
- await context.push(() => const AboutPage());
- },
- ),
- _ButtonEntry(
- label: t.settingsTab.other.support,
- buttonLabel: t.settingsTab.other.donate,
- onTap: () async {
- await context.push(() => const DonationPage());
- },
- ),
- _ButtonEntry(
- label: t.settingsTab.other.privacyPolicy,
- buttonLabel: t.general.open,
- onTap: () async {
- await launchUrl(
- Uri.parse('https://localsend.org/privacy'),
- mode: LaunchMode.externalApplication,
- );
- },
- ),
- if (checkPlatform([TargetPlatform.iOS, TargetPlatform.macOS]))
- _ButtonEntry(
- label: t.settingsTab.other.termsOfUse,
- buttonLabel: t.general.open,
- onTap: () async {
- await launchUrl(
- Uri.parse('https://www.apple.com/legal/internet-services/itunes/dev/stdeula/'),
- mode: LaunchMode.externalApplication,
- );
- },
- ),
- ],
- ),
- Row(
- mainAxisAlignment: MainAxisAlignment.end,
- children: [
- LabeledCheckbox(
- label: t.settingsTab.advancedSettings,
- value: vm.advanced,
- labelFirst: true,
- onChanged: (b) async {
- vm.onTapAdvanced(b == true);
- await ref.notifier(settingsProvider).setAdvancedSettingsEnabled(b == true);
- },
- ),
- const SizedBox(width: 10),
- ],
- ),
- const SizedBox(height: 20),
- const LocalSendLogo(withText: true),
- const SizedBox(height: 5),
- ref.watch(versionProvider).maybeWhen(
- data: (version) => Text(
- 'Version: $version',
+ const SizedBox(height: 20),
+ const LocalSendLogo(withText: true),
+ const SizedBox(height: 5),
+ ref.watch(versionProvider).maybeWhen(
+ data: (version) => Text(
+ 'Version: $version',
+ textAlign: TextAlign.center,
+ ),
+ orElse: () => Container(),
+ ),
+ Text(
+ '© ${DateTime.now().year} Tien Do Nam',
textAlign: TextAlign.center,
),
- orElse: () => Container(),
- ),
- Text(
- '© ${DateTime.now().year} Tien Do Nam',
- textAlign: TextAlign.center,
- ),
- Center(
- child: TextButton.icon(
- style: TextButton.styleFrom(
- foregroundColor: Theme.of(context).colorScheme.onSurface,
- ),
- onPressed: () async {
- await context.push(() => const ChangelogPage());
- },
- icon: const Icon(Icons.history),
- label: Text(t.changelogPage.title),
+ Center(
+ child: TextButton.icon(
+ style: TextButton.styleFrom(
+ foregroundColor: Theme.of(context).colorScheme.onSurface,
+ ),
+ onPressed: () async {
+ await context.push(() => const ChangelogPage());
+ },
+ icon: const Icon(Icons.history),
+ label: Text(t.changelogPage.title),
+ ),
+ ),
+ const SizedBox(height: 80),
+ ],
),
),
- const SizedBox(height: 80),
+ // a pseudo appbar that is draggable for the settings page
+ SizedBox(
+ height: 50 + MediaQuery.of(context).padding.top,
+ child: ClipRRect(
+ child: BackdropFilter(
+ filter: ImageFilter.blur(
+ sigmaX: 20.0,
+ sigmaY: 20.0,
+ ),
+ child: MoveWindow(
+ child: SafeArea(
+ child: Container(
+ alignment: Alignment.center,
+ child: Padding(
+ padding: const EdgeInsets.only(left: 8),
+ child: Text(t.settingsTab.title, style: Theme.of(context).textTheme.titleLarge, textAlign: TextAlign.center),
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ )
],
);
},
diff --git a/app/lib/pages/troubleshoot_page.dart b/app/lib/pages/troubleshoot_page.dart
index e0ff35f5..58895762 100644
--- a/app/lib/pages/troubleshoot_page.dart
+++ b/app/lib/pages/troubleshoot_page.dart
@@ -6,6 +6,7 @@ import 'package:localsend_app/gen/strings.g.dart';
import 'package:localsend_app/provider/settings_provider.dart';
import 'package:localsend_app/util/native/cmd_helper.dart';
import 'package:localsend_app/util/native/platform_check.dart';
+import 'package:localsend_app/widget/custom_basic_appbar.dart';
import 'package:localsend_app/widget/custom_icon_button.dart';
import 'package:localsend_app/widget/dialogs/not_available_on_platform_dialog.dart';
import 'package:localsend_app/widget/responsive_list_view.dart';
@@ -18,9 +19,7 @@ class TroubleshootPage extends StatelessWidget {
Widget build(BuildContext context) {
final settings = context.ref.watch(settingsProvider);
return Scaffold(
- appBar: AppBar(
- title: Text(t.troubleshootPage.title),
- ),
+ appBar: basicLocalSendAppbar(t.troubleshootPage.title),
body: ResponsiveListView(
padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 30),
children: [
diff --git a/app/lib/pages/web_send_page.dart b/app/lib/pages/web_send_page.dart
index 41dd7b50..7721088d 100644
--- a/app/lib/pages/web_send_page.dart
+++ b/app/lib/pages/web_send_page.dart
@@ -9,6 +9,7 @@ import 'package:localsend_app/provider/network/server/server_provider.dart';
import 'package:localsend_app/provider/settings_provider.dart';
import 'package:localsend_app/util/native/platform_check.dart';
import 'package:localsend_app/util/ui/snackbar.dart';
+import 'package:localsend_app/widget/custom_basic_appbar.dart';
import 'package:localsend_app/widget/dialogs/pin_dialog.dart';
import 'package:localsend_app/widget/dialogs/qr_dialog.dart';
import 'package:localsend_app/widget/dialogs/zoom_dialog.dart';
@@ -99,9 +100,7 @@ class _WebSendPageState extends State with Refena {
},
canPop: false,
child: Scaffold(
- appBar: AppBar(
- title: Text(t.webSharePage.title),
- ),
+ appBar: basicLocalSendAppbar(t.webSharePage.title),
body: Builder(
builder: (context) {
if (_stateEnum != _ServerState.running) {
diff --git a/app/lib/util/native/tray_helper.dart b/app/lib/util/native/tray_helper.dart
index 399d29f5..255ba10b 100644
--- a/app/lib/util/native/tray_helper.dart
+++ b/app/lib/util/native/tray_helper.dart
@@ -1,5 +1,6 @@
import 'dart:io';
+import 'package:bitsdojo_window/bitsdojo_window.dart';
import 'package:flutter/foundation.dart';
import 'package:localsend_app/gen/assets.gen.dart';
import 'package:localsend_app/gen/strings.g.dart';
@@ -81,6 +82,7 @@ Future showFromTray() async {
// This will crash on Windows
// https://github.com/localsend/localsend/issues/32
await windowManager.setSkipTaskbar(false);
+ appWindow.show();
}
// Enable animations
diff --git a/app/lib/widget/custom_basic_appbar.dart b/app/lib/widget/custom_basic_appbar.dart
new file mode 100644
index 00000000..4df116ac
--- /dev/null
+++ b/app/lib/widget/custom_basic_appbar.dart
@@ -0,0 +1,69 @@
+import 'dart:io';
+import 'dart:ui';
+
+import 'package:bitsdojo_window/bitsdojo_window.dart';
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+import 'package:localsend_app/util/native/platform_check.dart';
+
+class CustomBackButton extends StatelessWidget {
+ final Color? color;
+
+ const CustomBackButton({super.key, this.color});
+
+ @override
+ Widget build(BuildContext context) {
+ final isRtl = Directionality.of(context) == TextDirection.rtl;
+ return IconButton(
+ icon: Icon(
+ isRtl ? Icons.arrow_forward_ios_rounded : Icons.arrow_back_ios_new_rounded,
+ color: color ?? IconTheme.of(context).color,
+ ),
+ tooltip: MaterialLocalizations.of(context).backButtonTooltip,
+ onPressed: () async {
+ await Navigator.maybePop(context);
+ },
+ );
+ }
+}
+
+PreferredSizeWidget basicLocalSendAppbar(String title) {
+ // Creates a very simple new appBar to support bitsdojo_windows on mac and make them draggable
+ // if you want have more items on here for a specific page, make sure to add it here as an option
+ // so that mac users can still appreciate this near native new design
+ return checkPlatform([TargetPlatform.macOS])
+ ? PreferredSize(
+ preferredSize: const Size.fromHeight(kToolbarHeight),
+ child: ClipRRect(
+ child: BackdropFilter(
+ filter: ImageFilter.blur(
+ sigmaX: 20.0,
+ sigmaY: 20.0,
+ ),
+ child: MoveWindow(
+ child: Container(
+ color: Colors.transparent,
+ child: Row(
+ crossAxisAlignment: CrossAxisAlignment.center,
+ children: [
+ // Padding space for macOS traffic lights
+ if (!kIsWeb && Platform.isMacOS) const SizedBox(width: 60),
+ // Originally leading Icon
+ CustomBackButton(),
+ // Center Title
+ Expanded(
+ child: Padding(
+ padding: const EdgeInsets.all(8.0),
+ child: Center(
+ child: FittedBox(fit: BoxFit.scaleDown, child: Text(title, style: TextStyle(fontSize: 100, fontWeight: FontWeight.normal))),
+ ),
+ )),
+ // For true centering of the icon since it shifted
+ const SizedBox(width: 60),
+ ],
+ ),
+ ),
+ ),
+ )))
+ : AppBar(title: Text(title));
+}
diff --git a/app/linux/flutter/generated_plugin_registrant.cc b/app/linux/flutter/generated_plugin_registrant.cc
index d4c9863b..7ec0cb2b 100644
--- a/app/linux/flutter/generated_plugin_registrant.cc
+++ b/app/linux/flutter/generated_plugin_registrant.cc
@@ -6,6 +6,7 @@
#include "generated_plugin_registrant.h"
+#include
#include
#include
#include
@@ -20,6 +21,9 @@
#include
void fl_register_plugins(FlPluginRegistry* registry) {
+ g_autoptr(FlPluginRegistrar) bitsdojo_window_linux_registrar =
+ fl_plugin_registry_get_registrar_for_plugin(registry, "BitsdojoWindowPlugin");
+ bitsdojo_window_plugin_register_with_registrar(bitsdojo_window_linux_registrar);
g_autoptr(FlPluginRegistrar) desktop_drop_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "DesktopDropPlugin");
desktop_drop_plugin_register_with_registrar(desktop_drop_registrar);
diff --git a/app/linux/flutter/generated_plugins.cmake b/app/linux/flutter/generated_plugins.cmake
index f820aa26..0c29172d 100644
--- a/app/linux/flutter/generated_plugins.cmake
+++ b/app/linux/flutter/generated_plugins.cmake
@@ -3,6 +3,7 @@
#
list(APPEND FLUTTER_PLUGIN_LIST
+ bitsdojo_window_linux
desktop_drop
dynamic_color
file_selector_linux
diff --git a/app/macos/Flutter/GeneratedPluginRegistrant.swift b/app/macos/Flutter/GeneratedPluginRegistrant.swift
index 84fd835a..d731239b 100644
--- a/app/macos/Flutter/GeneratedPluginRegistrant.swift
+++ b/app/macos/Flutter/GeneratedPluginRegistrant.swift
@@ -5,6 +5,7 @@
import FlutterMacOS
import Foundation
+import bitsdojo_window_macos
import connectivity_plus
import desktop_drop
import device_info_plus
@@ -28,6 +29,7 @@ import wakelock_plus
import window_manager
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
+ BitsdojoWindowPlugin.register(with: registry.registrar(forPlugin: "BitsdojoWindowPlugin"))
ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin"))
DesktopDropPlugin.register(with: registry.registrar(forPlugin: "DesktopDropPlugin"))
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
diff --git a/app/macos/Runner/AppDelegate.swift b/app/macos/Runner/AppDelegate.swift
index e1ca4ca3..f63d0b7d 100644
--- a/app/macos/Runner/AppDelegate.swift
+++ b/app/macos/Runner/AppDelegate.swift
@@ -3,6 +3,7 @@ import FlutterMacOS
import Defaults
import DockProgress
import LaunchAtLogin
+import bitsdojo_window_macos
enum DockIcon: CaseIterable {
case regular
diff --git a/app/macos/Runner/MainFlutterWindow.swift b/app/macos/Runner/MainFlutterWindow.swift
index c9700270..b7174531 100644
--- a/app/macos/Runner/MainFlutterWindow.swift
+++ b/app/macos/Runner/MainFlutterWindow.swift
@@ -1,8 +1,13 @@
import Cocoa
import FlutterMacOS
import window_manager
+import bitsdojo_window_macos // used to make custom window bars on macOS (or any desktop operating system for that matter)
-class MainFlutterWindow: NSWindow {
+class MainFlutterWindow: BitsdojoWindow {
+ // just following intructions from https://pub.dev/packages/bitsdojo_window
+ override func bitsdojo_window_configure() -> UInt {
+ return BDW_CUSTOM_FRAME | BDW_HIDE_ON_STARTUP
+ }
override func awakeFromNib() {
let flutterViewController = FlutterViewController.init()
let windowFrame = self.frame
@@ -10,13 +15,9 @@ class MainFlutterWindow: NSWindow {
self.setFrame(windowFrame, display: true)
RegisterGeneratedPlugins(registry: flutterViewController)
+ // window_manager: start window hidden
+ hiddenWindowAtLaunch()
super.awakeFromNib()
}
-
- // window_manager: start hidden
- override public func order(_ place: NSWindow.OrderingMode, relativeTo otherWin: Int) {
- super.order(place, relativeTo: otherWin)
- hiddenWindowAtLaunch()
- }
}
diff --git a/app/pubspec.lock b/app/pubspec.lock
index e59d2472..e39bda1f 100644
--- a/app/pubspec.lock
+++ b/app/pubspec.lock
@@ -86,6 +86,46 @@ packages:
url: "https://pub.dev"
source: hosted
version: "5.7.0"
+ bitsdojo_window:
+ dependency: "direct main"
+ description:
+ name: bitsdojo_window
+ sha256: "88ef7765dafe52d97d7a3684960fb5d003e3151e662c18645c1641c22b873195"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.1.6"
+ bitsdojo_window_linux:
+ dependency: transitive
+ description:
+ name: bitsdojo_window_linux
+ sha256: "9519c0614f98be733e0b1b7cb15b827007886f6fe36a4fb62cf3d35b9dd578ab"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.1.4"
+ bitsdojo_window_macos:
+ dependency: transitive
+ description:
+ name: bitsdojo_window_macos
+ sha256: f7c5be82e74568c68c5b8449e2c5d8fd12ec195ecd70745a7b9c0f802bb0268f
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.1.4"
+ bitsdojo_window_platform_interface:
+ dependency: transitive
+ description:
+ name: bitsdojo_window_platform_interface
+ sha256: "65daa015a0c6dba749bdd35a0f092e7a8ba8b0766aa0480eb3ef808086f6e27c"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.1.2"
+ bitsdojo_window_windows:
+ dependency: transitive
+ description:
+ name: bitsdojo_window_windows
+ sha256: fa982cf61ede53f483e50b257344a1c250af231a3cdc93a7064dd6dc0d720b68
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.1.6"
boolean_selector:
dependency: transitive
description:
diff --git a/app/pubspec.yaml b/app/pubspec.yaml
index d7e9ae55..1f3a147a 100644
--- a/app/pubspec.yaml
+++ b/app/pubspec.yaml
@@ -11,6 +11,7 @@ environment:
dependencies:
basic_utils: 5.7.0
+ bitsdojo_window: ^0.1.6
collection: ^1.17.2 # allow newer versions, so it can compile with newer Flutter versions
common:
path: ../common
diff --git a/app/windows/flutter/generated_plugin_registrant.cc b/app/windows/flutter/generated_plugin_registrant.cc
index e1b56e62..842cfae6 100644
--- a/app/windows/flutter/generated_plugin_registrant.cc
+++ b/app/windows/flutter/generated_plugin_registrant.cc
@@ -6,6 +6,7 @@
#include "generated_plugin_registrant.h"
+#include
#include
#include
#include
@@ -22,6 +23,8 @@
#include
void RegisterPlugins(flutter::PluginRegistry* registry) {
+ BitsdojoWindowPluginRegisterWithRegistrar(
+ registry->GetRegistrarForPlugin("BitsdojoWindowPlugin"));
ConnectivityPlusWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin"));
DesktopDropPluginRegisterWithRegistrar(
diff --git a/app/windows/flutter/generated_plugins.cmake b/app/windows/flutter/generated_plugins.cmake
index 53cf69f8..26c6a0fb 100644
--- a/app/windows/flutter/generated_plugins.cmake
+++ b/app/windows/flutter/generated_plugins.cmake
@@ -3,6 +3,7 @@
#
list(APPEND FLUTTER_PLUGIN_LIST
+ bitsdojo_window_windows
connectivity_plus
desktop_drop
dynamic_color