feat: custom device target

This commit is contained in:
Tien Do Nam
2023-01-02 02:31:11 +01:00
parent 7900234448
commit bbfda295b6
15 changed files with 250 additions and 21 deletions
+5
View File
@@ -1,3 +1,8 @@
## 1.3.0 (2023-01-)
- feat: enter custom target address
- fix: update nearby device attributes when scan again
## 1.2.0 (2022-12-31)
- feat: drag and drop files
+7
View File
@@ -13,6 +13,7 @@
"done": "Done",
"edit": "Edit",
"error": "Error",
"example": "Example",
"finished": "Finished",
"hide": "Hide",
"offline": "Offline",
@@ -48,6 +49,7 @@
},
"nearbyDevices": "Nearby devices",
"thisDevice": "This Device",
"scan": "Look for devices",
"help": "Please ensure that the desired target is also in the same wifi network.",
"placeItems": "Place items to share."
},
@@ -121,6 +123,11 @@
},
"messageInput": {
"title": "Type message"
},
"addressInput": {
"title": "Enter address",
"hashtag": "Hashtag",
"ip": "IP"
}
}
}
+7
View File
@@ -13,6 +13,7 @@
"done": "Fertig",
"edit": "Bearbeiten",
"error": "Fehler",
"example": "Beispiel",
"finished": "Abgeschlossen",
"hide": "Verstecken",
"offline": "Offline",
@@ -48,6 +49,7 @@
},
"nearbyDevices": "Geräte in der Nähe",
"thisDevice": "Dieses Gerät",
"scan": "Geräte suchen",
"help": "Bitte stelle sicher, dass sich das gewünschte Ziel auch im selben WLAN-Netzwerk befindet.",
"placeItems": "Dateien ablegen, um zu teilen."
},
@@ -121,6 +123,11 @@
},
"messageInput": {
"title": "Nachricht eingeben"
},
"addressInput": {
"title": "Adresse eingeben",
"hashtag": "Hashtag",
"ip": "IP"
}
}
}
+1 -1
View File
@@ -6,9 +6,9 @@ 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/network/server_provider.dart';
import 'package:localsend_app/provider/persistence_provider.dart';
import 'package:localsend_app/provider/selected_files_provider.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:localsend_app/util/snackbar.dart';
import 'package:share_handler/share_handler.dart';
+2 -1
View File
@@ -5,6 +5,7 @@ import 'package:localsend_app/gen/strings.g.dart';
import 'package:localsend_app/init.dart';
import 'package:localsend_app/pages/home_page.dart';
import 'package:localsend_app/provider/device_info_provider.dart';
import 'package:localsend_app/provider/persistence_provider.dart';
import 'package:localsend_app/provider/settings_provider.dart';
import 'package:localsend_app/theme.dart';
import 'package:localsend_app/util/device_info_helper.dart';
@@ -16,7 +17,7 @@ Future<void> main() async {
child: ProviderScope(
overrides: [
deviceRawInfoProvider.overrideWithValue(await getDeviceInfo()),
settingsProvider.overrideWith((ref) => SettingsNotifier(persistenceService)),
persistenceProvider.overrideWithValue(persistenceService),
],
child: const LocalSendApp(),
),
+1 -1
View File
@@ -7,6 +7,6 @@ part 'nearby_devices_state.freezed.dart';
class NearbyDevicesState with _$NearbyDevicesState {
const factory NearbyDevicesState({
required bool running,
required List<Device> devices,
required Map<String, Device> devices, // ip -> device
}) = _NearbyDevicesState;
}
+44 -7
View File
@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:localsend_app/gen/strings.g.dart';
import 'package:localsend_app/model/cross_file.dart';
import 'package:localsend_app/model/device.dart';
import 'package:localsend_app/pages/selected_files_page.dart';
import 'package:localsend_app/provider/device_info_provider.dart';
import 'package:localsend_app/provider/network/nearby_devices_provider.dart';
@@ -14,6 +15,7 @@ import 'package:localsend_app/util/file_picker.dart';
import 'package:localsend_app/util/file_size_helper.dart';
import 'package:localsend_app/widget/big_button.dart';
import 'package:localsend_app/widget/dialogs/add_file_dialog.dart';
import 'package:localsend_app/widget/dialogs/address_input_dialog.dart';
import 'package:localsend_app/widget/dialogs/no_files_dialog.dart';
import 'package:localsend_app/widget/list_tile/device_list_tile.dart';
import 'package:localsend_app/widget/rotating_widget.dart';
@@ -182,17 +184,52 @@ class _SendTabState extends ConsumerState<SendTab> {
Row(
children: [
Text(t.sendTab.nearbyDevices, style: Theme.of(context).textTheme.subtitle1),
RotatingWidget(
duration: const Duration(seconds: 2),
spinning: nearbyDevicesState.running,
reverse: true,
const SizedBox(width: 10),
Tooltip(
message: t.sendTab.scan,
child: RotatingWidget(
duration: const Duration(seconds: 2),
spinning: nearbyDevicesState.running,
reverse: true,
child: TextButton(
style: TextButton.styleFrom(
foregroundColor: Theme.of(context).colorScheme.onSurface,
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
minimumSize: Size.zero,
shape: const CircleBorder(),
),
onPressed: _scan,
child: const Icon(Icons.sync),
),
),
),
Tooltip(
message: t.dialogs.addressInput.title,
child: TextButton(
style: TextButton.styleFrom(
foregroundColor: Theme.of(context).colorScheme.onSurface,
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
minimumSize: Size.zero,
shape: const CircleBorder(),
),
onPressed: _scan,
child: const Icon(Icons.sync),
onPressed: () async {
final files = ref.read(selectedFilesProvider);
if (files.isEmpty) {
context.pushBottomSheet(() => const NoFilesDialog());
return;
}
final device = await showDialog<Device?>(
context: context,
builder: (_) => const AddressInputDialog(),
);
if (device != null && mounted) {
ref.read(sendProvider.notifier).startSession(
target: device,
files: files,
);
}
},
child: const Icon(Icons.ads_click),
),
),
],
@@ -205,7 +242,7 @@ class _SendTabState extends ConsumerState<SendTab> {
),
),
const SizedBox(height: 10),
...nearbyDevicesState.devices.map((device) {
...nearbyDevicesState.devices.values.map((device) {
return Padding(
padding: const EdgeInsets.only(bottom: 10),
child: Hero(
@@ -14,7 +14,7 @@ final nearbyDevicesProvider = StateNotifierProvider<NearbyDevicesNotifier, Nearb
class NearbyDevicesNotifier extends StateNotifier<NearbyDevicesState> {
final Dio dio;
NearbyDevicesNotifier(this.dio) : super(const NearbyDevicesState(running: false, devices: []));
NearbyDevicesNotifier(this.dio) : super(const NearbyDevicesState(running: false, devices: {}));
Future<void> startScan({required int port, required String localIp}) async {
if (state.running) {
@@ -24,11 +24,9 @@ class NearbyDevicesNotifier extends StateNotifier<NearbyDevicesState> {
state = state.copyWith(running: true);
await _getStream(localIp, port).forEach((device) {
if (state.devices.every((d) => d.ip != device.ip)) {
state = state.copyWith(
devices: [...state.devices, device],
);
}
state = state.copyWith(
devices: {...state.devices}..update(device.ip, (_) => device, ifAbsent: () => device),
);
});
state = state.copyWith(running: false);
+1 -1
View File
@@ -14,8 +14,8 @@ import 'package:localsend_app/model/server/server_state.dart';
import 'package:localsend_app/model/session_status.dart';
import 'package:localsend_app/pages/receive_page.dart';
import 'package:localsend_app/provider/device_info_provider.dart';
import 'package:localsend_app/provider/persistence_provider.dart';
import 'package:localsend_app/provider/progress_provider.dart';
import 'package:localsend_app/service/persistence_service.dart';
import 'package:localsend_app/util/alias_generator.dart';
import 'package:localsend_app/util/api_route_builder.dart';
import 'package:localsend_app/util/device_info_helper.dart';
@@ -0,0 +1,35 @@
import 'package:dio/dio.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:localsend_app/model/device.dart';
import 'package:localsend_app/model/dto/info_dto.dart';
import 'package:localsend_app/provider/dio_provider.dart';
import 'package:localsend_app/util/api_route_builder.dart';
final targetedDiscoveryProvider = Provider((ref) {
final dio = ref.watch(dioProvider(2 * 1000));
return TargetedDiscoveryService(dio);
});
/// Try to discover a single device using the given IP and port.
class TargetedDiscoveryService {
final Dio _dio;
TargetedDiscoveryService(this._dio);
Future<Device?> discover(String ip, int port) async {
final url = ApiRoute.info.targetRaw(ip, port);
Device? device;
try {
final response = await _dio.get(url);
final dto = InfoDto.fromJson(response.data);
device = dto.toDevice(ip, port);
} on DioError catch (e) {
device = null;
print('$url: ${e.error}');
} catch (e) {
device = null;
print(e);
}
return device;
}
}
@@ -1,9 +1,14 @@
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:localsend_app/gen/strings.g.dart';
import 'package:localsend_app/util/alias_generator.dart';
import 'package:shared_preferences/shared_preferences.dart';
final persistenceProvider = Provider<PersistenceService>((ref) {
throw Exception('persistenceProvider not initialized');
});
const _aliasKey = 'ls_alias';
const _themeKey = 'ls_theme';
const _localeKey = 'ls_locale';
+3 -2
View File
@@ -2,10 +2,11 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:localsend_app/gen/strings.g.dart';
import 'package:localsend_app/model/settings.dart';
import 'package:localsend_app/service/persistence_service.dart';
import 'package:localsend_app/provider/persistence_provider.dart';
final settingsProvider = StateNotifierProvider<SettingsNotifier, Settings>((ref) {
throw Exception('settingsProvider not initialized');
final persistenceService = ref.watch(persistenceProvider);
return SettingsNotifier(persistenceService);
});
class SettingsNotifier extends StateNotifier<Settings> {
+7
View File
@@ -1,3 +1,4 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
@@ -36,6 +37,12 @@ ThemeData getTheme(Brightness brightness) {
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
foregroundColor: brightness == Brightness.dark ? Colors.white : null,
padding: defaultTargetPlatform == TargetPlatform.windows ? const EdgeInsets.symmetric(horizontal: 8, vertical: 16) : null,
),
),
textButtonTheme: TextButtonThemeData(
style: TextButton.styleFrom(
padding: defaultTargetPlatform == TargetPlatform.windows ? const EdgeInsets.symmetric(horizontal: 8, vertical: 16) : null,
),
),
);
-2
View File
@@ -1,4 +1,3 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
class BigButton extends StatelessWidget {
@@ -16,7 +15,6 @@ class BigButton extends StatelessWidget {
Widget build(BuildContext context) {
return ElevatedButton(
style: ElevatedButton.styleFrom(
padding: EdgeInsets.symmetric(vertical: defaultTargetPlatform == TargetPlatform.windows ? 20 : 10),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
@@ -0,0 +1,128 @@
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/network/targeted_discovery_provider.dart';
import 'package:localsend_app/provider/network_info_provider.dart';
import 'package:localsend_app/provider/settings_provider.dart';
import 'package:routerino/routerino.dart';
enum _InputMode {
hashtag,
ip;
String get label {
switch (this) {
case _InputMode.hashtag:
return t.dialogs.addressInput.hashtag;
case _InputMode.ip:
return t.dialogs.addressInput.ip;
}
}
}
class AddressInputDialog extends ConsumerStatefulWidget {
const AddressInputDialog();
@override
ConsumerState<AddressInputDialog> createState() => _AddressInputDialogState();
}
class _AddressInputDialogState extends ConsumerState<AddressInputDialog> {
final _addressController = TextEditingController();
final _selected = List.generate(_InputMode.values.length, (index) => index == 0);
_InputMode _mode = _InputMode.hashtag;
bool _fetching = false;
bool _failed = false;
@override
Widget build(BuildContext context) {
final localIp = ref.watch(networkInfoProvider.select((info) => info?.localIp));
final ipPrefix = localIp?.split('.').take(3).join('.');
final settings = ref.watch(settingsProvider);
return AlertDialog(
title: Text(t.dialogs.addressInput.title),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ToggleButtons(
isSelected: _selected,
onPressed: (int index) {
setState(() {
for (int i = 0; i < _selected.length; i++) {
_selected[i] = i == index;
}
_mode = _InputMode.values[index];
});
},
borderRadius: BorderRadius.circular(10),
children: _InputMode.values.map((mode) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Text(mode.label),
);
}).toList(),
),
const SizedBox(height: 20),
TextFormField(
controller: _addressController,
autofocus: true,
enabled: !_fetching,
decoration: InputDecoration(
prefixText: _mode == _InputMode.hashtag ? '# ' : 'IP: ',
),
),
const SizedBox(height: 10),
Text('${t.general.example}: ${_mode == _InputMode.hashtag ? '123' : '${ipPrefix ?? '192.168.2'}.123'}'),
if (_failed)
Padding(
padding: const EdgeInsets.only(top: 10),
child: Text(t.general.error, style: const TextStyle(color: Colors.orange)),
),
],
),
actions: [
TextButton(
style: ElevatedButton.styleFrom(
foregroundColor: Theme.of(context).brightness == Brightness.dark ? Theme.of(context).buttonTheme.colorScheme!.onPrimary : null,
),
onPressed: () => context.pop(),
child: Text(t.general.cancel),
),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Theme.of(context).buttonTheme.colorScheme!.primary,
foregroundColor: Theme.of(context).buttonTheme.colorScheme!.onPrimary,
),
onPressed: _fetching ? null : () async {
final String ip;
final String input = _addressController.text.trim();
if (_mode == _InputMode.ip) {
ip = input;
} else {
ip = '$ipPrefix.$input';
}
setState(() {
_fetching = true;
});
final device = await ref.read(targetedDiscoveryProvider).discover(ip, settings.port);
if (device != null) {
if (mounted) {
context.pop(device);
}
} else {
setState(() {
_fetching = false;
_failed = true;
});
}
},
child: Text(t.general.confirm),
),
],
);
}
}