A clean UI revamp (#2416)
CI / format (push) Has been cancelled
CI / test (push) Has been cancelled
CI / packaging (push) Has been cancelled

This commit is contained in:
ReallLucky
2025-05-26 18:49:52 +02:00
committed by GitHub
parent 0f26007f6f
commit 42d8c82898
35 changed files with 1017 additions and 819 deletions
+3 -3
View File
@@ -41,9 +41,9 @@
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
android:name="io.flutter.embedding.android.EnableCutout"
android:value="true"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
@@ -5,6 +5,7 @@
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
@@ -14,5 +15,6 @@
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
</style>
</resources>
@@ -5,6 +5,7 @@
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
@@ -14,5 +15,6 @@
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
</style>
</resources>
+18 -10
View File
@@ -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)
}
}
}
+9 -6
View File
@@ -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<RefenaContainer> preInit(List<String> 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();
+2 -3
View File
@@ -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: [
+4 -5
View File
@@ -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),
),
+2 -3
View File
@@ -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: [
@@ -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: [
+2 -3
View File
@@ -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: [
+2 -3
View File
@@ -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,
+2 -3
View File
@@ -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(
+71 -53
View File
@@ -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<HomePage> 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),
],
),
),
],
),
),
],
+2 -3
View File
@@ -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<LanguagePage> {
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: [
+12 -5
View File
@@ -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<ProgressPage> 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<ProgressPage> with Refena {
),
),
),
checkPlatform([TargetPlatform.macOS])
? Positioned(
top: 0,
left: 0,
right: 0,
height: 40,
child: MoveWindow(),
)
: SizedBox(),
],
),
),
+2 -4
View File
@@ -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: [
+2 -3
View File
@@ -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),
+2 -1
View File
@@ -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<SendPage> with Refena {
},
canPop: true,
child: Scaffold(
appBar: widget.showAppBar ? AppBar() : null,
appBar: widget.showAppBar ? basicLocalSendAppbar('') : null,
body: SafeArea(
child: Center(
child: ConstrainedBox(
@@ -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<NetworkInterfacesPage> {
? 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,
+5
View File
@@ -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),
+208 -196
View File
@@ -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,
),
],
);
},
File diff suppressed because it is too large Load Diff
+2 -3
View File
@@ -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: [
+2 -3
View File
@@ -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<WebSendPage> 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) {
+2
View File
@@ -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<void> showFromTray() async {
// This will crash on Windows
// https://github.com/localsend/localsend/issues/32
await windowManager.setSkipTaskbar(false);
appWindow.show();
}
// Enable animations
+69
View File
@@ -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));
}
@@ -6,6 +6,7 @@
#include "generated_plugin_registrant.h"
#include <bitsdojo_window_linux/bitsdojo_window_plugin.h>
#include <desktop_drop/desktop_drop_plugin.h>
#include <dynamic_color/dynamic_color_plugin.h>
#include <file_selector_linux/file_selector_plugin.h>
@@ -20,6 +21,9 @@
#include <yaru_window_linux/yaru_window_linux_plugin.h>
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);
@@ -3,6 +3,7 @@
#
list(APPEND FLUTTER_PLUGIN_LIST
bitsdojo_window_linux
desktop_drop
dynamic_color
file_selector_linux
@@ -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"))
+1
View File
@@ -3,6 +3,7 @@ import FlutterMacOS
import Defaults
import DockProgress
import LaunchAtLogin
import bitsdojo_window_macos
enum DockIcon: CaseIterable {
case regular
+8 -7
View File
@@ -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()
}
}
+40
View File
@@ -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:
+1
View File
@@ -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
@@ -6,6 +6,7 @@
#include "generated_plugin_registrant.h"
#include <bitsdojo_window_windows/bitsdojo_window_plugin.h>
#include <connectivity_plus/connectivity_plus_windows_plugin.h>
#include <desktop_drop/desktop_drop_plugin.h>
#include <dynamic_color/dynamic_color_plugin_c_api.h>
@@ -22,6 +23,8 @@
#include <windows_taskbar/windows_taskbar_plugin.h>
void RegisterPlugins(flutter::PluginRegistry* registry) {
BitsdojoWindowPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("BitsdojoWindowPlugin"));
ConnectivityPlusWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin"));
DesktopDropPluginRegisterWithRegistrar(
@@ -3,6 +3,7 @@
#
list(APPEND FLUTTER_PLUGIN_LIST
bitsdojo_window_windows
connectivity_plus
desktop_drop
dynamic_color