mirror of
https://github.com/localsend/localsend.git
synced 2026-06-23 04:10:07 +00:00
feat: support multiple local IP addresses
This commit is contained in:
@@ -1,3 +1,7 @@
|
||||
## 1.4.0 (2023-01-)
|
||||
|
||||
- feat: support multiple local IP addresses
|
||||
|
||||
## 1.3.1 (2023-01-03)
|
||||
|
||||
- fix: local IP sometimes not found
|
||||
|
||||
@@ -136,7 +136,7 @@
|
||||
"addressInput": {
|
||||
"title": "Enter address",
|
||||
"hashtag": "Hashtag",
|
||||
"ip": "IP"
|
||||
"ip": "IP Address"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,7 +136,7 @@
|
||||
"addressInput": {
|
||||
"title": "Adresse eingeben",
|
||||
"hashtag": "Hashtag",
|
||||
"ip": "IP"
|
||||
"ip": "IP-Adresse"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ part 'nearby_devices_state.freezed.dart';
|
||||
@freezed
|
||||
class NearbyDevicesState with _$NearbyDevicesState {
|
||||
const factory NearbyDevicesState({
|
||||
required bool running,
|
||||
required Set<String> runningIps, // list of local ips
|
||||
required Map<String, Device> devices, // ip -> device
|
||||
}) = _NearbyDevicesState;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ part 'network_info.freezed.dart';
|
||||
@freezed
|
||||
class NetworkInfo with _$NetworkInfo {
|
||||
const factory NetworkInfo({
|
||||
required String? localIp,
|
||||
required String? netMask,
|
||||
required List<String> localIps,
|
||||
required String? netMask, // not used
|
||||
}) = _NetworkInfo;
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ class _ReceiveTagState extends ConsumerState<ReceiveTab> with AutomaticKeepAlive
|
||||
duration: const Duration(milliseconds: 300),
|
||||
delay: const Duration(milliseconds: 500),
|
||||
child: Text(
|
||||
serverState == null ? t.general.offline : '#${networkInfo?.localIp?.visualId ?? '?'}',
|
||||
serverState == null ? t.general.offline : networkInfo?.localIps.map((ip) => '#${ip.visualId}').toSet().join(' ') ?? '?',
|
||||
style: const TextStyle(fontSize: 24),
|
||||
),
|
||||
),
|
||||
@@ -105,7 +105,14 @@ class _ReceiveTagState extends ConsumerState<ReceiveTab> with AutomaticKeepAlive
|
||||
children: [
|
||||
Text(t.receiveTab.infoBox.ip),
|
||||
const SizedBox(width: 10),
|
||||
Text(networkInfo?.localIp ?? t.general.unknown),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (networkInfo?.localIps.isEmpty ?? true)
|
||||
Text(t.general.unknown),
|
||||
...?networkInfo?.localIps.map((ip) => Text(ip)),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
TableRow(
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
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';
|
||||
@@ -37,15 +38,15 @@ class _SendTabState extends ConsumerState<SendTab> {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
final devices = ref.read(nearbyDevicesProvider.select((state) => state.devices));
|
||||
if (devices.isEmpty) {
|
||||
_scan();
|
||||
_scan(null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _scan() {
|
||||
void _scan(String? localIp) {
|
||||
final port = ref.read(settingsProvider.select((settings) => settings.port));
|
||||
final networkInfo = ref.read(networkInfoProvider);
|
||||
final localIp = networkInfo?.localIp;
|
||||
localIp ??= networkInfo?.localIps.firstOrNull;
|
||||
if (localIp != null) {
|
||||
ref.read(nearbyDevicesProvider.notifier).startScan(port: port, localIp: localIp);
|
||||
}
|
||||
@@ -54,6 +55,7 @@ class _SendTabState extends ConsumerState<SendTab> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final selectedFiles = ref.watch(selectedFilesProvider);
|
||||
final networkInfo = ref.watch(networkInfoProvider);
|
||||
final myDevice = ref.watch(deviceInfoProvider);
|
||||
final nearbyDevicesState = ref.watch(nearbyDevicesProvider);
|
||||
final addOptions = [
|
||||
@@ -184,24 +186,25 @@ class _SendTabState extends ConsumerState<SendTab> {
|
||||
children: [
|
||||
Text(t.sendTab.nearbyDevices, style: Theme.of(context).textTheme.subtitle1),
|
||||
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(),
|
||||
if (networkInfo?.localIps.length == 1)
|
||||
Tooltip(
|
||||
message: t.sendTab.scan,
|
||||
child: RotatingWidget(
|
||||
duration: const Duration(seconds: 2),
|
||||
spinning: nearbyDevicesState.runningIps.isNotEmpty,
|
||||
reverse: true,
|
||||
child: TextButton(
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: Theme.of(context).colorScheme.onSurface,
|
||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
minimumSize: Size.zero,
|
||||
shape: const CircleBorder(),
|
||||
),
|
||||
onPressed: () => _scan(null),
|
||||
child: const Icon(Icons.sync),
|
||||
),
|
||||
onPressed: _scan,
|
||||
child: const Icon(Icons.sync),
|
||||
),
|
||||
),
|
||||
),
|
||||
Tooltip(
|
||||
message: t.dialogs.addressInput.title,
|
||||
child: TextButton(
|
||||
@@ -233,6 +236,46 @@ class _SendTabState extends ConsumerState<SendTab> {
|
||||
),
|
||||
],
|
||||
),
|
||||
if ((networkInfo?.localIps.length ?? 0) > 1)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 15),
|
||||
child: Wrap(
|
||||
spacing: 15,
|
||||
runSpacing: 15,
|
||||
children: networkInfo!.localIps.map((ip) {
|
||||
return Card(
|
||||
margin: EdgeInsets.zero,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(right: 8),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Tooltip(
|
||||
message: t.sendTab.scan,
|
||||
child: RotatingWidget(
|
||||
duration: const Duration(seconds: 2),
|
||||
spinning: nearbyDevicesState.runningIps.contains(ip),
|
||||
reverse: true,
|
||||
child: TextButton(
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: Theme.of(context).colorScheme.onSurface,
|
||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
minimumSize: Size.zero,
|
||||
shape: const CircleBorder(),
|
||||
),
|
||||
onPressed: () => _scan(ip),
|
||||
child: const Icon(Icons.sync),
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(ip),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
Hero(
|
||||
tag: 'this-device',
|
||||
child: DeviceListTile(
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:localsend_app/model/device.dart';
|
||||
import 'package:localsend_app/provider/network/server_provider.dart';
|
||||
@@ -13,7 +14,7 @@ final deviceInfoProvider = Provider((ref) {
|
||||
final serverState = ref.watch(serverProvider);
|
||||
final rawInfo = ref.watch(deviceRawInfoProvider);
|
||||
return Device(
|
||||
ip: networkInfo?.localIp ?? '-',
|
||||
ip: networkInfo?.localIps.firstOrNull ?? '-',
|
||||
port: serverState?.port ?? -1,
|
||||
alias: serverState?.alias ?? '-',
|
||||
deviceModel: rawInfo.deviceModel,
|
||||
|
||||
@@ -12,16 +12,19 @@ final nearbyDevicesProvider = StateNotifierProvider<NearbyDevicesNotifier, Nearb
|
||||
return NearbyDevicesNotifier(dio);
|
||||
});
|
||||
|
||||
Map<String, TaskRunner> _runners = {};
|
||||
|
||||
class NearbyDevicesNotifier extends StateNotifier<NearbyDevicesState> {
|
||||
final Dio dio;
|
||||
NearbyDevicesNotifier(this.dio) : super(const NearbyDevicesState(running: false, devices: {}));
|
||||
NearbyDevicesNotifier(this.dio) : super(const NearbyDevicesState(runningIps: {}, devices: {}));
|
||||
|
||||
Future<void> startScan({required int port, required String localIp}) async {
|
||||
if (state.running) {
|
||||
if (state.runningIps.contains(localIp)) {
|
||||
// already running for the same localIp
|
||||
return;
|
||||
}
|
||||
|
||||
state = state.copyWith(running: true);
|
||||
state = state.copyWith(runningIps: {...state.runningIps, localIp});
|
||||
|
||||
await _getStream(localIp, port).forEach((device) {
|
||||
state = state.copyWith(
|
||||
@@ -29,12 +32,13 @@ class NearbyDevicesNotifier extends StateNotifier<NearbyDevicesState> {
|
||||
);
|
||||
});
|
||||
|
||||
state = state.copyWith(running: false);
|
||||
state = state.copyWith(runningIps: state.runningIps.where((ip) => ip != localIp).toSet());
|
||||
}
|
||||
|
||||
Stream<Device> _getStream(String localIp, int port) {
|
||||
final ipList = List.generate(256, (i) => '${localIp.split('.').take(3).join('.')}.$i').where((ip) => ip != localIp).toList();
|
||||
final runner = TaskRunner<Device?>(
|
||||
_runners[localIp]?.stop();
|
||||
_runners[localIp] = TaskRunner<Device?>(
|
||||
initialTasks: List.generate(
|
||||
ipList.length,
|
||||
(index) => () => _doRequest(dio, ipList[index], port),
|
||||
@@ -42,7 +46,7 @@ class NearbyDevicesNotifier extends StateNotifier<NearbyDevicesState> {
|
||||
concurrency: 50,
|
||||
);
|
||||
|
||||
return runner.stream.where((device) => device != null).cast<Device>();
|
||||
return _runners[localIp]!.stream.where((device) => device != null).cast<Device>();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -38,11 +38,12 @@ class NetworkInfoNotifier extends StateNotifier<NetworkInfo?> {
|
||||
print(e);
|
||||
}
|
||||
|
||||
if (!kIsWeb && ip == null) {
|
||||
List<String> nativeResult = [];
|
||||
if (!kIsWeb) {
|
||||
try {
|
||||
// fallback with dart:io NetworkInterface
|
||||
final result = (await NetworkInterface.list()).map((networkInterface) => networkInterface.addresses).expand((ip) => ip);
|
||||
ip = result.firstWhereOrNull((ip) => ip.type == InternetAddressType.IPv4)?.address;
|
||||
nativeResult = result.where((ip) => ip.type == InternetAddressType.IPv4).map((address) => address.address).toList();
|
||||
} catch (e, st) {
|
||||
print(e);
|
||||
print(st);
|
||||
@@ -52,8 +53,38 @@ class NetworkInfoNotifier extends StateNotifier<NetworkInfo?> {
|
||||
print('New network state: $ip ($mask)');
|
||||
|
||||
return NetworkInfo(
|
||||
localIp: ip,
|
||||
localIps: rankIpAddresses(nativeResult, ip),
|
||||
netMask: mask,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
List<String> rankIpAddresses(List<String> nativeResult, String? thirdPartyResult) {
|
||||
if (thirdPartyResult == null) {
|
||||
// only take the list
|
||||
return nativeResult._rankIpAddresses(null);
|
||||
} else if (nativeResult.isEmpty) {
|
||||
// only take the first IP from third party library
|
||||
return [thirdPartyResult];
|
||||
} else if (thirdPartyResult.endsWith('.1')) {
|
||||
// merge
|
||||
return {thirdPartyResult, ...nativeResult}.toList()._rankIpAddresses(null);
|
||||
} else {
|
||||
// merge but prefer result from third party library
|
||||
return {thirdPartyResult, ...nativeResult}.toList()._rankIpAddresses(thirdPartyResult);
|
||||
}
|
||||
}
|
||||
|
||||
/// Sorts Ip addresses with first being the most likely primary local address
|
||||
/// Currently,
|
||||
/// - sorts ending with ".1" last
|
||||
/// - primary is always first
|
||||
extension ListIpExt on List<String> {
|
||||
List<String> _rankIpAddresses(String? primary) {
|
||||
return sorted((a, b) {
|
||||
int scoreA = a == primary ? 10 : (a.endsWith('.1') ? 0 : 1);
|
||||
int scoreB = b == primary ? 10 : (b.endsWith('.1') ? 0 : 1);
|
||||
return scoreB.compareTo(scoreA);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,19 @@
|
||||
import 'package:collection/collection.dart';
|
||||
|
||||
extension StringIpExt on String {
|
||||
String get visualId => split('.').last;
|
||||
}
|
||||
|
||||
class IpHelper {
|
||||
/// Sorts Ip addresses with first being the most likely primary local address
|
||||
/// Currently,
|
||||
/// - sorts ending with ".1" last
|
||||
/// - primary is always first
|
||||
static List<String> rankIpAddresses(List<String> addresses, String primary) {
|
||||
return addresses.sorted((a, b) {
|
||||
int scoreA = a == primary ? 10 : (a.endsWith('.1') ? 0 : 1);
|
||||
int scoreB = b == primary ? 10 : (b.endsWith('.1') ? 0 : 1);
|
||||
return scoreB.compareTo(scoreA);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ class TaskRunner<T> {
|
||||
|
||||
final int concurrency;
|
||||
int _runnerCount = 0;
|
||||
bool _stopped = false;
|
||||
|
||||
/// If [true], then the stream will be closed as soon as every task has been finished.
|
||||
/// By default, it is [false] when [initialTasks] is provided with a non-empty list.
|
||||
@@ -31,6 +32,10 @@ class TaskRunner<T> {
|
||||
_fireRunners();
|
||||
}
|
||||
|
||||
void stop() {
|
||||
_stopped = true;
|
||||
}
|
||||
|
||||
Stream<T> get stream => _streamController.stream;
|
||||
|
||||
/// Starts multiple runners until [concurrency].
|
||||
@@ -41,7 +46,7 @@ class TaskRunner<T> {
|
||||
_runner(
|
||||
onFinish: () {
|
||||
_runnerCount--;
|
||||
if (_runnerCount == 0 && !_stayAlive) {
|
||||
if (_stopped || (_runnerCount == 0 && !_stayAlive)) {
|
||||
_streamController.close();
|
||||
onFinish?.call();
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
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';
|
||||
@@ -28,15 +29,15 @@ class AddressInputDialog extends ConsumerStatefulWidget {
|
||||
}
|
||||
|
||||
class _AddressInputDialogState extends ConsumerState<AddressInputDialog> {
|
||||
final _addressController = TextEditingController();
|
||||
final _selected = List.generate(_InputMode.values.length, (index) => index == 0);
|
||||
_InputMode _mode = _InputMode.hashtag;
|
||||
String _input = '';
|
||||
bool _fetching = false;
|
||||
bool _failed = false;
|
||||
|
||||
Future<void> _submit(String? ipPrefix, int port) async {
|
||||
final String ip;
|
||||
final String input = _addressController.text.trim();
|
||||
final String input = _input.trim();
|
||||
if (_mode == _InputMode.ip) {
|
||||
ip = input;
|
||||
} else {
|
||||
@@ -62,8 +63,8 @@ class _AddressInputDialogState extends ConsumerState<AddressInputDialog> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final localIp = ref.watch(networkInfoProvider.select((info) => info?.localIp));
|
||||
final ipPrefix = localIp?.split('.').take(3).join('.');
|
||||
final localIps = ref.watch(networkInfoProvider.select((info) => info?.localIps));
|
||||
final ipPrefix = localIps?.firstOrNull?.split('.').take(3).join('.');
|
||||
final settings = ref.watch(settingsProvider);
|
||||
|
||||
return AlertDialog(
|
||||
@@ -83,25 +84,37 @@ class _AddressInputDialogState extends ConsumerState<AddressInputDialog> {
|
||||
});
|
||||
},
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
constraints: const BoxConstraints(minWidth: 0, minHeight: 0),
|
||||
children: _InputMode.values.map((mode) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
|
||||
child: Text(mode.label),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
const SizedBox(height: 15),
|
||||
TextFormField(
|
||||
controller: _addressController,
|
||||
autofocus: true,
|
||||
enabled: !_fetching,
|
||||
decoration: InputDecoration(
|
||||
prefixText: _mode == _InputMode.hashtag ? '# ' : 'IP: ',
|
||||
),
|
||||
onChanged: (s) {
|
||||
setState(() => _input = s);
|
||||
},
|
||||
onFieldSubmitted: (s) => _submit(ipPrefix, settings.port),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Text('${t.general.example}: ${_mode == _InputMode.hashtag ? '123' : '${ipPrefix ?? '192.168.2'}.123'}'),
|
||||
Text(
|
||||
'${t.general.example}: ${_mode == _InputMode.hashtag ? '123' : '${ipPrefix ?? '192.168.2'}.123'}',
|
||||
style: const TextStyle(color: Colors.grey),
|
||||
),
|
||||
if (_mode == _InputMode.hashtag)
|
||||
Text(
|
||||
'${t.dialogs.addressInput.ip}: ${ipPrefix ?? '192.168.2'}.$_input',
|
||||
style: const TextStyle(color: Colors.grey),
|
||||
),
|
||||
if (_failed)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 10),
|
||||
|
||||
@@ -43,6 +43,13 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "5.4.2"
|
||||
boolean_selector:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: boolean_selector
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
build:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -176,6 +183,13 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.1.1"
|
||||
coverage:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: coverage
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.6.1"
|
||||
cross_file:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -611,6 +625,13 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.5.0"
|
||||
node_preamble:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: node_preamble
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.1"
|
||||
open_filex:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -919,6 +940,13 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
shelf_packages_handler:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shelf_packages_handler
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.0.1"
|
||||
shelf_router:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -926,6 +954,13 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.1.3"
|
||||
shelf_static:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shelf_static
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
shelf_web_socket:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -973,6 +1008,20 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.3.3"
|
||||
source_map_stack_trace:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: source_map_stack_trace
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
source_maps:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: source_maps
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.10.11"
|
||||
source_span:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1022,6 +1071,27 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.2.1"
|
||||
test:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: test
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.22.1"
|
||||
test_api:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.4.17"
|
||||
test_core:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_core
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.4.21"
|
||||
time:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1148,6 +1218,13 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.13"
|
||||
vm_service:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vm_service
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "9.4.0"
|
||||
watcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1162,6 +1239,13 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
webkit_inspection_protocol:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: webkit_inspection_protocol
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
wechat_assets_picker:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
||||
@@ -52,6 +52,7 @@ dev_dependencies:
|
||||
json_serializable: 6.5.4
|
||||
msix: 3.7.0
|
||||
slang_build_runner: 3.7.0
|
||||
test: 1.22.1
|
||||
|
||||
flutter:
|
||||
uses-material-design: true
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import 'package:localsend_app/provider/network_info_provider.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
group('rankIpAddresses', () {
|
||||
test('should only sort list if no primary', () {
|
||||
expect(rankIpAddresses(['123.456', '222.1', '321.222'], null), ['123.456', '321.222', '222.1']);
|
||||
});
|
||||
|
||||
test('should only take primary', () {
|
||||
expect(rankIpAddresses([], '123.123'), ['123.123']);
|
||||
});
|
||||
|
||||
test('should sort primary first', () {
|
||||
expect(rankIpAddresses(['123.456', '222.1', '321.222'], '123.123'), ['123.123','123.456', '321.222', '222.1']);
|
||||
});
|
||||
|
||||
test('should sort primary first and remove duplicates', () {
|
||||
expect(rankIpAddresses(['123.456', '123.123', '222.1', '222.1', '321.222'], '123.123'), ['123.123','123.456', '321.222', '222.1']);
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user