From 42d8c82898c8e0110a63283a90c660b73d54935f Mon Sep 17 00:00:00 2001 From: ReallLucky <138239837+ReallLucky@users.noreply.github.com> Date: Mon, 26 May 2025 18:49:52 +0200 Subject: [PATCH] A clean UI revamp (#2416) --- app/android/app/src/main/AndroidManifest.xml | 6 +- .../app/src/main/res/values-night/styles.xml | 2 + .../app/src/main/res/values/styles.xml | 2 + app/ios/Runner/AppDelegate.swift | 28 +- app/lib/config/init.dart | 15 +- app/lib/pages/about/about_page.dart | 5 +- app/lib/pages/changelog_page.dart | 9 +- app/lib/pages/debug/debug_page.dart | 5 +- app/lib/pages/debug/discovery_debug_page.dart | 5 +- app/lib/pages/debug/http_logs_page.dart | 5 +- app/lib/pages/debug/security_debug_page.dart | 5 +- app/lib/pages/donation/donation_page.dart | 5 +- app/lib/pages/home_page.dart | 124 +- app/lib/pages/language_page.dart | 5 +- app/lib/pages/progress_page.dart | 17 +- app/lib/pages/receive_history_page.dart | 6 +- app/lib/pages/selected_files_page.dart | 5 +- app/lib/pages/send_page.dart | 3 +- .../settings/network_interfaces_page.dart | 5 +- app/lib/pages/tabs/receive_tab.dart | 5 + app/lib/pages/tabs/send_tab.dart | 404 +++---- app/lib/pages/tabs/settings_tab.dart | 1021 +++++++++-------- app/lib/pages/troubleshoot_page.dart | 5 +- app/lib/pages/web_send_page.dart | 5 +- app/lib/util/native/tray_helper.dart | 2 + app/lib/widget/custom_basic_appbar.dart | 69 ++ .../flutter/generated_plugin_registrant.cc | 4 + app/linux/flutter/generated_plugins.cmake | 1 + .../Flutter/GeneratedPluginRegistrant.swift | 2 + app/macos/Runner/AppDelegate.swift | 1 + app/macos/Runner/MainFlutterWindow.swift | 15 +- app/pubspec.lock | 40 + app/pubspec.yaml | 1 + .../flutter/generated_plugin_registrant.cc | 3 + app/windows/flutter/generated_plugins.cmake | 1 + 35 files changed, 1017 insertions(+), 819 deletions(-) create mode 100644 app/lib/widget/custom_basic_appbar.dart 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