mirror of
https://github.com/localsend/localsend.git
synced 2026-08-07 07:14:52 +00:00
feat: add device detail page
This commit is contained in:
@@ -41,6 +41,29 @@ class SignalingChannel extends DeviceChannel with SignalingChannelMappable {
|
||||
const SignalingChannel({required this.signalingServer});
|
||||
}
|
||||
|
||||
/// Whether a [DeviceLog] discovered the device or re-confirmed it.
|
||||
enum DeviceLogKind {
|
||||
discovered,
|
||||
updated,
|
||||
}
|
||||
|
||||
/// One retained confirmation of a stored device, for the device details UI.
|
||||
class DeviceLog {
|
||||
/// When the confirmation happened.
|
||||
final DateTime timestamp;
|
||||
|
||||
final DeviceLogKind kind;
|
||||
|
||||
/// The channel the confirmation happened on.
|
||||
final HttpChannel channel;
|
||||
|
||||
const DeviceLog({
|
||||
required this.timestamp,
|
||||
required this.kind,
|
||||
required this.channel,
|
||||
});
|
||||
}
|
||||
|
||||
enum TransmissionMethod {
|
||||
http('HTTP'),
|
||||
webrtc('WebRTC')
|
||||
|
||||
@@ -6,6 +6,29 @@ import 'package:typed_isolates/typed_isolates.dart';
|
||||
|
||||
sealed class DiscoveryTask {}
|
||||
|
||||
/// A result sent from the discovery isolate to the main isolate,
|
||||
/// routed to the task that requested it.
|
||||
sealed class DiscoveryResult {}
|
||||
|
||||
/// A confirmed device, emitted on the [DiscoveryListenTask] stream.
|
||||
class DiscoveryDeviceResult implements DiscoveryResult {
|
||||
final Device device;
|
||||
|
||||
DiscoveryDeviceResult({
|
||||
required this.device,
|
||||
});
|
||||
}
|
||||
|
||||
/// The retained confirmations of one stored device, answering a
|
||||
/// [DiscoveryDeviceLogsTask].
|
||||
class DiscoveryDeviceLogsResult implements DiscoveryResult {
|
||||
final List<DeviceLog> logs;
|
||||
|
||||
DiscoveryDeviceLogsResult({
|
||||
required this.logs,
|
||||
});
|
||||
}
|
||||
|
||||
/// Starts the discovery and streams every confirmed device.
|
||||
/// The stream never completes; it survives [DiscoveryRestartTask]s.
|
||||
class DiscoveryListenTask implements DiscoveryTask {}
|
||||
@@ -56,9 +79,20 @@ class DiscoveryAddDeviceTask implements DiscoveryTask {
|
||||
});
|
||||
}
|
||||
|
||||
/// Fetches the retained confirmations of a stored device, oldest first.
|
||||
/// Answered with one [DiscoveryDeviceLogsResult]; the logs are empty when
|
||||
/// the fingerprint is unknown.
|
||||
class DiscoveryDeviceLogsTask implements DiscoveryTask {
|
||||
final String fingerprint;
|
||||
|
||||
DiscoveryDeviceLogsTask({
|
||||
required this.fingerprint,
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> setupDiscoveryIsolate(
|
||||
Stream<SendToIsolateData<IsolateTask<DiscoveryTask>>> receiveFromMain,
|
||||
void Function(IsolateTaskStreamResult<Device>) sendToMain,
|
||||
void Function(IsolateTaskStreamResult<DiscoveryResult>) sendToMain,
|
||||
InitialData initialData,
|
||||
) async {
|
||||
await setupChildIsolateHelper(
|
||||
@@ -73,7 +107,7 @@ Future<void> setupDiscoveryIsolate(
|
||||
sendToMain(
|
||||
IsolateTaskStreamResult.event(
|
||||
id: task.id,
|
||||
data: device,
|
||||
data: DiscoveryDeviceResult(device: device),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -104,6 +138,15 @@ Future<void> setupDiscoveryIsolate(
|
||||
case DiscoveryAddDeviceTask data:
|
||||
await ref.read(discoveryProvider).addDevice(data.device);
|
||||
break;
|
||||
case DiscoveryDeviceLogsTask data:
|
||||
final logs = await ref.read(discoveryProvider).deviceLogs(data.fingerprint);
|
||||
sendToMain(
|
||||
IsolateTaskStreamResult.event(
|
||||
id: task.id,
|
||||
data: DiscoveryDeviceLogsResult(logs: logs),
|
||||
),
|
||||
);
|
||||
break;
|
||||
}
|
||||
sendToMain(
|
||||
IsolateTaskStreamResult.done(
|
||||
|
||||
@@ -25,9 +25,11 @@ class IsolateDiscoveryListenAction extends ReduxActionWithResult<IsolateControll
|
||||
|
||||
return (
|
||||
state,
|
||||
connection.sendWrappedTaskAndListenStream(
|
||||
task: DiscoveryListenTask(),
|
||||
),
|
||||
connection
|
||||
.sendWrappedTaskAndListenStream(
|
||||
task: DiscoveryListenTask(),
|
||||
)
|
||||
.toDeviceStream(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -56,13 +58,15 @@ class IsolateDiscoverySubnetScanAction extends ReduxActionWithResult<IsolateCont
|
||||
|
||||
return (
|
||||
state,
|
||||
connection.sendWrappedTaskAndListenStream(
|
||||
task: DiscoverySubnetScanTask(
|
||||
networkInterface: networkInterface,
|
||||
port: port,
|
||||
https: https,
|
||||
),
|
||||
),
|
||||
connection
|
||||
.sendWrappedTaskAndListenStream(
|
||||
task: DiscoverySubnetScanTask(
|
||||
networkInterface: networkInterface,
|
||||
port: port,
|
||||
https: https,
|
||||
),
|
||||
)
|
||||
.toDeviceStream(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -88,16 +92,45 @@ class IsolateDiscoveryFavoriteScanAction extends ReduxActionWithResult<IsolateCo
|
||||
|
||||
return (
|
||||
state,
|
||||
connection.sendWrappedTaskAndListenStream(
|
||||
task: DiscoveryFavoriteScanTask(
|
||||
favorites: favorites,
|
||||
https: https,
|
||||
),
|
||||
),
|
||||
connection
|
||||
.sendWrappedTaskAndListenStream(
|
||||
task: DiscoveryFavoriteScanTask(
|
||||
favorites: favorites,
|
||||
https: https,
|
||||
),
|
||||
)
|
||||
.toDeviceStream(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetches the retained confirmations of a stored device, oldest first.
|
||||
/// The logs are empty when the fingerprint is unknown or the discovery is
|
||||
/// not running.
|
||||
class IsolateDiscoveryDeviceLogsAction extends AsyncReduxActionWithResult<IsolateController, ParentIsolateState, List<DeviceLog>> {
|
||||
final String fingerprint;
|
||||
|
||||
IsolateDiscoveryDeviceLogsAction({
|
||||
required this.fingerprint,
|
||||
});
|
||||
|
||||
@override
|
||||
Future<(ParentIsolateState, List<DeviceLog>)> reduce() async {
|
||||
final connection = state.discovery;
|
||||
if (connection == null) {
|
||||
throw StateError('discovery is not initialized');
|
||||
}
|
||||
|
||||
final result = await connection
|
||||
.sendWrappedTaskAndListenStream(
|
||||
task: DiscoveryDeviceLogsTask(fingerprint: fingerprint),
|
||||
)
|
||||
.first;
|
||||
|
||||
return (state, (result as DiscoveryDeviceLogsResult).logs);
|
||||
}
|
||||
}
|
||||
|
||||
/// Sends an announcement which makes every other LocalSend device on the
|
||||
/// network register with this device's HTTP server.
|
||||
class IsolateDiscoveryAnnouncementAction extends ReduxAction<IsolateController, ParentIsolateState> {
|
||||
@@ -496,6 +529,13 @@ class IsolateHttpServerFailFileDownloadAction extends ReduxAction<IsolateControl
|
||||
}
|
||||
}
|
||||
|
||||
extension _DeviceStreamExt on Stream<DiscoveryResult> {
|
||||
/// Unwraps the [DiscoveryDeviceResult]s of a device stream.
|
||||
Stream<Device> toDeviceStream() {
|
||||
return map((result) => (result as DiscoveryDeviceResult).device);
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds the [SendToIsolateData] envelope on top of the generic
|
||||
/// [IsolateTaskConnector.sendTaskAndListenStream] from `typed_isolates`.
|
||||
extension _WrappedTaskConnector<R, T> on IsolateConnector<IsolateTaskStreamResult<R>, SendToIsolateData<IsolateTask<T>>> {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import 'package:dart_mappable/dart_mappable.dart';
|
||||
import 'package:localsend_isolates/model/device.dart';
|
||||
import 'package:localsend_isolates/src/isolate/child/discovery_isolate.dart';
|
||||
import 'package:localsend_isolates/src/isolate/child/main.dart';
|
||||
import 'package:localsend_isolates/src/isolate/child/server_isolate.dart';
|
||||
@@ -18,7 +17,7 @@ part 'parent_isolate_provider.mapper.dart';
|
||||
@MappableClass()
|
||||
class ParentIsolateState with ParentIsolateStateMappable {
|
||||
final SyncState syncState;
|
||||
final IsolateConnector<IsolateTaskStreamResult<Device>, SendToIsolateData<IsolateTask<DiscoveryTask>>>? discovery;
|
||||
final IsolateConnector<IsolateTaskStreamResult<DiscoveryResult>, SendToIsolateData<IsolateTask<DiscoveryTask>>>? discovery;
|
||||
final IsolateConnector<IsolateTaskStreamResult<HttpUploadEvent>, SendToIsolateData<IsolateTask<BaseHttpUploadTask>>>? httpUpload;
|
||||
final IsolateConnector<IsolateTaskStreamResult<HttpServerEvent>, SendToIsolateData<IsolateTask<BaseHttpServerTask>>>? httpServer;
|
||||
|
||||
@@ -62,13 +61,14 @@ class IsolateController extends ReduxNotifier<ParentIsolateState> {
|
||||
class IsolateSetupAction extends AsyncReduxAction<IsolateController, ParentIsolateState> {
|
||||
@override
|
||||
Future<ParentIsolateState> reduce() async {
|
||||
final discovery = await TypedIsolates.startIsolate<IsolateTaskStreamResult<Device>, SendToIsolateData<IsolateTask<DiscoveryTask>>, InitialData>(
|
||||
task: setupDiscoveryIsolate,
|
||||
param: InitialData(
|
||||
syncState: state.syncState,
|
||||
logLevel: Logger.root.level,
|
||||
),
|
||||
);
|
||||
final discovery =
|
||||
await TypedIsolates.startIsolate<IsolateTaskStreamResult<DiscoveryResult>, SendToIsolateData<IsolateTask<DiscoveryTask>>, InitialData>(
|
||||
task: setupDiscoveryIsolate,
|
||||
param: InitialData(
|
||||
syncState: state.syncState,
|
||||
logLevel: Logger.root.level,
|
||||
),
|
||||
);
|
||||
|
||||
final httpUpload =
|
||||
await TypedIsolates.startIsolate<IsolateTaskStreamResult<HttpUploadEvent>, SendToIsolateData<IsolateTask<BaseHttpUploadTask>>, InitialData>(
|
||||
|
||||
+3
-4
@@ -16,7 +16,6 @@ class ParentIsolateStateMapper extends ClassMapperBase<ParentIsolateState> {
|
||||
if (_instance == null) {
|
||||
MapperContainer.globals.use(_instance = ParentIsolateStateMapper._());
|
||||
SyncStateMapper.ensureInitialized();
|
||||
DeviceMapper.ensureInitialized();
|
||||
}
|
||||
return _instance!;
|
||||
}
|
||||
@@ -30,14 +29,14 @@ class ParentIsolateStateMapper extends ClassMapperBase<ParentIsolateState> {
|
||||
_$syncState,
|
||||
);
|
||||
static IsolateConnector<
|
||||
IsolateTaskStreamResult<Device>,
|
||||
IsolateTaskStreamResult<DiscoveryResult>,
|
||||
SendToIsolateData<IsolateTask<DiscoveryTask>>
|
||||
>?
|
||||
_$discovery(ParentIsolateState v) => v.discovery;
|
||||
static const Field<
|
||||
ParentIsolateState,
|
||||
IsolateConnector<
|
||||
IsolateTaskStreamResult<Device>,
|
||||
IsolateTaskStreamResult<DiscoveryResult>,
|
||||
SendToIsolateData<IsolateTask<DiscoveryTask>>
|
||||
>
|
||||
>
|
||||
@@ -161,7 +160,7 @@ abstract class ParentIsolateStateCopyWith<
|
||||
$R call({
|
||||
SyncState? syncState,
|
||||
IsolateConnector<
|
||||
IsolateTaskStreamResult<Device>,
|
||||
IsolateTaskStreamResult<DiscoveryResult>,
|
||||
SendToIsolateData<IsolateTask<DiscoveryTask>>
|
||||
>?
|
||||
discovery,
|
||||
|
||||
@@ -164,6 +164,19 @@ class DiscoveryService {
|
||||
]);
|
||||
}
|
||||
|
||||
/// The retained confirmations of a stored device, oldest first.
|
||||
/// Empty when the fingerprint is unknown or the discovery is not running.
|
||||
Future<List<DeviceLog>> deviceLogs(String fingerprint) async {
|
||||
final discovery = _discovery;
|
||||
if (discovery == null) {
|
||||
_logger.info('Discovery is not running, skipping device logs');
|
||||
return const [];
|
||||
}
|
||||
|
||||
final logs = await discovery.deviceLogs(fingerprint: fingerprint);
|
||||
return logs.map((log) => log.toDeviceLog()).toList();
|
||||
}
|
||||
|
||||
/// Feeds a device confirmed outside of the discovery into the store, e.g.
|
||||
/// one that registered with this device's HTTP server.
|
||||
/// The device comes back on the [startListener] stream.
|
||||
|
||||
@@ -159,6 +159,23 @@ extension RsStoredDeviceExt on rust_discovery.RsStoredDevice {
|
||||
}
|
||||
}
|
||||
|
||||
extension RsDeviceLogExt on rust_discovery.RsDeviceLog {
|
||||
DeviceLog toDeviceLog() {
|
||||
return DeviceLog(
|
||||
timestamp: DateTime.fromMillisecondsSinceEpoch(timestampMillis.toInt()),
|
||||
kind: switch (kind) {
|
||||
rust_discovery.DeviceLogKind.discovered => DeviceLogKind.discovered,
|
||||
rust_discovery.DeviceLogKind.updated => DeviceLogKind.updated,
|
||||
},
|
||||
channel: HttpChannel(
|
||||
host: channel.host,
|
||||
port: channel.port,
|
||||
https: channel.protocol == rust_model.ProtocolType.https,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
extension DeviceToRsDiscoveredDeviceExt on Device {
|
||||
rust_discovery.RsDiscoveredDevice toRsDiscoveredDevice(String ip) {
|
||||
return rust_discovery.RsDiscoveredDevice(
|
||||
|
||||
Reference in New Issue
Block a user