Merge branch 'main' into feature/webrtc

# Conflicts:
#	app/lib/pages/tabs/send_tab.dart
This commit is contained in:
Tien Do Nam
2025-07-09 22:40:10 +02:00
140 changed files with 1645 additions and 901 deletions
+71 -36
View File
@@ -1,58 +1,93 @@
name: Bug report
description: Create a report to help us improve
title: Bug report
labels: ["bug :bug:"]
description: Create a report to help us address issues you are facing
title: "bug: "
labels: [bug]
body:
- type: checkboxes
id: duplication
attributes:
label:
options:
- label: This issue is not duplicated with any other open or closed issues
required: true
- type: checkboxes
id: latest
attributes:
label:
options:
- label: I am using the latest version from the release
required: true
- type: textarea
id: bug_report_description
id: description
attributes:
label: Describe the bug
description: A clear and concise description of what the bug is
placeholder: |
App crashes on startup every time after changing settings.
/
App crashes when sending/receiving massive files.
validations:
required: true
- type: textarea
id: bug_report_reproduce
attributes:
label: To reproduce
description: Steps to reproduce the behavior
value: |
1. Go to '...'
2. Click on '...'
3. Scroll down to '...'
4. See error
validations:
required: true
- type: textarea
id: bug_report_expected_behavior
id: expected
attributes:
label: Expected behavior
description: A clear and concise description of what you expected to happen
placeholder: |
App started normally, everything worked fine.
validations:
required: true
- type: textarea
id: bug_report_screenshots
id: reproduce
attributes:
label: Reproduction
description: Steps to reproduce the bug
placeholder: |
1. Turn on "What" in "What settings"
2. Restart the app
3. Crash
/
1. Transfer a "What" file is What size takes What long
2. Observe What
3. Crash
validations:
required: true
- type: textarea
id: environment
attributes:
label: Device information
description: Provide details about your system environment
placeholder: |
Device: (Pixel 9 Pro/Lenovo Yoga 9i)
System: (Android 16 BP31.2505/Windows 11 24H2)
Special things: (Rooted/Hardware damaged)
If applicable, both info of sender and receiver.
validations:
required: true
- type: textarea
id: screenshots
attributes:
label: Screenshots
description: |
If applicable, add screenshots to help explain your problem
Tip: You can attach images by clicking this area to highlight it and then dragging files in.
- type: textarea
id: bug_report_desktop
attributes:
label: Desktop (please complete the following information)
description: If applicable, add screenshots to help explain your problem
placeholder: |
- OS: [e.g. iOS]
- Version: [e.g. 1.6.2]
modified_setting_items.jpg
files_to_send.jpg
crashed_screen.jpg
validations:
required: false
- type: textarea
id: bug_report_smartphone
attributes:
label: Smartphone (please complete the following information)
placeholder: |
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Version: [e.g. 1.6.2]
- type: textarea
id: bug_report_additional_context
id: additional
attributes:
label: Additional context
description: Add any other context about the problem here
placeholder: |
Crash report or other helpful information
validations:
required: false
+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';
@@ -127,12 +128,14 @@ Future<RefenaContainer> preInit(List<String> args) async {
} else if (defaultTargetPlatform == TargetPlatform.macOS) {
startHidden = await isLaunchedAsLoginItem() && await getLaunchAtLoginMinimized();
}
if (startHidden) {
unawaited(hideToTray());
} else {
await WindowManager.instance.show();
}
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: [
+25 -8
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';
@@ -20,6 +21,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';
@@ -71,9 +73,19 @@ class _ProgressPageState extends State<ProgressPage> with Refena {
// Periodically call WakelockPlus.enable() to keep the screen awake
_wakelockPlusTimer = Timer.periodic(const Duration(seconds: 30), (timer) {
try {
unawaited(WakelockPlus.enable());
} catch (_) {}
final finished = ref.read(serverProvider)?.session?.files.values.map((e) => e.status).isFinishedOrSkipped ??
ref.read(sendProvider)[widget.sessionId]?.files.values.map((e) => e.status).isFinishedOrSkipped ??
true;
if (finished) {
timer.cancel();
try {
unawaited(WakelockPlus.disable());
} catch (_) {}
} else {
try {
unawaited(WakelockPlus.enable());
} catch (_) {}
}
});
if (ref.read(settingsProvider).autoFinish) {
@@ -222,11 +234,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(
@@ -503,6 +511,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
@@ -14,6 +14,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';
@@ -62,11 +63,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: device.ip == null ? null : () 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) {
@@ -1,4 +1,4 @@
import 'dart:convert' show utf8;
import 'dart:convert' show jsonDecode, utf8;
import 'dart:io';
import 'dart:typed_data';
@@ -13,6 +13,7 @@ import 'package:localsend_app/util/send_ignore.dart';
import 'package:logging/logging.dart';
import 'package:path/path.dart' as p;
import 'package:refena_flutter/refena_flutter.dart';
import 'package:share_handler/share_handler.dart';
import 'package:uuid/uuid.dart';
final _logger = Logger('SelectedSendingFiles');
@@ -287,7 +288,27 @@ class LoadSelectionFromArgsAction extends AsyncReduxActionWithResult<SelectedSen
@override
Future<(List<CrossFile>, bool)> reduce() async {
bool filesAdded = false;
bool nextShare = false;
for (final arg in args) {
if (arg == '--share') {
nextShare = true;
continue;
}
if (nextShare) {
nextShare = false;
final json = jsonDecode(arg);
final SharedMedia payload = SharedMedia.decode(json);
final message = payload.content;
if (message != null && message.trim().isNotEmpty) {
dispatch(AddMessageAction(message: message));
}
await dispatchAsync(AddFilesAction(
files: payload.attachments?.where((a) => a != null).cast<SharedAttachment>() ?? <SharedAttachment>[],
converter: CrossFileConverters.convertSharedAttachment,
));
filesAdded = true;
continue;
}
if (arg.startsWith('-')) {
continue;
}
+4 -4
View File
@@ -1,7 +1,7 @@
import 'package:common/model/file_type.dart';
/// Matches myFile-123 -> 123
final _fileNumberRegex = RegExp(r'^(.*)(?:-(\d+))$');
/// Matches myFile (123) -> "myFile", " (123)"
final _fileNumberRegex = RegExp(r'^(.*)(?:(\s\(\d+\)))$');
extension FilePathStringExt on String {
String get extension {
@@ -43,9 +43,9 @@ extension FilePathStringExt on String {
final match = _fileNumberRegex.firstMatch(fileName);
if (match != null) {
return '${match.group(1)}-$count'.withExtension(extension);
return '${match.group(1)} ($count)'.withExtension(extension);
} else {
return '$fileName-$count'.withExtension(extension);
return '$fileName ($count)'.withExtension(extension);
}
}
+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
+2
View File
@@ -2,6 +2,8 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>LSUIElement</key>
<true/>
<key>AppIdentifierPrefix</key>
<string>$(AppIdentifierPrefix)</string>
<key>CFBundleDevelopmentRegion</key>
+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
@@ -0,0 +1,9 @@
import 'package:localsend_app/util/file_path_helper.dart';
import 'package:test/test.dart';
void main() {
test('fileName with counter', () async {
expect('myFile'.withCount(1), 'myFile (1)');
expect('myFile (1)'.withCount(2), 'myFile (2)');
});
}
+2
View File
@@ -15,3 +15,5 @@ x86/
*.[Cc]ache
# but keep track of directories ending in .cache
!*.[Cc]ache/
localsend_msix_helper.msix
+7
View File
@@ -74,6 +74,13 @@ set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}")
install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}"
COMPONENT Runtime)
install(FILES
"${BINARY_NAME}.exe.manifest"
"localsend_msix_helper.msix"
"install_msix_helper.ps1"
DESTINATION "${CMAKE_INSTALL_PREFIX}"
COMPONENT Runtime)
install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
COMPONENT Runtime)
@@ -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
+1
View File
@@ -0,0 +1 @@
Add-AppxPackage .\localsend_msix_helper.msix -ExternalLocation $(Get-Location)
+9
View File
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<assemblyIdentity version="0.0.0.0" name="localsendapp" />
<msix xmlns="urn:schemas-microsoft-com:msix.v1"
publisher="CN=Tien Do Nam, O=Tien Do Nam, S=Sachsen, C=DE"
packageName="com.flutter.localsendapp"
applicationId="localsendapp"
/>
</assembly>
+1
View File
@@ -11,6 +11,7 @@ add_executable(${BINARY_NAME} WIN32
"main.cpp"
"utils.cpp"
"win32_window.cpp"
"winrt_ext.cpp"
"${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc"
"Runner.rc"
"runner.exe.manifest"
+10
View File
@@ -4,6 +4,7 @@
#include "flutter_window.h"
#include "utils.h"
#include "winrt_ext.h"
int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
_In_ wchar_t *command_line, _In_ int show_command) {
@@ -22,6 +23,15 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
std::vector<std::string> command_line_arguments =
GetCommandLineArguments();
if (IsRunningWithIdentity()) {
winrt::hstring share_arg = GetSharedMedia();
if (!share_arg.empty()) {
printf("share: %ls\n", share_arg.c_str());
command_line_arguments.push_back("--share");
command_line_arguments.push_back(Utf8FromUtf16(share_arg.c_str()));
}
}
project.set_dart_entrypoint_arguments(std::move(command_line_arguments));
FlutterWindow window(project);
+69
View File
@@ -0,0 +1,69 @@
#include "winrt_ext.h"
#include <windows.h>
#include <appmodel.h>
#include <winrt/windows.foundation.h>
#include <winrt/windows.foundation.collections.h>
#include <winrt/windows.storage.h>
#include <winrt/windows.storage.streams.h>
#include <winrt/windows.applicationmodel.activation.h>
#include <winrt/windows.applicationmodel.datatransfer.h>
#include <winrt/windows.applicationmodel.datatransfer.sharetarget.h>
#include <winrt/windows.data.json.h>
using winrt::Windows::ApplicationModel::AppInstance;
using winrt::Windows::ApplicationModel::Activation::ActivationKind;
using winrt::Windows::ApplicationModel::Activation::ShareTargetActivatedEventArgs;
using winrt::Windows::ApplicationModel::DataTransfer::DataPackageView;
using winrt::Windows::ApplicationModel::DataTransfer::StandardDataFormats;
using winrt::Windows::Data::Json::JsonArray;
using winrt::Windows::Data::Json::JsonObject;
using winrt::Windows::Data::Json::JsonValue;
enum class SharedAttachmentType {
IMAGE,
VIDEO,
AUDIO,
FILE,
};
bool IsRunningWithIdentity() {
constexpr SIZE_T kPackageNameMaxLength = 1024;
UINT32 length = kPackageNameMaxLength;
wchar_t packageName[kPackageNameMaxLength];
LONG result = GetCurrentPackageFullName(&length, packageName);
return (result == ERROR_SUCCESS);
}
winrt::hstring GetSharedMedia() {
auto args = AppInstance::GetActivatedEventArgs();
if (args == nullptr)
return winrt::hstring();
if (args.Kind() != ActivationKind::ShareTarget)
return winrt::hstring();
auto share_target_args = args.as<ShareTargetActivatedEventArgs>();
auto op = share_target_args.ShareOperation();
auto data = op.Data();
JsonObject json;
if (data.Contains(StandardDataFormats::Text())) {
auto text = data.GetTextAsync().get();
json.SetNamedValue(L"content", JsonValue::CreateStringValue(text));
}
if (data.Contains(StandardDataFormats::Uri())) {
auto uri = data.GetUriAsync().get();
json.SetNamedValue(L"content", JsonValue::CreateStringValue(uri.ToString()));
}
if (data.Contains(StandardDataFormats::StorageItems())) {
JsonArray attachments;
auto storage_items = data.GetStorageItemsAsync().get();
for (const auto& item : storage_items) {
JsonObject attachment;
attachment.SetNamedValue(L"type", JsonValue::CreateNumberValue(double(SharedAttachmentType::FILE)));
attachment.SetNamedValue(L"path", JsonValue::CreateStringValue(item.Path()));
attachments.Append(attachment);
}
json.SetNamedValue(L"attachments", attachments);
}
return json.Stringify();
}
+9
View File
@@ -0,0 +1,9 @@
#ifndef RUNNER_WINRT_EXT_H_
#define RUNNER_WINRT_EXT_H_
#include <winrt/base.h>
bool IsRunningWithIdentity();
winrt::hstring GetSharedMedia();
#endif // RUNNER_WINRT_EXT_H_
+75
View File
@@ -0,0 +1,75 @@
<?xml version="1.0" encoding="utf-8"?>
<Package xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
xmlns:uap2="http://schemas.microsoft.com/appx/manifest/uap/windows10/2"
xmlns:uap3="http://schemas.microsoft.com/appx/manifest/uap/windows10/3"
xmlns:uap4="http://schemas.microsoft.com/appx/manifest/uap/windows10/4"
xmlns:uap6="http://schemas.microsoft.com/appx/manifest/uap/windows10/6"
xmlns:uap7="http://schemas.microsoft.com/appx/manifest/uap/windows10/7"
xmlns:uap8="http://schemas.microsoft.com/appx/manifest/uap/windows10/8"
xmlns:uap10="http://schemas.microsoft.com/appx/manifest/uap/windows10/10"
xmlns:iot="http://schemas.microsoft.com/appx/manifest/iot/windows10"
xmlns:desktop="http://schemas.microsoft.com/appx/manifest/desktop/windows10"
xmlns:desktop2="http://schemas.microsoft.com/appx/manifest/desktop/windows10/2"
xmlns:desktop4="http://schemas.microsoft.com/appx/manifest/desktop/windows10/4"
xmlns:desktop5="http://schemas.microsoft.com/appx/manifest/desktop/windows10/5"
xmlns:desktop6="http://schemas.microsoft.com/appx/manifest/desktop/windows10/6"
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
xmlns:rescap3="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities/3"
xmlns:rescap6="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities/6"
xmlns:com="http://schemas.microsoft.com/appx/manifest/com/windows10"
xmlns:com2="http://schemas.microsoft.com/appx/manifest/com/windows10/2"
xmlns:com3="http://schemas.microsoft.com/appx/manifest/com/windows10/3"
IgnorableNamespaces="uap3 desktop">
<Identity Name="11157TienDoNam.LocalSend" Version="1.14.0.0"
Publisher="CN=Tien Do Nam, O=Tien Do Nam, S=Sachsen, C=DE" ProcessorArchitecture="x64" />
<Properties>
<DisplayName>LocalSend MSIX Helper</DisplayName>
<PublisherDisplayName>Tien Do Nam</PublisherDisplayName>
<Logo>Images\StoreLogo.png</Logo>
<Description>An open source cross-platform alternative to AirDrop</Description>
<uap10:AllowExternalContent>true</uap10:AllowExternalContent>
</Properties>
<Resources>
<Resource Language="en" /><Resource Language="ar" /><Resource Language="bn" /><Resource Language="cs" /><Resource Language="da" /><Resource Language="de" /><Resource Language="el" /><Resource Language="es-ES" /><Resource Language="eu" /><Resource Language="fa" /><Resource Language="fr" /><Resource Language="he" /><Resource Language="hu" /><Resource Language="in" /><Resource Language="it" /><Resource Language="ja" /><Resource Language="ko" /><Resource Language="ne" /><Resource Language="nl" /><Resource Language="pl" /><Resource Language="pt-BR" /><Resource Language="ru" /><Resource Language="sv" /><Resource Language="th" /><Resource Language="tr" /><Resource Language="uk" /><Resource Language="vi" /><Resource Language="zh-CN" /><Resource Language="zh-HK" /><Resource Language="zh-TW" />
</Resources>
<Dependencies>
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.19041.0" MaxVersionTested="10.0.22621.2506" />
</Dependencies>
<Capabilities>
<rescap:Capability Name="runFullTrust" />
</Capabilities>
<Applications>
<Application Id="localsendapp" Executable="localsend_app.exe" EntryPoint="Windows.FullTrustApplication">
<uap:VisualElements AppListEntry="none" BackgroundColor="transparent"
DisplayName="LocalSend" Square150x150Logo="Images\Square150x150Logo.png"
Square44x44Logo="Images\Square44x44Logo.png" Description="An open source cross-platform alternative to AirDrop">
<uap:DefaultTile ShortName="LocalSend" Square310x310Logo="Images\LargeTile.png"
Square71x71Logo="Images\SmallTile.png" Wide310x150Logo="Images\Wide310x150Logo.png">
<uap:ShowNameOnTiles>
<uap:ShowOn Tile="square150x150Logo"/>
<uap:ShowOn Tile="square310x310Logo"/>
<uap:ShowOn Tile="wide310x150Logo"/>
</uap:ShowNameOnTiles>
</uap:DefaultTile>
<uap:SplashScreen Image="Images\SplashScreen.png"/>
<uap:LockScreen BadgeLogo="Images\BadgeLogo.png" Notification="badge"/>
</uap:VisualElements>
<Extensions>
<desktop:Extension Category="windows.startupTask" Executable="localsend_app.exe" EntryPoint="Windows.FullTrustApplication" uap10:Parameters="autostart">
<desktop:StartupTask TaskId="localsend" Enabled="false" DisplayName="LocalSend"/>
</desktop:Extension>
<uap:Extension Category="windows.shareTarget">
<uap:ShareTarget Description="Send to Localsend">
<uap:SupportedFileTypes>
<uap:SupportsAnyFileType />
</uap:SupportedFileTypes>
<uap:DataFormat>Text</uap:DataFormat>
<uap:DataFormat>Uri</uap:DataFormat>
<uap:DataFormat>StorageItems</uap:DataFormat>
</uap:ShareTarget>
</uap:Extension>
</Extensions>
</Application>
</Applications>
</Package>
Binary file not shown.

After

Width:  |  Height:  |  Size: 973 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 622 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 779 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 973 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 622 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 779 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 973 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Some files were not shown because too many files have changed in this diff Show More