mirror of
https://github.com/localsend/localsend.git
synced 2026-06-23 04:10:07 +00:00
feat: add receiver, discovery and settings
This commit is contained in:
@@ -1,3 +1,7 @@
|
||||
*.g.dart
|
||||
*.gen.dart
|
||||
*.freezed.dart
|
||||
|
||||
# Miscellaneous
|
||||
*.class
|
||||
*.log
|
||||
|
||||
+2
-16
@@ -10,20 +10,6 @@
|
||||
include: package:flutter_lints/flutter.yaml
|
||||
|
||||
linter:
|
||||
# The lint rules applied to this project can be customized in the
|
||||
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
|
||||
# included above or to enable additional rules. A list of all available lints
|
||||
# and their documentation is published at
|
||||
# https://dart-lang.github.io/linter/lints/index.html.
|
||||
#
|
||||
# Instead of disabling a lint rule for the entire project in the
|
||||
# section below, it can also be suppressed for a single line of code
|
||||
# or a specific dart file by using the `// ignore: name_of_lint` and
|
||||
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
|
||||
# producing the lint.
|
||||
rules:
|
||||
# avoid_print: false # Uncomment to disable the `avoid_print` rule
|
||||
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
|
||||
|
||||
# Additional information about this file can be found at
|
||||
# https://dart.dev/guides/language/analysis-options
|
||||
avoid_print: false
|
||||
use_key_in_widget_constructors: false
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"appName": "LocalSend",
|
||||
"general": {
|
||||
"advanced": "Advanced",
|
||||
"hide": "Hide",
|
||||
"unknown": "Unknown"
|
||||
},
|
||||
"receive": {
|
||||
"title": "Receive",
|
||||
"advanced": {
|
||||
"ip": "IP:",
|
||||
"server": "Server:",
|
||||
"subnetMask": "Subnet mask:",
|
||||
"alias": "Alias:"
|
||||
}
|
||||
},
|
||||
"send": {
|
||||
"title": "Send"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Settings",
|
||||
"alias": "Alias",
|
||||
"theme": "Theme",
|
||||
"themeOptions": {
|
||||
"system": "System",
|
||||
"dark": "Dark",
|
||||
"light": "Light"
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
+10
@@ -0,0 +1,10 @@
|
||||
targets:
|
||||
$default:
|
||||
builders:
|
||||
slang_build_runner:
|
||||
options:
|
||||
base_locale: en
|
||||
fallback_strategy: base_locale
|
||||
input_directory: assets/i18n
|
||||
input_file_pattern: .i18n.json
|
||||
output_directory: lib/gen
|
||||
@@ -47,5 +47,9 @@
|
||||
<true/>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true/>
|
||||
<key>CFBundleLocalizations</key>
|
||||
<array>
|
||||
<string>en</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
+33
-107
@@ -1,118 +1,44 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:localsend_app/constants.dart';
|
||||
import 'package:localsend_app/service/receiver_service.dart';
|
||||
import 'package:network_info_plus/network_info_plus.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:localsend_app/gen/strings.g.dart';
|
||||
import 'package:localsend_app/pages/home_page.dart';
|
||||
import 'package:localsend_app/provider/settings_provider.dart';
|
||||
import 'package:localsend_app/service/persistence_service.dart';
|
||||
import 'package:localsend_app/theme.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
|
||||
void main() {
|
||||
runApp(const MyApp());
|
||||
Future<void> main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
LocaleSettings.useDeviceLocale();
|
||||
|
||||
if (!kIsWeb && defaultTargetPlatform == TargetPlatform.windows) {
|
||||
await windowManager.ensureInitialized();
|
||||
WindowManager.instance.setMinimumSize(const Size(400, 500));
|
||||
}
|
||||
|
||||
final persistenceService = await PersistenceService.initialize();
|
||||
runApp(ProviderScope(
|
||||
overrides: [
|
||||
settingsProvider.overrideWith((ref) => SettingsNotifier(persistenceService)),
|
||||
],
|
||||
child: const LocalSendApp(),
|
||||
));
|
||||
}
|
||||
|
||||
class MyApp extends StatelessWidget {
|
||||
const MyApp({super.key});
|
||||
class LocalSendApp extends ConsumerWidget {
|
||||
const LocalSendApp();
|
||||
|
||||
// This widget is the root of your application.
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final themeMode = ref.watch(settingsProvider.select((settings) => settings.theme));
|
||||
return MaterialApp(
|
||||
title: 'Flutter Demo',
|
||||
theme: ThemeData(
|
||||
primarySwatch: Colors.blue,
|
||||
),
|
||||
title: t.appName,
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme: getTheme(Brightness.light),
|
||||
darkTheme: getTheme(Brightness.dark),
|
||||
themeMode: themeMode,
|
||||
home: const HomePage(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class HomePage extends StatefulWidget {
|
||||
const HomePage();
|
||||
|
||||
@override
|
||||
State<HomePage> createState() => _HomePageState();
|
||||
}
|
||||
|
||||
class _HomePageState extends State<HomePage> {
|
||||
final _receiver = ReceiverService();
|
||||
String? _ip;
|
||||
String? _mask;
|
||||
|
||||
String _targetIp = '';
|
||||
String? _response;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_init();
|
||||
}
|
||||
|
||||
Future<void> _init() async {
|
||||
final info = NetworkInfo();
|
||||
_ip = await info.getWifiIP();
|
||||
_mask = await info.getWifiSubmask();
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: SafeArea(
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 30),
|
||||
children: [
|
||||
Text('IP: $_ip'),
|
||||
Text('Mask: $_mask'),
|
||||
Text('Status: ${_receiver.running}'),
|
||||
const SizedBox(height: 20),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
ElevatedButton(
|
||||
onPressed: () async {
|
||||
await _receiver.start();
|
||||
setState(() {});
|
||||
},
|
||||
child: const Text('Start'),
|
||||
),
|
||||
const SizedBox(width: 20),
|
||||
ElevatedButton(
|
||||
onPressed: () async {
|
||||
await _receiver.stop();
|
||||
setState(() {});
|
||||
},
|
||||
child: const Text('Stop'),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
TextField(
|
||||
onChanged: (s) {
|
||||
setState(() {
|
||||
_targetIp = s;
|
||||
});
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
ElevatedButton(
|
||||
onPressed: () async {
|
||||
try {
|
||||
final response = await Dio().get('http://$_targetIp:${Constants.defaultPort}/hello');
|
||||
setState(() {
|
||||
_response = response.data.toString();
|
||||
});
|
||||
} catch (e) {
|
||||
print(e);
|
||||
setState(() {
|
||||
_response = e.toString();
|
||||
});
|
||||
}
|
||||
},
|
||||
child: const Text('Ping'),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Text('Response: $_response'),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'device.freezed.dart';
|
||||
part 'device.g.dart';
|
||||
|
||||
enum DeviceType {
|
||||
mobile(Icons.smartphone),
|
||||
desktop(Icons.computer),
|
||||
web(Icons.language);
|
||||
|
||||
const DeviceType(this.icon);
|
||||
|
||||
final IconData icon;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class Device with _$Device {
|
||||
const factory Device({
|
||||
required String ip,
|
||||
required String alias,
|
||||
required String? deviceModel,
|
||||
required DeviceType deviceType,
|
||||
}) = _Device;
|
||||
|
||||
factory Device.fromJson(Map<String, Object?> json) => _$DeviceFromJson(json);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:localsend_app/model/device.dart';
|
||||
|
||||
part 'info_dto.freezed.dart';
|
||||
part 'info_dto.g.dart';
|
||||
|
||||
@freezed
|
||||
class InfoDto with _$InfoDto {
|
||||
const factory InfoDto({
|
||||
required String alias,
|
||||
required String? deviceModel,
|
||||
required DeviceType deviceType,
|
||||
}) = _InfoDto;
|
||||
|
||||
factory InfoDto.fromJson(Map<String, Object?> json) => _$InfoDtoFromJson(json);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'network_info.freezed.dart';
|
||||
|
||||
@freezed
|
||||
class NetworkInfo with _$NetworkInfo {
|
||||
const factory NetworkInfo({
|
||||
required String? localIp,
|
||||
required String? netMask,
|
||||
}) = _NetworkInfo;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'settings.freezed.dart';
|
||||
|
||||
@freezed
|
||||
class Settings with _$Settings {
|
||||
const factory Settings({
|
||||
required String alias,
|
||||
required ThemeMode theme,
|
||||
}) = _Settings;
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:localsend_app/gen/strings.g.dart';
|
||||
import 'package:localsend_app/pages/tabs/receive_page.dart';
|
||||
import 'package:localsend_app/pages/tabs/send_page.dart';
|
||||
import 'package:localsend_app/pages/tabs/settings_page.dart';
|
||||
|
||||
enum _Tab {
|
||||
receive(Icons.wifi),
|
||||
send(Icons.send),
|
||||
settings(Icons.settings);
|
||||
|
||||
const _Tab(this.icon);
|
||||
|
||||
final IconData icon;
|
||||
|
||||
String get label {
|
||||
switch (this) {
|
||||
case _Tab.receive:
|
||||
return t.receive.title;
|
||||
case _Tab.send:
|
||||
return t.send.title;
|
||||
case _Tab.settings:
|
||||
return t.settings.title;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class HomePage extends StatefulWidget {
|
||||
const HomePage({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<HomePage> createState() => _HomePageState();
|
||||
}
|
||||
|
||||
class _HomePageState extends State<HomePage> {
|
||||
_Tab _currentTab = _Tab.receive;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: SafeArea(
|
||||
child: IndexedStack(
|
||||
index: _currentTab.index,
|
||||
children: const [
|
||||
ReceivePage(),
|
||||
SendPage(),
|
||||
SettingsPage(),
|
||||
],
|
||||
),
|
||||
),
|
||||
bottomNavigationBar: NavigationBar(
|
||||
selectedIndex: _currentTab.index,
|
||||
onDestinationSelected: (index) {
|
||||
setState(() {
|
||||
_currentTab = _Tab.values[index];
|
||||
});
|
||||
},
|
||||
destinations: _Tab.values.map((tab) {
|
||||
return NavigationDestination(icon: Icon(tab.icon), label: tab.label);
|
||||
}).toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:localsend_app/constants.dart';
|
||||
import 'package:localsend_app/gen/assets.gen.dart';
|
||||
import 'package:localsend_app/gen/strings.g.dart';
|
||||
import 'package:localsend_app/provider/network_info_provider.dart';
|
||||
import 'package:localsend_app/provider/receiver_provider.dart';
|
||||
import 'package:localsend_app/provider/settings_provider.dart';
|
||||
import 'package:localsend_app/util/ip_helper.dart';
|
||||
import 'package:localsend_app/widget/rotating_widget.dart';
|
||||
|
||||
class ReceivePage extends ConsumerStatefulWidget {
|
||||
const ReceivePage({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
ConsumerState<ReceivePage> createState() => _ReceivePageState();
|
||||
}
|
||||
|
||||
class _ReceivePageState extends ConsumerState<ReceivePage> {
|
||||
bool _advanced = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
ref.read(receiverProvider.notifier).startServer();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final settings = ref.watch(settingsProvider);
|
||||
final networkInfo = ref.watch(networkInfoProvider);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Expanded(
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
RotatingWidget(
|
||||
duration: const Duration(seconds: 10),
|
||||
child: Assets.img.logo512.image(width: 200),
|
||||
),
|
||||
Text(networkInfo?.localIp?.visualId ?? t.general.unknown, style: const TextStyle(fontSize: 48)),
|
||||
Text(settings.alias, style: const TextStyle(fontSize: 24)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Center(
|
||||
child: TextButton(
|
||||
onPressed: () {
|
||||
setState(() => _advanced = !_advanced);
|
||||
},
|
||||
child: Text(_advanced ? t.general.hide : t.general.advanced),
|
||||
),
|
||||
),
|
||||
AnimatedCrossFade(
|
||||
crossFadeState: _advanced ? CrossFadeState.showSecond : CrossFadeState.showFirst,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
firstChild: Container(),
|
||||
secondChild: Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(15),
|
||||
child: Table(
|
||||
columnWidths: const {
|
||||
0: IntrinsicColumnWidth(),
|
||||
1: IntrinsicColumnWidth(),
|
||||
2: FlexColumnWidth(),
|
||||
},
|
||||
children: [
|
||||
TableRow(
|
||||
children: [
|
||||
Text(t.receive.advanced.alias),
|
||||
const SizedBox(width: 10),
|
||||
Text(settings.alias),
|
||||
],
|
||||
),
|
||||
TableRow(
|
||||
children: [
|
||||
Text(t.receive.advanced.ip),
|
||||
const SizedBox(width: 10),
|
||||
Text(networkInfo?.localIp ?? t.general.unknown),
|
||||
],
|
||||
),
|
||||
TableRow(
|
||||
children: [
|
||||
Text(t.receive.advanced.server),
|
||||
const SizedBox(width: 10),
|
||||
Text(networkInfo?.localIp == null ? t.general.unknown : '${networkInfo!.localIp}:${Constants.defaultPort}/localsend'),
|
||||
],
|
||||
),
|
||||
TableRow(
|
||||
children: [
|
||||
Text(t.receive.advanced.subnetMask),
|
||||
const SizedBox(width: 10),
|
||||
Text(networkInfo?.netMask ?? t.general.unknown),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 15),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:localsend_app/provider/nearby_devices_provider.dart';
|
||||
import 'package:localsend_app/widget/device_widget.dart';
|
||||
|
||||
class SendPage extends ConsumerStatefulWidget {
|
||||
const SendPage({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
ConsumerState<SendPage> createState() => _SendPageState();
|
||||
}
|
||||
|
||||
class _SendPageState extends ConsumerState<SendPage> {
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final devices = ref.watch(nearbyDevicesProvider);
|
||||
return ListView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 30),
|
||||
children: [
|
||||
...devices.when(
|
||||
data: (data) {
|
||||
return data.map((device) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 10),
|
||||
child: DeviceWidget(device: device),
|
||||
);
|
||||
});
|
||||
},
|
||||
error: (e, st) {
|
||||
return [
|
||||
Center(
|
||||
child: Text(e.toString()),
|
||||
),
|
||||
];
|
||||
},
|
||||
loading: () {
|
||||
return [
|
||||
const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
];
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:localsend_app/gen/strings.g.dart';
|
||||
import 'package:localsend_app/provider/settings_provider.dart';
|
||||
|
||||
class SettingsPage extends ConsumerStatefulWidget {
|
||||
const SettingsPage({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
ConsumerState<SettingsPage> createState() => _SettingsPageState();
|
||||
}
|
||||
|
||||
class _SettingsPageState extends ConsumerState<SettingsPage> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final settings = ref.watch(settingsProvider);
|
||||
return ListView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 30),
|
||||
children: [
|
||||
_SettingsEntry(
|
||||
label: t.settings.alias,
|
||||
child: SizedBox(
|
||||
width: 200,
|
||||
child: TextField(
|
||||
onChanged: (s) {},
|
||||
),
|
||||
),
|
||||
),
|
||||
_SettingsEntry(
|
||||
label: t.settings.theme,
|
||||
child: DropdownButton<ThemeMode>(
|
||||
value: settings.theme,
|
||||
items: ThemeMode.values.map((theme) {
|
||||
return DropdownMenuItem(
|
||||
value: theme,
|
||||
child: Text(theme.humanName),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (theme) {
|
||||
if (theme != null) {
|
||||
ref.read(settingsProvider.notifier).setTheme(theme);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SettingsEntry extends StatelessWidget {
|
||||
final String label;
|
||||
final Widget child;
|
||||
|
||||
const _SettingsEntry({required this.label, required this.child});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 20),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(label),
|
||||
),
|
||||
child,
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
extension on ThemeMode {
|
||||
String get humanName {
|
||||
switch (this) {
|
||||
case ThemeMode.system:
|
||||
return t.settings.themeOptions.system;
|
||||
case ThemeMode.light:
|
||||
return t.settings.themeOptions.light;
|
||||
case ThemeMode.dark:
|
||||
return t.settings.themeOptions.dark;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:localsend_app/model/device.dart';
|
||||
import 'package:localsend_app/provider/network_info_provider.dart';
|
||||
import 'package:localsend_app/service/polling_service.dart';
|
||||
|
||||
final nearbyDevicesProvider = StreamProvider.autoDispose<List<Device>>((ref) {
|
||||
final networkInfo = ref.watch(networkInfoProvider)!;
|
||||
final localIp = networkInfo.localIp;
|
||||
|
||||
if (localIp == null) {
|
||||
return Stream.value([]);
|
||||
}
|
||||
|
||||
return PollingService(localIp.split('.').take(3).join('.')).startPolling();
|
||||
});
|
||||
@@ -0,0 +1,27 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:localsend_app/model/network_info.dart';
|
||||
import 'package:network_info_plus/network_info_plus.dart' as plugin;
|
||||
|
||||
final networkInfoProvider = StateNotifierProvider<NetworkInfoNotifier, NetworkInfo?>((ref) => NetworkInfoNotifier());
|
||||
|
||||
class NetworkInfoNotifier extends StateNotifier<NetworkInfo?> {
|
||||
NetworkInfoNotifier() : super(null) {
|
||||
_init();
|
||||
}
|
||||
|
||||
Future<void> _init() async {
|
||||
final info = plugin.NetworkInfo();
|
||||
String? ip;
|
||||
String? mask;
|
||||
try {
|
||||
ip = await info.getWifiIP();
|
||||
mask = await info.getWifiSubmask();
|
||||
} catch (e) {
|
||||
print(e);
|
||||
}
|
||||
state = NetworkInfo(
|
||||
localIp: ip,
|
||||
netMask: mask,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:localsend_app/constants.dart';
|
||||
import 'package:localsend_app/model/dto/info_dto.dart';
|
||||
import 'package:localsend_app/provider/settings_provider.dart';
|
||||
import 'package:localsend_app/util/device_info_helper.dart';
|
||||
import 'package:shelf/shelf.dart';
|
||||
import 'package:shelf/shelf_io.dart';
|
||||
import 'package:shelf_router/shelf_router.dart';
|
||||
|
||||
final receiverProvider = StateNotifierProvider((ref) {
|
||||
final alias = ref.watch(settingsProvider.select((settings) => settings.alias));
|
||||
return ReceiverNotifier(alias);
|
||||
});
|
||||
|
||||
class ReceiverNotifier extends StateNotifier<HttpServer?> {
|
||||
final String alias;
|
||||
|
||||
ReceiverNotifier(this.alias) : super(null);
|
||||
|
||||
Future<void> startServer() async {
|
||||
if (state != null) {
|
||||
print('Server already running.');
|
||||
return;
|
||||
}
|
||||
|
||||
final app = Router();
|
||||
|
||||
final deviceInfo = await getDeviceInfo();
|
||||
app.get('/localsend/v1/info', (Request request) {
|
||||
final dto = InfoDto(
|
||||
alias: alias,
|
||||
deviceModel: deviceInfo.deviceModel,
|
||||
deviceType: deviceInfo.deviceType,
|
||||
);
|
||||
return Response.ok(jsonEncode(dto.toJson()), headers: {'Content-Type': 'application/json'});
|
||||
});
|
||||
|
||||
app.get('/user/<user>', (Request request, String user) {
|
||||
return Response.ok('hello $user');
|
||||
});
|
||||
|
||||
print('Starting server...');
|
||||
state = await serve(app, '0.0.0.0', Constants.defaultPort);
|
||||
print('Server started. (Port: ${state?.port})');
|
||||
}
|
||||
|
||||
Future<void> stop() async {
|
||||
await state?.close(force: true);
|
||||
state = null;
|
||||
print('Server stopped.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:localsend_app/model/settings.dart';
|
||||
import 'package:localsend_app/service/persistence_service.dart';
|
||||
|
||||
final settingsProvider = StateNotifierProvider<SettingsNotifier, Settings>((ref) {
|
||||
throw Exception('settingsProvider not initialized');
|
||||
});
|
||||
|
||||
class SettingsNotifier extends StateNotifier<Settings> {
|
||||
final PersistenceService _service;
|
||||
|
||||
SettingsNotifier(this._service) : super(_loadFromPersistence(_service));
|
||||
|
||||
static Settings _loadFromPersistence(PersistenceService service) {
|
||||
return Settings(
|
||||
alias: service.getAlias(),
|
||||
theme: service.getTheme(),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> setAlias(String alias) async {
|
||||
await _service.setAlias(alias);
|
||||
state = state.copyWith(
|
||||
alias: alias,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> setTheme(ThemeMode theme) async {
|
||||
await _service.setTheme(theme);
|
||||
state = state.copyWith(
|
||||
theme: theme,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:localsend_app/util/alias_generator.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
const _aliasKey = 'alias';
|
||||
const _themeKey = 'theme';
|
||||
|
||||
/// This service abstracts the persistence layer.
|
||||
class PersistenceService {
|
||||
final SharedPreferences _prefs;
|
||||
|
||||
PersistenceService._(this._prefs);
|
||||
|
||||
static Future<PersistenceService> initialize() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
|
||||
if (prefs.getString(_aliasKey) == null) {
|
||||
await prefs.setString(_aliasKey, generateRandomAlias());
|
||||
}
|
||||
|
||||
if (prefs.getString(_themeKey) == null) {
|
||||
await prefs.setString(_themeKey, ThemeMode.system.name);
|
||||
}
|
||||
|
||||
return PersistenceService._(prefs);
|
||||
}
|
||||
|
||||
String getAlias() {
|
||||
return _prefs.getString(_aliasKey) ?? generateRandomAlias();
|
||||
}
|
||||
|
||||
Future<void> setAlias(String alias) async {
|
||||
await _prefs.setString(_aliasKey, alias);
|
||||
}
|
||||
|
||||
ThemeMode getTheme() {
|
||||
final value = _prefs.getString(_themeKey);
|
||||
if (value == null) {
|
||||
return ThemeMode.system;
|
||||
}
|
||||
return ThemeMode.values.firstWhereOrNull((theme) => theme.name == value) ?? ThemeMode.system;
|
||||
}
|
||||
|
||||
Future<void> setTheme(ThemeMode theme) async {
|
||||
await _prefs.setString(_themeKey, theme.name);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:localsend_app/constants.dart';
|
||||
import 'package:localsend_app/model/device.dart';
|
||||
import 'package:localsend_app/model/dto/info_dto.dart';
|
||||
import 'package:localsend_app/util/sleep.dart';
|
||||
import 'package:localsend_app/util/task_runner.dart';
|
||||
|
||||
final _dio = Dio(
|
||||
BaseOptions(
|
||||
connectTimeout: 2000,
|
||||
sendTimeout: 2000,
|
||||
),
|
||||
);
|
||||
|
||||
const _concurrentRequests = 50;
|
||||
|
||||
class PollingService {
|
||||
final List<String> _possibleIps;
|
||||
int _requestCount = 0;
|
||||
bool _running = false;
|
||||
|
||||
PollingService(String ipPrefix) : _possibleIps = List.generate(256, (i) => '$ipPrefix.$i');
|
||||
|
||||
Stream<List<Device>> startPolling() async* {
|
||||
if (_running) {
|
||||
print('Already running poll.');
|
||||
return;
|
||||
}
|
||||
_running = true;
|
||||
final deviceMap = <String, Device>{}; // ip -> device
|
||||
final runner = TaskRunner<int, _RunnerResult>(
|
||||
task: _doRequest,
|
||||
maxConcurrentTasks: _concurrentRequests,
|
||||
);
|
||||
|
||||
runner.addAll(List.generate(_concurrentRequests, (i) => i));
|
||||
|
||||
await for (final result in runner.stream) {
|
||||
final device = result.device;
|
||||
if (device != null) {
|
||||
deviceMap[device.ip] = device;
|
||||
yield deviceMap.values.toList();
|
||||
}
|
||||
|
||||
if (_requestCount >= 1024) {
|
||||
_running = false;
|
||||
}
|
||||
if (_running) {
|
||||
_requestCount++;
|
||||
// print('#$_requestCount');
|
||||
runner.add((result.index + _concurrentRequests) % 256);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<_RunnerResult> _doRequest(int index) async {
|
||||
if (_requestCount > 512) {
|
||||
await sleepAsync(2000);
|
||||
} else if (_requestCount > 256) {
|
||||
await sleepAsync(1000);
|
||||
}
|
||||
|
||||
final String currentIp = _possibleIps[index];
|
||||
// print('Requesting $currentIp');
|
||||
final url = 'http://$currentIp:${Constants.defaultPort}/localsend/v1/info';
|
||||
Device? device;
|
||||
try {
|
||||
final response = await _dio.get(url);
|
||||
final dto = InfoDto.fromJson(response.data);
|
||||
device = Device(
|
||||
ip: currentIp,
|
||||
alias: dto.alias,
|
||||
deviceModel: dto.deviceModel,
|
||||
deviceType: dto.deviceType,
|
||||
);
|
||||
} on DioError catch (e) {
|
||||
device = null;
|
||||
// print('$url: ${e.error}');
|
||||
} catch (e) {
|
||||
device = null;
|
||||
// print(e);
|
||||
}
|
||||
return _RunnerResult(index, device);
|
||||
}
|
||||
|
||||
void cancel() {
|
||||
_running = true;
|
||||
}
|
||||
}
|
||||
|
||||
class _RunnerResult {
|
||||
final int index;
|
||||
final Device? device;
|
||||
|
||||
_RunnerResult(this.index, this.device);
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:localsend_app/constants.dart';
|
||||
import 'package:shelf/shelf.dart';
|
||||
import 'package:shelf/shelf_io.dart' as io;
|
||||
import 'package:shelf_router/shelf_router.dart';
|
||||
|
||||
class ReceiverService {
|
||||
HttpServer? _server;
|
||||
|
||||
ReceiverService();
|
||||
|
||||
Future<void> start() async {
|
||||
if (_server != null) {
|
||||
print('Server already running.');
|
||||
return;
|
||||
}
|
||||
|
||||
final app = Router();
|
||||
|
||||
app.get('/hello', (Request request) {
|
||||
return Response.ok('hello-world');
|
||||
});
|
||||
|
||||
app.get('/user/<user>', (Request request, String user) {
|
||||
return Response.ok('hello $user');
|
||||
});
|
||||
|
||||
print('Starting server...');
|
||||
_server = await io.serve(app, '0.0.0.0', Constants.defaultPort);
|
||||
print('Server started. (Port: ${_server?.port})');
|
||||
}
|
||||
|
||||
Future<void> stop() async {
|
||||
await _server?.close(force: true);
|
||||
_server = null;
|
||||
print('Server stopped.');
|
||||
}
|
||||
|
||||
bool get running => _server != null;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
ThemeData getTheme(Brightness brightness) {
|
||||
return ThemeData(
|
||||
brightness: brightness,
|
||||
primarySwatch: Colors.teal,
|
||||
useMaterial3: true,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
|
||||
import 'dart:math';
|
||||
|
||||
const _adj = ['Fast', 'Slow', 'Big', 'Small', 'Good', 'Bad'];
|
||||
|
||||
const _fruit = ['Apple', 'Banana', 'Cherry', 'Coconut', 'Lemon', 'Orange'];
|
||||
|
||||
String generateRandomAlias() {
|
||||
final random = Random();
|
||||
return '${_adj[random.nextInt(_adj.length)]} ${_fruit[random.nextInt(_adj.length)]}';
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:localsend_app/model/device.dart';
|
||||
import 'package:slang/builder/model/enums.dart';
|
||||
import 'package:slang/builder/utils/string_extensions.dart';
|
||||
|
||||
class DeviceInfoResult {
|
||||
final DeviceType deviceType;
|
||||
final String? deviceModel;
|
||||
|
||||
DeviceInfoResult(this.deviceType, this.deviceModel);
|
||||
}
|
||||
|
||||
Future<DeviceInfoResult> getDeviceInfo() async {
|
||||
final plugin = DeviceInfoPlugin();
|
||||
final DeviceType deviceType;
|
||||
final String? deviceModel;
|
||||
|
||||
if (kIsWeb) {
|
||||
deviceType = DeviceType.web;
|
||||
final deviceInfo = await plugin.webBrowserInfo;
|
||||
deviceModel = deviceInfo.browserName.humanName;
|
||||
} else {
|
||||
switch (defaultTargetPlatform) {
|
||||
case TargetPlatform.android:
|
||||
case TargetPlatform.iOS:
|
||||
deviceType = DeviceType.mobile;
|
||||
break;
|
||||
case TargetPlatform.linux:
|
||||
case TargetPlatform.macOS:
|
||||
case TargetPlatform.windows:
|
||||
case TargetPlatform.fuchsia:
|
||||
deviceType = DeviceType.desktop;
|
||||
break;
|
||||
}
|
||||
|
||||
switch (defaultTargetPlatform) {
|
||||
case TargetPlatform.android:
|
||||
final deviceInfo = await plugin.androidInfo;
|
||||
deviceModel = deviceInfo.brand.toCase(CaseStyle.pascal);
|
||||
break;
|
||||
case TargetPlatform.iOS:
|
||||
final deviceInfo = await plugin.iosInfo;
|
||||
deviceModel = deviceInfo.localizedModel;
|
||||
break;
|
||||
case TargetPlatform.linux:
|
||||
final info = await plugin.linuxInfo;
|
||||
deviceModel = info.name;
|
||||
break;
|
||||
case TargetPlatform.macOS:
|
||||
deviceModel = 'MacOS';
|
||||
break;
|
||||
case TargetPlatform.windows:
|
||||
deviceModel = 'Windows';
|
||||
break;
|
||||
case TargetPlatform.fuchsia:
|
||||
deviceModel = 'Fuchsia';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return DeviceInfoResult(deviceType, deviceModel);
|
||||
}
|
||||
|
||||
extension on BrowserName {
|
||||
String? get humanName {
|
||||
switch (this) {
|
||||
case BrowserName.firefox:
|
||||
return 'Firefox';
|
||||
case BrowserName.samsungInternet:
|
||||
return 'Samsung Internet';
|
||||
case BrowserName.opera:
|
||||
return 'Opera';
|
||||
case BrowserName.msie:
|
||||
return 'Internet Explorer';
|
||||
case BrowserName.edge:
|
||||
return 'Microsoft Edge';
|
||||
case BrowserName.chrome:
|
||||
return 'Google Chrome';
|
||||
case BrowserName.safari:
|
||||
return 'Safari';
|
||||
case BrowserName.unknown:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
extension StringIpExt on String {
|
||||
String get visualId => split('.').last;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
Future<void> sleepAsync(int millis) {
|
||||
return Future.delayed(Duration(milliseconds: millis), () {});
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import 'dart:async';
|
||||
import 'dart:collection';
|
||||
|
||||
// https://stackoverflow.com/questions/62878704/how-to-implement-an-async-task-queue-with-multiple-concurrent-workers-async-in
|
||||
class TaskRunner<A, B> {
|
||||
final Queue<A> _input = Queue();
|
||||
final StreamController<B> _streamController = StreamController();
|
||||
final Future<B> Function(A) task;
|
||||
|
||||
final int maxConcurrentTasks;
|
||||
int runningTasks = 0;
|
||||
|
||||
TaskRunner({
|
||||
required this.task,
|
||||
required this.maxConcurrentTasks,
|
||||
});
|
||||
|
||||
Stream<B> get stream => _streamController.stream;
|
||||
|
||||
void add(A value) {
|
||||
_input.add(value);
|
||||
_startExecution();
|
||||
}
|
||||
|
||||
void addAll(Iterable<A> iterable) {
|
||||
_input.addAll(iterable);
|
||||
_startExecution();
|
||||
}
|
||||
|
||||
void _startExecution() {
|
||||
if (runningTasks == maxConcurrentTasks || _input.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
while (_input.isNotEmpty && runningTasks < maxConcurrentTasks) {
|
||||
runningTasks++;
|
||||
|
||||
task(_input.removeFirst()).then((value) async {
|
||||
_streamController.add(value);
|
||||
|
||||
while (_input.isNotEmpty) {
|
||||
_streamController.add(await task(_input.removeFirst()));
|
||||
}
|
||||
|
||||
runningTasks--;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:localsend_app/model/device.dart';
|
||||
import 'package:localsend_app/util/ip_helper.dart';
|
||||
|
||||
class DeviceWidget extends StatelessWidget {
|
||||
final Device device;
|
||||
|
||||
const DeviceWidget({required this.device});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(15),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(device.deviceType.icon, size: 46),
|
||||
const SizedBox(width: 15),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(device.alias, style: const TextStyle(fontSize: 20)),
|
||||
const SizedBox(height: 5),
|
||||
Wrap(
|
||||
runSpacing: 10,
|
||||
spacing: 10,
|
||||
children: [
|
||||
_Badge(
|
||||
color: Theme.of(context).colorScheme.tertiaryContainer,
|
||||
label: '#${device.ip.visualId}',
|
||||
),
|
||||
if (device.deviceModel != null)
|
||||
_Badge(
|
||||
color: Theme.of(context).colorScheme.tertiaryContainer,
|
||||
label: device.deviceModel!,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Badge extends StatelessWidget {
|
||||
final Color color;
|
||||
final String label;
|
||||
|
||||
const _Badge({required this.color, required this.label});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
),
|
||||
child: Text(label, style: TextStyle(color: Theme.of(context).colorScheme.onInverseSurface)),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class RotatingWidget extends StatefulWidget {
|
||||
final Duration duration;
|
||||
final Widget child;
|
||||
|
||||
const RotatingWidget({required this.duration, required this.child, super.key});
|
||||
|
||||
@override
|
||||
State<RotatingWidget> createState() => _RotatingWidgetState();
|
||||
}
|
||||
|
||||
class _RotatingWidgetState extends State<RotatingWidget> with TickerProviderStateMixin{
|
||||
|
||||
late final AnimationController _controller = AnimationController(
|
||||
duration: widget.duration,
|
||||
vsync: this,
|
||||
)..repeat();
|
||||
|
||||
late final Animation<double> _animation = CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: Curves.linear,
|
||||
);
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return RotationTransition(
|
||||
turns: _animation,
|
||||
child: widget.child,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,14 @@
|
||||
|
||||
#include "generated_plugin_registrant.h"
|
||||
|
||||
#include <screen_retriever/screen_retriever_plugin.h>
|
||||
#include <window_manager/window_manager_plugin.h>
|
||||
|
||||
void fl_register_plugins(FlPluginRegistry* registry) {
|
||||
g_autoptr(FlPluginRegistrar) screen_retriever_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "ScreenRetrieverPlugin");
|
||||
screen_retriever_plugin_register_with_registrar(screen_retriever_registrar);
|
||||
g_autoptr(FlPluginRegistrar) window_manager_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "WindowManagerPlugin");
|
||||
window_manager_plugin_register_with_registrar(window_manager_registrar);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
#
|
||||
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
screen_retriever
|
||||
window_manager
|
||||
)
|
||||
|
||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||
|
||||
@@ -5,8 +5,16 @@
|
||||
import FlutterMacOS
|
||||
import Foundation
|
||||
|
||||
import device_info_plus
|
||||
import network_info_plus
|
||||
import screen_retriever
|
||||
import shared_preferences_macos
|
||||
import window_manager
|
||||
|
||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
|
||||
NetworkInfoPlusPlugin.register(with: registry.registrar(forPlugin: "NetworkInfoPlusPlugin"))
|
||||
ScreenRetrieverPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverPlugin"))
|
||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||
WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin"))
|
||||
}
|
||||
|
||||
+520
-2
@@ -1,6 +1,20 @@
|
||||
# Generated by pub
|
||||
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||
packages:
|
||||
_fe_analyzer_shared:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: _fe_analyzer_shared
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "50.0.0"
|
||||
analyzer:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: analyzer
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "5.2.0"
|
||||
args:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -15,6 +29,62 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.9.0"
|
||||
build:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.3.1"
|
||||
build_config:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build_config
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
build_daemon:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build_daemon
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.1.0"
|
||||
build_resolvers:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build_resolvers
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
build_runner:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: build_runner
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.3.3"
|
||||
build_runner_core:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build_runner_core
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "7.2.7"
|
||||
built_collection:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: built_collection
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "5.1.1"
|
||||
built_value:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: built_value
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "8.4.2"
|
||||
characters:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -22,13 +92,76 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.2.1"
|
||||
collection:
|
||||
checked_yaml:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: checked_yaml
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.1"
|
||||
clock:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: clock
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
code_builder:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: code_builder
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "4.3.0"
|
||||
collection:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: collection
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.16.0"
|
||||
color:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: color
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.0.0"
|
||||
convert:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: convert
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.1.1"
|
||||
crypto:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: crypto
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.0.2"
|
||||
csv:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: csv
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "5.0.1"
|
||||
dart_style:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: dart_style
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.2.4"
|
||||
dartx:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: dartx
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
dbus:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -36,6 +169,20 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.7.8"
|
||||
device_info_plus:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: device_info_plus
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "8.0.0"
|
||||
device_info_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: device_info_plus_platform_interface
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "7.0.0"
|
||||
dio:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -50,11 +197,39 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.1"
|
||||
file:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: file
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "6.1.4"
|
||||
fixnum:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: fixnum
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.1"
|
||||
flutter:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_gen_core:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_gen_core
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "5.1.0+1"
|
||||
flutter_gen_runner:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: flutter_gen_runner
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "5.1.0+1"
|
||||
flutter_lints:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
@@ -62,11 +237,53 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.1"
|
||||
flutter_riverpod:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_riverpod
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
flutter_web_plugins:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
freezed:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: freezed
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.3.2"
|
||||
freezed_annotation:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: freezed_annotation
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
frontend_server_client:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: frontend_server_client
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.2.0"
|
||||
glob:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: glob
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
graphs:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: graphs
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
http_methods:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -74,6 +291,13 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
http_multi_server:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: http_multi_server
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.2.1"
|
||||
http_parser:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -81,6 +305,13 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "4.0.2"
|
||||
io:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: io
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.3"
|
||||
js:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -88,6 +319,27 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.6.4"
|
||||
json2yaml:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: json2yaml
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.0.0"
|
||||
json_annotation:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: json_annotation
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "4.7.0"
|
||||
json_serializable:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: json_serializable
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "6.5.4"
|
||||
lints:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -95,6 +347,20 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.1"
|
||||
logging:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: logging
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
matcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: matcher
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.12.14"
|
||||
material_color_utilities:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -109,6 +375,13 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.8.0"
|
||||
mime:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: mime
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.3"
|
||||
network_info_plus:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -130,6 +403,13 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.5.0"
|
||||
package_config:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: package_config
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
path:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -137,6 +417,27 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.8.2"
|
||||
path_provider_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_linux
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.7"
|
||||
path_provider_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_platform_interface
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.5"
|
||||
path_provider_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_windows
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.3"
|
||||
petitparser:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -144,6 +445,13 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "5.1.0"
|
||||
platform:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: platform
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.1.0"
|
||||
plugin_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -151,6 +459,104 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.3"
|
||||
pool:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pool
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.5.1"
|
||||
process:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: process
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "4.2.4"
|
||||
pub_semver:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pub_semver
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.3"
|
||||
pubspec_parse:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pubspec_parse
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.2.1"
|
||||
riverpod:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: riverpod
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
screen_retriever:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: screen_retriever
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.1.4"
|
||||
shared_preferences:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: shared_preferences
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.15"
|
||||
shared_preferences_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_android
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.14"
|
||||
shared_preferences_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_ios
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
shared_preferences_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_linux
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.2"
|
||||
shared_preferences_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_macos
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.4"
|
||||
shared_preferences_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_platform_interface
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
shared_preferences_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_web
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.4"
|
||||
shared_preferences_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_windows
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.2"
|
||||
shelf:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -165,11 +571,53 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.1.3"
|
||||
shelf_web_socket:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shelf_web_socket
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.3"
|
||||
sky_engine:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.99"
|
||||
slang:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: slang
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.7.0"
|
||||
slang_build_runner:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: slang_build_runner
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.7.0"
|
||||
slang_flutter:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: slang_flutter
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.7.0"
|
||||
source_gen:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: source_gen
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.2.6"
|
||||
source_helper:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: source_helper
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.3.3"
|
||||
source_span:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -184,6 +632,13 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.10.0"
|
||||
state_notifier:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: state_notifier
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.7.2+1"
|
||||
stream_channel:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -191,6 +646,13 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
stream_transform:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: stream_transform
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
string_scanner:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -205,6 +667,20 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.2.1"
|
||||
time:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: time
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.3"
|
||||
timing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: timing
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
typed_data:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -219,6 +695,41 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.2"
|
||||
watcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: watcher
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.2"
|
||||
web_socket_channel:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: web_socket_channel
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
win32:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: win32
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.1.3"
|
||||
window_manager:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: window_manager
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.2.8"
|
||||
xdg_directories:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: xdg_directories
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.2.0+2"
|
||||
xml:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -226,6 +737,13 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "6.1.0"
|
||||
yaml:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: yaml
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.1.1"
|
||||
sdks:
|
||||
dart: ">=2.18.5 <3.0.0"
|
||||
flutter: ">=2.11.0"
|
||||
flutter: ">=3.0.0"
|
||||
|
||||
@@ -9,15 +9,32 @@ environment:
|
||||
sdk: '>=2.18.5 <3.0.0'
|
||||
|
||||
dependencies:
|
||||
collection: 1.16.0
|
||||
device_info_plus: 8.0.0
|
||||
dio: 4.0.6
|
||||
flutter:
|
||||
sdk: flutter
|
||||
network_info_plus: 3.0.1
|
||||
flutter_riverpod: 2.1.1
|
||||
freezed_annotation: 2.2.0
|
||||
json_annotation: 4.7.0
|
||||
shared_preferences: 2.0.15
|
||||
shelf: 1.4.0
|
||||
shelf_router: 1.1.3
|
||||
slang: 3.7.0
|
||||
slang_flutter: 3.7.0
|
||||
window_manager: 0.2.8
|
||||
|
||||
dev_dependencies:
|
||||
build_runner: 2.3.3
|
||||
flutter_gen_runner: 5.1.0+1
|
||||
flutter_lints: 2.0.1
|
||||
freezed: 2.3.2
|
||||
json_serializable: 6.5.4
|
||||
slang_build_runner: 3.7.0
|
||||
|
||||
flutter:
|
||||
uses-material-design: true
|
||||
|
||||
assets:
|
||||
- assets/img/
|
||||
@@ -7,8 +7,14 @@
|
||||
#include "generated_plugin_registrant.h"
|
||||
|
||||
#include <network_info_plus/network_info_plus_windows_plugin.h>
|
||||
#include <screen_retriever/screen_retriever_plugin.h>
|
||||
#include <window_manager/window_manager_plugin.h>
|
||||
|
||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||
NetworkInfoPlusWindowsPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("NetworkInfoPlusWindowsPlugin"));
|
||||
ScreenRetrieverPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("ScreenRetrieverPlugin"));
|
||||
WindowManagerPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("WindowManagerPlugin"));
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
network_info_plus
|
||||
screen_retriever
|
||||
window_manager
|
||||
)
|
||||
|
||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||
|
||||
@@ -27,7 +27,7 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
|
||||
FlutterWindow window(project);
|
||||
Win32Window::Point origin(10, 10);
|
||||
Win32Window::Size size(1280, 720);
|
||||
if (!window.CreateAndShow(L"localsend_app", origin, size)) {
|
||||
if (!window.CreateAndShow(L"LocalSend", origin, size)) {
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
window.SetQuitOnClose(true);
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 9.4 KiB |
Reference in New Issue
Block a user