mirror of
https://github.com/localsend/localsend.git
synced 2026-08-07 07:14:52 +00:00
feat: add web receive
This commit is contained in:
@@ -108,7 +108,7 @@ Save targets are decided in Dart (`prepareFileSaveTarget`) and written by Rust:
|
|||||||
|
|
||||||
Server event `ip`s are `PeerIp` (IP + IPv6 scope): a link-local peer renders as `fe80::1%3`, which the HTTP client accepts back as a host, so event ips stay dialable.
|
Server event `ip`s are `PeerIp` (IP + IPv6 scope): a link-local peer renders as `fe80::1%3`, which the HTTP client accepts back as a host, so event ips stay dialable.
|
||||||
|
|
||||||
TLS uses per-device on-the-fly certificates with **mandatory client certificates**; the peer identity is the uppercase-hex SHA-256 of the client cert DER, and `Register` is simply not emitted when a payload's claimed fingerprint disagrees with the cert. Prefer `event.certFingerprint ?? event.info.fingerprint` — the payload fallback only exists for encryption-off mode.
|
TLS uses per-device on-the-fly certificates with **mandatory client certificates** (optional while the web pages are served, so browsers can connect); the peer identity is the uppercase-hex SHA-256 of the client cert DER, and `Register` is simply not emitted when a payload's claimed fingerprint disagrees with the cert. Prefer `event.certFingerprint ?? event.info.fingerprint` — the payload fallback only exists for encryption-off mode.
|
||||||
|
|
||||||
Both the receive pin and the web-send pin are fixed at server start, so changing either restarts the server.
|
Both the receive pin and the web-send pin are fixed at server start, so changing either restarts the server.
|
||||||
|
|
||||||
|
|||||||
@@ -268,6 +268,9 @@
|
|||||||
"encryptionHint": "LocalSend uses a self-signed certificate. You need to accept it in your browser.",
|
"encryptionHint": "LocalSend uses a self-signed certificate. You need to accept it in your browser.",
|
||||||
"pendingRequests": "Pending requests: {n}"
|
"pendingRequests": "Pending requests: {n}"
|
||||||
},
|
},
|
||||||
|
"webReceivePage": {
|
||||||
|
"title": "Receive via link"
|
||||||
|
},
|
||||||
"aboutPage": {
|
"aboutPage": {
|
||||||
"title": "About LocalSend",
|
"title": "About LocalSend",
|
||||||
"description": [
|
"description": [
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
/// To regenerate, run: `dart run slang`
|
/// To regenerate, run: `dart run slang`
|
||||||
///
|
///
|
||||||
/// Locales: 55
|
/// Locales: 55
|
||||||
/// Strings: 18450 (335 per locale)
|
/// Strings: 18451 (335 per locale)
|
||||||
|
|
||||||
// coverage:ignore-file
|
// coverage:ignore-file
|
||||||
// ignore_for_file: type=lint, unused_import
|
// ignore_for_file: type=lint, unused_import
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ class Translations with BaseTranslations<AppLocale, Translations> {
|
|||||||
late final Translations$sendPage$en sendPage = Translations$sendPage$en.internal(_root);
|
late final Translations$sendPage$en sendPage = Translations$sendPage$en.internal(_root);
|
||||||
late final Translations$progressPage$en progressPage = Translations$progressPage$en.internal(_root);
|
late final Translations$progressPage$en progressPage = Translations$progressPage$en.internal(_root);
|
||||||
late final Translations$webSharePage$en webSharePage = Translations$webSharePage$en.internal(_root);
|
late final Translations$webSharePage$en webSharePage = Translations$webSharePage$en.internal(_root);
|
||||||
|
late final Translations$webReceivePage$en webReceivePage = Translations$webReceivePage$en.internal(_root);
|
||||||
late final Translations$aboutPage$en aboutPage = Translations$aboutPage$en.internal(_root);
|
late final Translations$aboutPage$en aboutPage = Translations$aboutPage$en.internal(_root);
|
||||||
late final Translations$donationPage$en donationPage = Translations$donationPage$en.internal(_root);
|
late final Translations$donationPage$en donationPage = Translations$donationPage$en.internal(_root);
|
||||||
late final Translations$changelogPage$en changelogPage = Translations$changelogPage$en.internal(_root);
|
late final Translations$changelogPage$en changelogPage = Translations$changelogPage$en.internal(_root);
|
||||||
@@ -530,6 +531,18 @@ class Translations$webSharePage$en {
|
|||||||
String pendingRequests({required Object n}) => 'Pending requests: ${n}';
|
String pendingRequests({required Object n}) => 'Pending requests: ${n}';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Path: webReceivePage
|
||||||
|
class Translations$webReceivePage$en {
|
||||||
|
Translations$webReceivePage$en.internal(this._root);
|
||||||
|
|
||||||
|
final Translations _root; // ignore: unused_field
|
||||||
|
|
||||||
|
// Translations
|
||||||
|
|
||||||
|
/// en: 'Receive via link'
|
||||||
|
String get title => 'Receive via link';
|
||||||
|
}
|
||||||
|
|
||||||
// Path: aboutPage
|
// Path: aboutPage
|
||||||
class Translations$aboutPage$en {
|
class Translations$aboutPage$en {
|
||||||
Translations$aboutPage$en.internal(this._root);
|
Translations$aboutPage$en.internal(this._root);
|
||||||
|
|||||||
@@ -12,16 +12,20 @@ class ServerState with ServerStateMappable {
|
|||||||
final ReceiveSessionState? session;
|
final ReceiveSessionState? session;
|
||||||
final WebSendState? webSendState;
|
final WebSendState? webSendState;
|
||||||
|
|
||||||
|
/// Whether the upload page is served so web browsers can upload files.
|
||||||
|
final bool webUpload;
|
||||||
|
|
||||||
const ServerState({
|
const ServerState({
|
||||||
required this.alias,
|
required this.alias,
|
||||||
required this.port,
|
required this.port,
|
||||||
required this.https,
|
required this.https,
|
||||||
required this.session,
|
required this.session,
|
||||||
required this.webSendState,
|
required this.webSendState,
|
||||||
|
required this.webUpload,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'ServerState(alias: $alias, port: $port, https: $https, session: $session, webSendState: $webSendState)';
|
return 'ServerState(alias: $alias, port: $port, https: $https, session: $session, webSendState: $webSendState, webUpload: $webUpload)';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,6 +40,11 @@ class ServerStateMapper extends ClassMapperBase<ServerState> {
|
|||||||
'webSendState',
|
'webSendState',
|
||||||
_$webSendState,
|
_$webSendState,
|
||||||
);
|
);
|
||||||
|
static bool _$webUpload(ServerState v) => v.webUpload;
|
||||||
|
static const Field<ServerState, bool> _f$webUpload = Field(
|
||||||
|
'webUpload',
|
||||||
|
_$webUpload,
|
||||||
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
final MappableFields<ServerState> fields = const {
|
final MappableFields<ServerState> fields = const {
|
||||||
@@ -48,6 +53,7 @@ class ServerStateMapper extends ClassMapperBase<ServerState> {
|
|||||||
#https: _f$https,
|
#https: _f$https,
|
||||||
#session: _f$session,
|
#session: _f$session,
|
||||||
#webSendState: _f$webSendState,
|
#webSendState: _f$webSendState,
|
||||||
|
#webUpload: _f$webUpload,
|
||||||
};
|
};
|
||||||
|
|
||||||
static ServerState _instantiate(DecodingData data) {
|
static ServerState _instantiate(DecodingData data) {
|
||||||
@@ -57,6 +63,7 @@ class ServerStateMapper extends ClassMapperBase<ServerState> {
|
|||||||
https: data.dec(_f$https),
|
https: data.dec(_f$https),
|
||||||
session: data.dec(_f$session),
|
session: data.dec(_f$session),
|
||||||
webSendState: data.dec(_f$webSendState),
|
webSendState: data.dec(_f$webSendState),
|
||||||
|
webUpload: data.dec(_f$webUpload),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,6 +136,7 @@ abstract class ServerStateCopyWith<$R, $In extends ServerState, $Out>
|
|||||||
bool? https,
|
bool? https,
|
||||||
ReceiveSessionState? session,
|
ReceiveSessionState? session,
|
||||||
WebSendState? webSendState,
|
WebSendState? webSendState,
|
||||||
|
bool? webUpload,
|
||||||
});
|
});
|
||||||
ServerStateCopyWith<$R2, $In, $Out2> $chain<$R2, $Out2>(Then<$Out2, $R2> t);
|
ServerStateCopyWith<$R2, $In, $Out2> $chain<$R2, $Out2>(Then<$Out2, $R2> t);
|
||||||
}
|
}
|
||||||
@@ -154,6 +162,7 @@ class _ServerStateCopyWithImpl<$R, $Out>
|
|||||||
bool? https,
|
bool? https,
|
||||||
Object? session = $none,
|
Object? session = $none,
|
||||||
Object? webSendState = $none,
|
Object? webSendState = $none,
|
||||||
|
bool? webUpload,
|
||||||
}) => $apply(
|
}) => $apply(
|
||||||
FieldCopyWithData({
|
FieldCopyWithData({
|
||||||
if (alias != null) #alias: alias,
|
if (alias != null) #alias: alias,
|
||||||
@@ -161,6 +170,7 @@ class _ServerStateCopyWithImpl<$R, $Out>
|
|||||||
if (https != null) #https: https,
|
if (https != null) #https: https,
|
||||||
if (session != $none) #session: session,
|
if (session != $none) #session: session,
|
||||||
if (webSendState != $none) #webSendState: webSendState,
|
if (webSendState != $none) #webSendState: webSendState,
|
||||||
|
if (webUpload != null) #webUpload: webUpload,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@override
|
@override
|
||||||
@@ -170,6 +180,7 @@ class _ServerStateCopyWithImpl<$R, $Out>
|
|||||||
https: data.get(#https, or: $value.https),
|
https: data.get(#https, or: $value.https),
|
||||||
session: data.get(#session, or: $value.session),
|
session: data.get(#session, or: $value.session),
|
||||||
webSendState: data.get(#webSendState, or: $value.webSendState),
|
webSendState: data.get(#webSendState, or: $value.webSendState),
|
||||||
|
webUpload: data.get(#webUpload, or: $value.webUpload),
|
||||||
);
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ class SettingsState with SettingsStateMappable {
|
|||||||
final DeviceType? deviceType;
|
final DeviceType? deviceType;
|
||||||
final String? deviceModel;
|
final String? deviceModel;
|
||||||
final bool shareViaLinkAutoAccept;
|
final bool shareViaLinkAutoAccept;
|
||||||
|
final bool receiveViaLinkAutoAccept;
|
||||||
final int discoveryTimeout;
|
final int discoveryTimeout;
|
||||||
final bool advancedSettings;
|
final bool advancedSettings;
|
||||||
|
|
||||||
@@ -61,6 +62,7 @@ class SettingsState with SettingsStateMappable {
|
|||||||
required this.deviceType,
|
required this.deviceType,
|
||||||
required this.deviceModel,
|
required this.deviceModel,
|
||||||
required this.shareViaLinkAutoAccept,
|
required this.shareViaLinkAutoAccept,
|
||||||
|
required this.receiveViaLinkAutoAccept,
|
||||||
required this.discoveryTimeout,
|
required this.discoveryTimeout,
|
||||||
required this.advancedSettings,
|
required this.advancedSettings,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -138,6 +138,12 @@ class SettingsStateMapper extends ClassMapperBase<SettingsState> {
|
|||||||
'shareViaLinkAutoAccept',
|
'shareViaLinkAutoAccept',
|
||||||
_$shareViaLinkAutoAccept,
|
_$shareViaLinkAutoAccept,
|
||||||
);
|
);
|
||||||
|
static bool _$receiveViaLinkAutoAccept(SettingsState v) =>
|
||||||
|
v.receiveViaLinkAutoAccept;
|
||||||
|
static const Field<SettingsState, bool> _f$receiveViaLinkAutoAccept = Field(
|
||||||
|
'receiveViaLinkAutoAccept',
|
||||||
|
_$receiveViaLinkAutoAccept,
|
||||||
|
);
|
||||||
static int _$discoveryTimeout(SettingsState v) => v.discoveryTimeout;
|
static int _$discoveryTimeout(SettingsState v) => v.discoveryTimeout;
|
||||||
static const Field<SettingsState, int> _f$discoveryTimeout = Field(
|
static const Field<SettingsState, int> _f$discoveryTimeout = Field(
|
||||||
'discoveryTimeout',
|
'discoveryTimeout',
|
||||||
@@ -175,6 +181,7 @@ class SettingsStateMapper extends ClassMapperBase<SettingsState> {
|
|||||||
#deviceType: _f$deviceType,
|
#deviceType: _f$deviceType,
|
||||||
#deviceModel: _f$deviceModel,
|
#deviceModel: _f$deviceModel,
|
||||||
#shareViaLinkAutoAccept: _f$shareViaLinkAutoAccept,
|
#shareViaLinkAutoAccept: _f$shareViaLinkAutoAccept,
|
||||||
|
#receiveViaLinkAutoAccept: _f$receiveViaLinkAutoAccept,
|
||||||
#discoveryTimeout: _f$discoveryTimeout,
|
#discoveryTimeout: _f$discoveryTimeout,
|
||||||
#advancedSettings: _f$advancedSettings,
|
#advancedSettings: _f$advancedSettings,
|
||||||
};
|
};
|
||||||
@@ -205,6 +212,7 @@ class SettingsStateMapper extends ClassMapperBase<SettingsState> {
|
|||||||
deviceType: data.dec(_f$deviceType),
|
deviceType: data.dec(_f$deviceType),
|
||||||
deviceModel: data.dec(_f$deviceModel),
|
deviceModel: data.dec(_f$deviceModel),
|
||||||
shareViaLinkAutoAccept: data.dec(_f$shareViaLinkAutoAccept),
|
shareViaLinkAutoAccept: data.dec(_f$shareViaLinkAutoAccept),
|
||||||
|
receiveViaLinkAutoAccept: data.dec(_f$receiveViaLinkAutoAccept),
|
||||||
discoveryTimeout: data.dec(_f$discoveryTimeout),
|
discoveryTimeout: data.dec(_f$discoveryTimeout),
|
||||||
advancedSettings: data.dec(_f$advancedSettings),
|
advancedSettings: data.dec(_f$advancedSettings),
|
||||||
);
|
);
|
||||||
@@ -301,6 +309,7 @@ abstract class SettingsStateCopyWith<$R, $In extends SettingsState, $Out>
|
|||||||
DeviceType? deviceType,
|
DeviceType? deviceType,
|
||||||
String? deviceModel,
|
String? deviceModel,
|
||||||
bool? shareViaLinkAutoAccept,
|
bool? shareViaLinkAutoAccept,
|
||||||
|
bool? receiveViaLinkAutoAccept,
|
||||||
int? discoveryTimeout,
|
int? discoveryTimeout,
|
||||||
bool? advancedSettings,
|
bool? advancedSettings,
|
||||||
});
|
});
|
||||||
@@ -359,6 +368,7 @@ class _SettingsStateCopyWithImpl<$R, $Out>
|
|||||||
Object? deviceType = $none,
|
Object? deviceType = $none,
|
||||||
Object? deviceModel = $none,
|
Object? deviceModel = $none,
|
||||||
bool? shareViaLinkAutoAccept,
|
bool? shareViaLinkAutoAccept,
|
||||||
|
bool? receiveViaLinkAutoAccept,
|
||||||
int? discoveryTimeout,
|
int? discoveryTimeout,
|
||||||
bool? advancedSettings,
|
bool? advancedSettings,
|
||||||
}) => $apply(
|
}) => $apply(
|
||||||
@@ -390,6 +400,8 @@ class _SettingsStateCopyWithImpl<$R, $Out>
|
|||||||
if (deviceModel != $none) #deviceModel: deviceModel,
|
if (deviceModel != $none) #deviceModel: deviceModel,
|
||||||
if (shareViaLinkAutoAccept != null)
|
if (shareViaLinkAutoAccept != null)
|
||||||
#shareViaLinkAutoAccept: shareViaLinkAutoAccept,
|
#shareViaLinkAutoAccept: shareViaLinkAutoAccept,
|
||||||
|
if (receiveViaLinkAutoAccept != null)
|
||||||
|
#receiveViaLinkAutoAccept: receiveViaLinkAutoAccept,
|
||||||
if (discoveryTimeout != null) #discoveryTimeout: discoveryTimeout,
|
if (discoveryTimeout != null) #discoveryTimeout: discoveryTimeout,
|
||||||
if (advancedSettings != null) #advancedSettings: advancedSettings,
|
if (advancedSettings != null) #advancedSettings: advancedSettings,
|
||||||
}),
|
}),
|
||||||
@@ -429,6 +441,10 @@ class _SettingsStateCopyWithImpl<$R, $Out>
|
|||||||
#shareViaLinkAutoAccept,
|
#shareViaLinkAutoAccept,
|
||||||
or: $value.shareViaLinkAutoAccept,
|
or: $value.shareViaLinkAutoAccept,
|
||||||
),
|
),
|
||||||
|
receiveViaLinkAutoAccept: data.get(
|
||||||
|
#receiveViaLinkAutoAccept,
|
||||||
|
or: $value.receiveViaLinkAutoAccept,
|
||||||
|
),
|
||||||
discoveryTimeout: data.get(#discoveryTimeout, or: $value.discoveryTimeout),
|
discoveryTimeout: data.get(#discoveryTimeout, or: $value.discoveryTimeout),
|
||||||
advancedSettings: data.get(#advancedSettings, or: $value.advancedSettings),
|
advancedSettings: data.get(#advancedSettings, or: $value.advancedSettings),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:localsend_app/config/theme.dart';
|
import 'package:localsend_app/config/theme.dart';
|
||||||
import 'package:localsend_app/gen/strings.g.dart';
|
import 'package:localsend_app/gen/strings.g.dart';
|
||||||
import 'package:localsend_app/model/state/server/receive_session_state.dart';
|
import 'package:localsend_app/model/state/server/receive_session_state.dart';
|
||||||
|
import 'package:localsend_app/pages/web_receive_page.dart';
|
||||||
import 'package:localsend_app/provider/network/send_provider.dart';
|
import 'package:localsend_app/provider/network/send_provider.dart';
|
||||||
import 'package:localsend_app/provider/network/server/server_provider.dart';
|
import 'package:localsend_app/provider/network/server/server_provider.dart';
|
||||||
import 'package:localsend_app/provider/progress_provider.dart';
|
import 'package:localsend_app/provider/progress_provider.dart';
|
||||||
@@ -25,6 +26,7 @@ import 'package:localsend_isolates/model/file_status.dart';
|
|||||||
import 'package:localsend_isolates/model/session_status.dart';
|
import 'package:localsend_isolates/model/session_status.dart';
|
||||||
import 'package:localsend_isolates/util/file_size_helper.dart';
|
import 'package:localsend_isolates/util/file_size_helper.dart';
|
||||||
import 'package:localsend_isolates/util/file_speed_helper.dart';
|
import 'package:localsend_isolates/util/file_speed_helper.dart';
|
||||||
|
import 'package:refena_flutter/addons.dart';
|
||||||
import 'package:refena_flutter/refena_flutter.dart';
|
import 'package:refena_flutter/refena_flutter.dart';
|
||||||
import 'package:routerino/routerino.dart';
|
import 'package:routerino/routerino.dart';
|
||||||
import 'package:wakelock_plus/wakelock_plus.dart';
|
import 'package:wakelock_plus/wakelock_plus.dart';
|
||||||
@@ -140,8 +142,11 @@ class _ProgressPageState extends State<ProgressPage> with Refena {
|
|||||||
final result = status == null || keepSession || await _askCancelConfirmation(status);
|
final result = status == null || keepSession || await _askCancelConfirmation(status);
|
||||||
|
|
||||||
if (result && mounted) {
|
if (result && mounted) {
|
||||||
// ignore: unawaited_futures
|
if (ref.read(serverProvider)?.webUpload == true) {
|
||||||
context.popUntilRoot();
|
context.global.dispatch(NavigateAction.popUntil<WebReceivePage>());
|
||||||
|
} else {
|
||||||
|
context.global.dispatch(NavigateAction.popUntilRoot());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import 'package:localsend_app/pages/home_page.dart';
|
|||||||
import 'package:localsend_app/pages/home_page_controller.dart';
|
import 'package:localsend_app/pages/home_page_controller.dart';
|
||||||
import 'package:localsend_app/pages/receive_history_page.dart';
|
import 'package:localsend_app/pages/receive_history_page.dart';
|
||||||
import 'package:localsend_app/pages/tabs/receive_tab_vm.dart';
|
import 'package:localsend_app/pages/tabs/receive_tab_vm.dart';
|
||||||
|
import 'package:localsend_app/pages/web_receive_page.dart';
|
||||||
import 'package:localsend_app/provider/animation_provider.dart';
|
import 'package:localsend_app/provider/animation_provider.dart';
|
||||||
import 'package:localsend_app/util/ip_helper.dart';
|
import 'package:localsend_app/util/ip_helper.dart';
|
||||||
import 'package:localsend_app/widget/animations/initial_fade_transition.dart';
|
import 'package:localsend_app/widget/animations/initial_fade_transition.dart';
|
||||||
@@ -12,15 +13,10 @@ import 'package:localsend_app/widget/custom_icon_button.dart';
|
|||||||
import 'package:localsend_app/widget/local_send_logo.dart';
|
import 'package:localsend_app/widget/local_send_logo.dart';
|
||||||
import 'package:localsend_app/widget/responsive_list_view.dart';
|
import 'package:localsend_app/widget/responsive_list_view.dart';
|
||||||
import 'package:localsend_app/widget/rotating_widget.dart';
|
import 'package:localsend_app/widget/rotating_widget.dart';
|
||||||
|
import 'package:refena_flutter/addons.dart';
|
||||||
import 'package:refena_flutter/refena_flutter.dart';
|
import 'package:refena_flutter/refena_flutter.dart';
|
||||||
import 'package:routerino/routerino.dart';
|
import 'package:routerino/routerino.dart';
|
||||||
|
|
||||||
enum _QuickSaveMode {
|
|
||||||
off,
|
|
||||||
favorites,
|
|
||||||
on,
|
|
||||||
}
|
|
||||||
|
|
||||||
class ReceiveTab extends StatelessWidget {
|
class ReceiveTab extends StatelessWidget {
|
||||||
const ReceiveTab();
|
const ReceiveTab();
|
||||||
|
|
||||||
@@ -76,53 +72,12 @@ class ReceiveTab extends StatelessWidget {
|
|||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(top: 10),
|
padding: const EdgeInsets.only(top: 10),
|
||||||
child: Center(
|
child: Center(
|
||||||
child: Column(
|
child: OutlinedButton.icon(
|
||||||
children: [
|
onPressed: () async {
|
||||||
Text(t.general.quickSave),
|
await context.global.dispatchAsync(NavigateAction.push(WebReceivePage()));
|
||||||
const SizedBox(height: 10),
|
|
||||||
SegmentedButton<_QuickSaveMode>(
|
|
||||||
multiSelectionEnabled: false,
|
|
||||||
emptySelectionAllowed: false,
|
|
||||||
showSelectedIcon: false,
|
|
||||||
onSelectionChanged: (selection) async {
|
|
||||||
if (selection.contains(_QuickSaveMode.off)) {
|
|
||||||
await vm.onSetQuickSave(context, false);
|
|
||||||
if (context.mounted) {
|
|
||||||
await vm.onSetQuickSaveFromFavorites(context, false);
|
|
||||||
}
|
|
||||||
} else if (selection.contains(_QuickSaveMode.favorites)) {
|
|
||||||
await vm.onSetQuickSave(context, false);
|
|
||||||
if (context.mounted) {
|
|
||||||
await vm.onSetQuickSaveFromFavorites(context, true);
|
|
||||||
}
|
|
||||||
} else if (selection.contains(_QuickSaveMode.on)) {
|
|
||||||
await vm.onSetQuickSaveFromFavorites(context, false);
|
|
||||||
if (context.mounted) {
|
|
||||||
await vm.onSetQuickSave(context, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
selected: {
|
icon: Icon(Icons.language),
|
||||||
if (!vm.quickSaveSettings && !vm.quickSaveFromFavoritesSettings) _QuickSaveMode.off,
|
label: Text(t.$wip.receiveTab.link('Receive via link')),
|
||||||
if (vm.quickSaveFromFavoritesSettings) _QuickSaveMode.favorites,
|
|
||||||
if (vm.quickSaveSettings) _QuickSaveMode.on,
|
|
||||||
},
|
|
||||||
segments: [
|
|
||||||
ButtonSegment(
|
|
||||||
value: _QuickSaveMode.off,
|
|
||||||
label: Text(t.receiveTab.quickSave.off),
|
|
||||||
),
|
|
||||||
ButtonSegment(
|
|
||||||
value: _QuickSaveMode.favorites,
|
|
||||||
label: Text(t.receiveTab.quickSave.favorites),
|
|
||||||
),
|
|
||||||
ButtonSegment(
|
|
||||||
value: _QuickSaveMode.on,
|
|
||||||
label: Text(t.receiveTab.quickSave.on),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -0,0 +1,252 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:localsend_app/config/theme.dart';
|
||||||
|
import 'package:localsend_app/gen/strings.g.dart';
|
||||||
|
import 'package:localsend_app/provider/local_ip_provider.dart';
|
||||||
|
import 'package:localsend_app/provider/network/server/server_provider.dart';
|
||||||
|
import 'package:localsend_app/provider/settings_provider.dart';
|
||||||
|
import 'package:localsend_app/util/native/platform_check.dart';
|
||||||
|
import 'package:localsend_app/util/ui/snackbar.dart';
|
||||||
|
import 'package:localsend_app/widget/dialogs/qr_dialog.dart';
|
||||||
|
import 'package:localsend_app/widget/dialogs/zoom_dialog.dart';
|
||||||
|
import 'package:localsend_app/widget/responsive_list_view.dart';
|
||||||
|
import 'package:localsend_isolates/util/sleep.dart';
|
||||||
|
import 'package:refena_flutter/refena_flutter.dart';
|
||||||
|
import 'package:routerino/routerino.dart';
|
||||||
|
|
||||||
|
enum _ServerState { initializing, running, error, stopping }
|
||||||
|
|
||||||
|
/// Lets web browsers upload files to this device.
|
||||||
|
/// Incoming requests are not listed here because they open the receive page
|
||||||
|
/// like any other incoming request.
|
||||||
|
class WebReceivePage extends StatefulWidget {
|
||||||
|
const WebReceivePage();
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<WebReceivePage> createState() => _WebReceivePageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _WebReceivePageState extends State<WebReceivePage> with Refena {
|
||||||
|
_ServerState _stateEnum = _ServerState.initializing;
|
||||||
|
bool _encrypted = false;
|
||||||
|
String? _initializedError;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
_init(encrypted: false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _init({required bool encrypted}) async {
|
||||||
|
final settings = ref.read(settingsProvider);
|
||||||
|
setState(() {
|
||||||
|
_stateEnum = _ServerState.initializing;
|
||||||
|
_encrypted = encrypted;
|
||||||
|
_initializedError = null;
|
||||||
|
});
|
||||||
|
await sleepAsync(500);
|
||||||
|
try {
|
||||||
|
await ref
|
||||||
|
.notifier(serverProvider)
|
||||||
|
.restartServer(
|
||||||
|
alias: settings.alias,
|
||||||
|
port: settings.port,
|
||||||
|
https: _encrypted,
|
||||||
|
webUpload: true,
|
||||||
|
);
|
||||||
|
setState(() {
|
||||||
|
_stateEnum = _ServerState.running;
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
if (context.mounted) {
|
||||||
|
setState(() {
|
||||||
|
_stateEnum = _ServerState.error;
|
||||||
|
_initializedError = e.toString();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Web receive uses unencrypted http by default, so we need to revert to the previous state.
|
||||||
|
Future<void> _revertServerState() async {
|
||||||
|
await ref.notifier(serverProvider).restartServerFromSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return PopScope(
|
||||||
|
onPopInvokedWithResult: (_, _) async {
|
||||||
|
if (_stateEnum != _ServerState.running) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_stateEnum = _ServerState.stopping;
|
||||||
|
});
|
||||||
|
await sleepAsync(250);
|
||||||
|
await _revertServerState();
|
||||||
|
await sleepAsync(250);
|
||||||
|
|
||||||
|
if (context.mounted) {
|
||||||
|
context.pop();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
canPop: false,
|
||||||
|
child: Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text(t.webReceivePage.title),
|
||||||
|
),
|
||||||
|
body: Builder(
|
||||||
|
builder: (context) {
|
||||||
|
if (_stateEnum != _ServerState.running) {
|
||||||
|
return Column(
|
||||||
|
mainAxisSize: MainAxisSize.max,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
if (_stateEnum == _ServerState.initializing || _stateEnum == _ServerState.stopping) ...[
|
||||||
|
const CircularProgressIndicator(),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
Center(
|
||||||
|
child: Text(
|
||||||
|
_stateEnum == _ServerState.initializing ? t.webSharePage.loading : t.webSharePage.stopping,
|
||||||
|
style: Theme.of(context).textTheme.titleLarge,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
] else if (_initializedError != null) ...[
|
||||||
|
const Icon(Icons.error_outline, size: 48, color: Colors.red),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
Center(
|
||||||
|
child: Text(t.webSharePage.error, style: Theme.of(context).textTheme.titleLarge),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
Center(
|
||||||
|
child: SelectableText(_initializedError!, style: Theme.of(context).textTheme.bodyMedium),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final serverState = context.watch(serverProvider);
|
||||||
|
if (serverState == null) {
|
||||||
|
// the server is restarting
|
||||||
|
return const Center(child: CircularProgressIndicator());
|
||||||
|
}
|
||||||
|
final networkState = context.watch(localIpProvider);
|
||||||
|
final settings = context.watch(settingsProvider);
|
||||||
|
|
||||||
|
return ResponsiveListView(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 20),
|
||||||
|
children: [
|
||||||
|
Text(t.webSharePage.openLink(n: networkState.localIps.length), style: Theme.of(context).textTheme.titleMedium),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
Card(
|
||||||
|
color: Theme.of(context).colorScheme.secondaryContainer,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(10),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
...networkState.localIps.map((ip) {
|
||||||
|
final url = '${_encrypted ? 'https' : 'http'}://$ip:${serverState.port}';
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.all(5),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
SelectableText(
|
||||||
|
url,
|
||||||
|
style: Theme.of(context).textTheme.bodyMedium,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 5),
|
||||||
|
InkWell(
|
||||||
|
onTap: () async {
|
||||||
|
await Clipboard.setData(ClipboardData(text: url));
|
||||||
|
if (context.mounted && checkPlatformIsDesktop()) {
|
||||||
|
context.showSnackBar(t.general.copiedToClipboard);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: const Padding(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
||||||
|
child: Icon(Icons.content_copy, size: 16),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
InkWell(
|
||||||
|
onTap: () async {
|
||||||
|
await showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (_) => QrDialog(
|
||||||
|
data: url,
|
||||||
|
label: url,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: const Padding(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
||||||
|
child: Icon(Icons.qr_code, size: 16),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
InkWell(
|
||||||
|
onTap: () async {
|
||||||
|
await showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (_) => ZoomDialog(
|
||||||
|
label: url,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: const Padding(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
||||||
|
child: Icon(Icons.tv, size: 16),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Text(t.webSharePage.encryption, style: Theme.of(context).textTheme.titleMedium),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
Checkbox(
|
||||||
|
value: _encrypted,
|
||||||
|
onChanged: (value) {
|
||||||
|
_init(encrypted: value == true);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
if (_encrypted)
|
||||||
|
Text(
|
||||||
|
t.webSharePage.encryptionHint,
|
||||||
|
style: Theme.of(context).textTheme.bodyMedium!.copyWith(color: Theme.of(context).colorScheme.warning),
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Text(t.webSharePage.autoAccept, style: Theme.of(context).textTheme.titleMedium),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
Checkbox(
|
||||||
|
value: settings.receiveViaLinkAutoAccept,
|
||||||
|
onChanged: (value) async {
|
||||||
|
await ref.notifier(settingsProvider).setReceiveViaLinkAutoAccept(value == true);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -128,6 +128,10 @@ class ReceiveController {
|
|||||||
quickSave = true;
|
quickSave = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (server.getState().webUpload && settings.receiveViaLinkAutoAccept && server.getState().session?.message == null) {
|
||||||
|
// The upload page (receive via link) is being served and requests should be accepted automatically.
|
||||||
|
quickSave = true;
|
||||||
|
}
|
||||||
|
|
||||||
if (quickSave) {
|
if (quickSave) {
|
||||||
// accept all files
|
// accept all files
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import 'package:localsend_app/util/alias_generator.dart';
|
|||||||
import 'package:localsend_isolates/constants.dart';
|
import 'package:localsend_isolates/constants.dart';
|
||||||
import 'package:localsend_isolates/isolate.dart';
|
import 'package:localsend_isolates/isolate.dart';
|
||||||
import 'package:localsend_isolates/model/dto/multicast_dto.dart';
|
import 'package:localsend_isolates/model/dto/multicast_dto.dart';
|
||||||
import 'package:localsend_isolates/rust/api/server.dart' show WebSendI18n, WebSendParams;
|
import 'package:localsend_isolates/rust/api/server.dart' show WebI18n, WebParams, WebSendParams;
|
||||||
import 'package:localsend_isolates/util/rust.dart';
|
import 'package:localsend_isolates/util/rust.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:refena_flutter/refena_flutter.dart';
|
import 'package:refena_flutter/refena_flutter.dart';
|
||||||
@@ -92,11 +92,13 @@ class ServerService extends Notifier<ServerState?> {
|
|||||||
|
|
||||||
/// Starts the server.
|
/// Starts the server.
|
||||||
/// Passing a [webSendState] additionally serves the web send (download) API.
|
/// Passing a [webSendState] additionally serves the web send (download) API.
|
||||||
|
/// Passing [webUpload] serves the upload page so web browsers can upload files.
|
||||||
Future<ServerState?> startServer({
|
Future<ServerState?> startServer({
|
||||||
required String alias,
|
required String alias,
|
||||||
required int port,
|
required int port,
|
||||||
required bool https,
|
required bool https,
|
||||||
WebSendState? webSendState,
|
WebSendState? webSendState,
|
||||||
|
bool webUpload = false,
|
||||||
}) async {
|
}) async {
|
||||||
if (state != null) {
|
if (state != null) {
|
||||||
_logger.info('Server already running.');
|
_logger.info('Server already running.');
|
||||||
@@ -124,18 +126,25 @@ class ServerService extends Notifier<ServerState?> {
|
|||||||
.dispatchTakeResult(
|
.dispatchTakeResult(
|
||||||
IsolateHttpServerStartAction(
|
IsolateHttpServerStartAction(
|
||||||
pin: settings.receivePin,
|
pin: settings.receivePin,
|
||||||
webSend: webSendState != null
|
web: webSendState != null || webUpload
|
||||||
|
? WebParams(
|
||||||
|
send: webSendState != null
|
||||||
? WebSendParams(
|
? WebSendParams(
|
||||||
files: {
|
files: {
|
||||||
for (final entry in webSendState.files.entries) entry.key: entry.value.file.toRust(),
|
for (final entry in webSendState.files.entries) entry.key: entry.value.file.toRust(),
|
||||||
},
|
},
|
||||||
pin: webSendState.pin,
|
pin: webSendState.pin,
|
||||||
i18N: WebSendI18n(
|
)
|
||||||
|
: null,
|
||||||
|
upload: webUpload,
|
||||||
|
i18N: WebI18n(
|
||||||
waiting: t.web.waiting,
|
waiting: t.web.waiting,
|
||||||
enterPin: t.web.enterPin,
|
enterPin: t.web.enterPin,
|
||||||
invalidPin: t.web.invalidPin,
|
invalidPin: t.web.invalidPin,
|
||||||
tooManyAttempts: t.web.tooManyAttempts,
|
tooManyAttempts: t.web.tooManyAttempts,
|
||||||
rejected: t.web.rejected,
|
rejected: t.web.rejected,
|
||||||
|
uploadRejected: t.sendPage.rejected,
|
||||||
|
busy: t.sendPage.busy,
|
||||||
files: t.web.files,
|
files: t.web.files,
|
||||||
fileName: t.web.fileName,
|
fileName: t.web.fileName,
|
||||||
size: t.web.size,
|
size: t.web.size,
|
||||||
@@ -183,6 +192,7 @@ class ServerService extends Notifier<ServerState?> {
|
|||||||
https: https,
|
https: https,
|
||||||
session: null,
|
session: null,
|
||||||
webSendState: webSendState,
|
webSendState: webSendState,
|
||||||
|
webUpload: webUpload,
|
||||||
);
|
);
|
||||||
|
|
||||||
state = newServerState;
|
state = newServerState;
|
||||||
@@ -204,9 +214,15 @@ class ServerService extends Notifier<ServerState?> {
|
|||||||
return await startServerFromSettings();
|
return await startServerFromSettings();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<ServerState?> restartServer({required String alias, required int port, required bool https, WebSendState? webSendState}) async {
|
Future<ServerState?> restartServer({
|
||||||
|
required String alias,
|
||||||
|
required int port,
|
||||||
|
required bool https,
|
||||||
|
WebSendState? webSendState,
|
||||||
|
bool webUpload = false,
|
||||||
|
}) async {
|
||||||
await stopServer();
|
await stopServer();
|
||||||
return await startServer(alias: alias, port: port, https: https, webSendState: webSendState);
|
return await startServer(alias: alias, port: port, https: https, webSendState: webSendState, webUpload: webUpload);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> acceptFileRequest(Map<String, String> fileNameMap) async {
|
Future<void> acceptFileRequest(Map<String, String> fileNameMap) async {
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ const _enableAnimations = 'ls_enable_animations';
|
|||||||
const _deviceType = 'ls_device_type';
|
const _deviceType = 'ls_device_type';
|
||||||
const _deviceModel = 'ls_device_model';
|
const _deviceModel = 'ls_device_model';
|
||||||
const _shareViaLinkAutoAccept = 'ls_share_via_link_auto_accept';
|
const _shareViaLinkAutoAccept = 'ls_share_via_link_auto_accept';
|
||||||
|
const _receiveViaLinkAutoAccept = 'ls_receive_via_link_auto_accept';
|
||||||
const _advancedSettingsKey = 'ls_advanced_settings';
|
const _advancedSettingsKey = 'ls_advanced_settings';
|
||||||
const _whatsNewKey = 'ls_whats_new';
|
const _whatsNewKey = 'ls_whats_new';
|
||||||
|
|
||||||
@@ -368,6 +369,14 @@ class PersistenceService {
|
|||||||
await _prefs.setBool(_shareViaLinkAutoAccept, shareViaLinkAutoAccept);
|
await _prefs.setBool(_shareViaLinkAutoAccept, shareViaLinkAutoAccept);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool getReceiveViaLinkAutoAccept() {
|
||||||
|
return _prefs.getBool(_receiveViaLinkAutoAccept) ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> setReceiveViaLinkAutoAccept(bool receiveViaLinkAutoAccept) async {
|
||||||
|
await _prefs.setBool(_receiveViaLinkAutoAccept, receiveViaLinkAutoAccept);
|
||||||
|
}
|
||||||
|
|
||||||
String getMulticastGroup() {
|
String getMulticastGroup() {
|
||||||
return _prefs.getString(_multicastGroupKey) ?? defaultMulticastGroup;
|
return _prefs.getString(_multicastGroupKey) ?? defaultMulticastGroup;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ class SettingsService extends PureNotifier<SettingsState> {
|
|||||||
deviceType: _persistence.getDeviceType(),
|
deviceType: _persistence.getDeviceType(),
|
||||||
deviceModel: _persistence.getDeviceModel(),
|
deviceModel: _persistence.getDeviceModel(),
|
||||||
shareViaLinkAutoAccept: _persistence.getShareViaLinkAutoAccept(),
|
shareViaLinkAutoAccept: _persistence.getShareViaLinkAutoAccept(),
|
||||||
|
receiveViaLinkAutoAccept: _persistence.getReceiveViaLinkAutoAccept(),
|
||||||
discoveryTimeout: _persistence.getDiscoveryTimeout(),
|
discoveryTimeout: _persistence.getDiscoveryTimeout(),
|
||||||
advancedSettings: _persistence.getAdvancedSettingsEnabled(),
|
advancedSettings: _persistence.getAdvancedSettingsEnabled(),
|
||||||
);
|
);
|
||||||
@@ -264,4 +265,12 @@ class SettingsService extends PureNotifier<SettingsState> {
|
|||||||
shareViaLinkAutoAccept: shareViaLinkAutoAccept,
|
shareViaLinkAutoAccept: shareViaLinkAutoAccept,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> setReceiveViaLinkAutoAccept(bool receiveViaLinkAutoAccept) async {
|
||||||
|
await _persistence.setReceiveViaLinkAutoAccept(receiveViaLinkAutoAccept);
|
||||||
|
|
||||||
|
state = state.copyWith(
|
||||||
|
receiveViaLinkAutoAccept: receiveViaLinkAutoAccept,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-4
@@ -1315,18 +1315,18 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: refena
|
name: refena
|
||||||
sha256: d2bb36fd39412563a4515b8228271cf8786936f77d4a6261b05eecf648e59b6b
|
sha256: bd609a1015728db75af79df295c44ccf60507f965e0c32b447d67c48816c7d3a
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.2.2"
|
version: "3.4.0"
|
||||||
refena_flutter:
|
refena_flutter:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: refena_flutter
|
name: refena_flutter
|
||||||
sha256: "18f704c3ba38a30aa05ae71b3c55777b8d09864570e3b88b2f8814644381e461"
|
sha256: "36417ef3a60c9c134286ab3f8491d98b07f40b2163b994c4a0c905831415a6fc"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.2.1"
|
version: "3.4.0"
|
||||||
refena_inspector:
|
refena_inspector:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
|
|||||||
+1
-1
@@ -48,7 +48,7 @@ dependencies:
|
|||||||
path_provider_foundation: 2.6.0
|
path_provider_foundation: 2.6.0
|
||||||
permission_handler: 12.0.3
|
permission_handler: 12.0.3
|
||||||
pretty_qr_code: 3.6.0
|
pretty_qr_code: 3.6.0
|
||||||
refena_flutter: 3.2.1
|
refena_flutter: 3.4.0
|
||||||
refena_inspector_client: 2.1.1
|
refena_inspector_client: 2.1.1
|
||||||
routerino: 0.8.1
|
routerino: 0.8.1
|
||||||
saf_stream: 2.0.0
|
saf_stream: 2.0.0
|
||||||
|
|||||||
@@ -296,6 +296,28 @@ class MockPersistenceService extends _i1.Mock implements _i3.PersistenceService
|
|||||||
)
|
)
|
||||||
as _i4.Future<void>);
|
as _i4.Future<void>);
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool getReceiveViaLinkAutoAccept() =>
|
||||||
|
(super.noSuchMethod(
|
||||||
|
Invocation.method(#getReceiveViaLinkAutoAccept, []),
|
||||||
|
returnValue: false,
|
||||||
|
returnValueForMissingStub: false,
|
||||||
|
)
|
||||||
|
as bool);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_i4.Future<void> setReceiveViaLinkAutoAccept(
|
||||||
|
bool? receiveViaLinkAutoAccept,
|
||||||
|
) =>
|
||||||
|
(super.noSuchMethod(
|
||||||
|
Invocation.method(#setReceiveViaLinkAutoAccept, [
|
||||||
|
receiveViaLinkAutoAccept,
|
||||||
|
]),
|
||||||
|
returnValue: _i4.Future<void>.value(),
|
||||||
|
returnValueForMissingStub: _i4.Future<void>.value(),
|
||||||
|
)
|
||||||
|
as _i4.Future<void>);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String getMulticastGroup() =>
|
String getMulticastGroup() =>
|
||||||
(super.noSuchMethod(
|
(super.noSuchMethod(
|
||||||
@@ -572,6 +594,15 @@ class MockPersistenceService extends _i1.Mock implements _i3.PersistenceService
|
|||||||
)
|
)
|
||||||
as _i4.Future<void>);
|
as _i4.Future<void>);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_i4.Future<void> setWhatsNew(String? version) =>
|
||||||
|
(super.noSuchMethod(
|
||||||
|
Invocation.method(#setWhatsNew, [version]),
|
||||||
|
returnValue: _i4.Future<void>.value(),
|
||||||
|
returnValueForMissingStub: _i4.Future<void>.value(),
|
||||||
|
)
|
||||||
|
as _i4.Future<void>);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_i4.Future<void> clear() =>
|
_i4.Future<void> clear() =>
|
||||||
(super.noSuchMethod(
|
(super.noSuchMethod(
|
||||||
|
|||||||
@@ -83,7 +83,7 @@
|
|||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<script src="main.js" defer></script>
|
<script src="download.js" defer></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<noscript>
|
<noscript>
|
||||||
@@ -0,0 +1,232 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>LocalSend</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: sans-serif, system-ui;
|
||||||
|
margin: 0;
|
||||||
|
line-height: 1.5;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#content {
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 400px;
|
||||||
|
margin: 15vh auto 0;
|
||||||
|
padding: 0 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#upload-button {
|
||||||
|
width: 100%;
|
||||||
|
padding: 1em;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 1.5em;
|
||||||
|
color: #FFFFFF;
|
||||||
|
background-color: #00796B;
|
||||||
|
border: none;
|
||||||
|
border-radius: 0.5em;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
#upload-button:hover {
|
||||||
|
background-color: #009688;
|
||||||
|
}
|
||||||
|
|
||||||
|
#status-text {
|
||||||
|
display: none;
|
||||||
|
font-size: 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#progress-text {
|
||||||
|
display: none;
|
||||||
|
font-size: 1.5em;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<noscript>
|
||||||
|
LocalSend requires JavaScript, which is currently disabled. Please enable it and try again.
|
||||||
|
</noscript>
|
||||||
|
|
||||||
|
<h1 style="padding: 0.5em">LocalSend</h1>
|
||||||
|
<div id="content">
|
||||||
|
<button id="upload-button" type="button">Upload</button>
|
||||||
|
<input id="file-input" type="file" multiple style="display: none">
|
||||||
|
<p id="status-text"></p>
|
||||||
|
<p id="progress-text"></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
var BASE_URL = '/api/localsend/v2';
|
||||||
|
|
||||||
|
var i18n = {};
|
||||||
|
|
||||||
|
function makeRequest(url, method, body, callback) {
|
||||||
|
var xhr = new XMLHttpRequest();
|
||||||
|
xhr.open(method, url, true);
|
||||||
|
xhr.onreadystatechange = function () {
|
||||||
|
if (xhr.readyState === 4) {
|
||||||
|
callback(xhr);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
xhr.send(body);
|
||||||
|
}
|
||||||
|
|
||||||
|
function fetchI18n(then) {
|
||||||
|
makeRequest('/i18n.json', 'GET', null, function (response) {
|
||||||
|
if (response.status === 200) {
|
||||||
|
i18n = JSON.parse(response.responseText);
|
||||||
|
then();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function showButton() {
|
||||||
|
document.getElementById('upload-button').style.display = 'block';
|
||||||
|
document.getElementById('status-text').style.display = 'none';
|
||||||
|
document.getElementById('progress-text').style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
function showStatus(text) {
|
||||||
|
document.getElementById('upload-button').style.display = 'none';
|
||||||
|
var statusText = document.getElementById('status-text');
|
||||||
|
statusText.style.display = 'block';
|
||||||
|
statusText.innerText = text;
|
||||||
|
}
|
||||||
|
|
||||||
|
function showProgress(finished, total) {
|
||||||
|
var progressText = document.getElementById('progress-text');
|
||||||
|
progressText.style.display = 'block';
|
||||||
|
progressText.innerText = finished + '/' + total + ' file(s)';
|
||||||
|
}
|
||||||
|
|
||||||
|
function startUpload(fileList) {
|
||||||
|
showStatus(i18n.waiting);
|
||||||
|
|
||||||
|
var selectedFiles = {};
|
||||||
|
var filesDto = {};
|
||||||
|
for (var i = 0; i < fileList.length; i++) {
|
||||||
|
var file = fileList[i];
|
||||||
|
var fileId = String(i);
|
||||||
|
selectedFiles[fileId] = file;
|
||||||
|
filesDto[fileId] = {
|
||||||
|
id: fileId,
|
||||||
|
fileName: file.name,
|
||||||
|
size: file.size,
|
||||||
|
fileType: file.type || 'application/octet-stream'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
var body = JSON.stringify({
|
||||||
|
info: {
|
||||||
|
alias: 'Web Browser',
|
||||||
|
version: '2.1',
|
||||||
|
deviceType: 'web',
|
||||||
|
fingerprint: getFingerprint(),
|
||||||
|
port: 0,
|
||||||
|
protocol: location.protocol === 'https:' ? 'https' : 'http',
|
||||||
|
download: false
|
||||||
|
},
|
||||||
|
files: filesDto
|
||||||
|
});
|
||||||
|
|
||||||
|
makeRequest(BASE_URL + '/prepare-upload', 'POST', body, function (response) {
|
||||||
|
if (response.status === 403) {
|
||||||
|
showStatus(i18n.uploadRejected);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.status === 409) {
|
||||||
|
showStatus(i18n.busy);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.status === 204) {
|
||||||
|
// The recipient accepted the request but declined every file.
|
||||||
|
showStatus(i18n.uploadRejected);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.status !== 200) {
|
||||||
|
showStatus('Error: ' + response.status);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var data = JSON.parse(response.responseText);
|
||||||
|
uploadFiles(data.sessionId, data.files, selectedFiles);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function uploadFiles(sessionId, tokens, selectedFiles) {
|
||||||
|
var fileIds = getKeys(tokens);
|
||||||
|
var total = fileIds.length;
|
||||||
|
|
||||||
|
showStatus('');
|
||||||
|
showProgress(0, total);
|
||||||
|
|
||||||
|
function uploadNext(index) {
|
||||||
|
if (index >= total) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var fileId = fileIds[index];
|
||||||
|
var url = BASE_URL + '/upload' +
|
||||||
|
'?sessionId=' + encodeURIComponent(sessionId) +
|
||||||
|
'&fileId=' + encodeURIComponent(fileId) +
|
||||||
|
'&token=' + encodeURIComponent(tokens[fileId]);
|
||||||
|
|
||||||
|
makeRequest(url, 'POST', selectedFiles[fileId], function (response) {
|
||||||
|
if (response.status !== 200) {
|
||||||
|
showStatus('Error: ' + response.status);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
showProgress(index + 1, total);
|
||||||
|
uploadNext(index + 1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadNext(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFingerprint() {
|
||||||
|
var fingerprint = sessionStorage.getItem('fingerprint');
|
||||||
|
if (!fingerprint) {
|
||||||
|
fingerprint = 'web-';
|
||||||
|
for (var i = 0; i < 32; i++) {
|
||||||
|
fingerprint += Math.floor(Math.random() * 16).toString(16);
|
||||||
|
}
|
||||||
|
sessionStorage.setItem('fingerprint', fingerprint);
|
||||||
|
}
|
||||||
|
return fingerprint;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getKeys(obj) {
|
||||||
|
var keys = [];
|
||||||
|
for (var key in obj) {
|
||||||
|
keys.push(key);
|
||||||
|
}
|
||||||
|
return keys;
|
||||||
|
}
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
var fileInput = document.getElementById('file-input');
|
||||||
|
document.getElementById('upload-button').onclick = function () {
|
||||||
|
fileInput.click();
|
||||||
|
};
|
||||||
|
fileInput.onchange = function () {
|
||||||
|
if (fileInput.files.length > 0) {
|
||||||
|
startUpload(fileInput.files);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
fetchI18n(function () {});
|
||||||
|
}
|
||||||
|
|
||||||
|
init();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -11,10 +11,15 @@ use x509_parser::nom::AsBytes;
|
|||||||
/// Enables client certificate verification.
|
/// Enables client certificate verification.
|
||||||
pub(crate) struct CustomClientCertVerifier {
|
pub(crate) struct CustomClientCertVerifier {
|
||||||
inner: Arc<dyn ClientCertVerifier>,
|
inner: Arc<dyn ClientCertVerifier>,
|
||||||
|
|
||||||
|
/// Whether clients must present a certificate.
|
||||||
|
/// Optional when the web pages are served: browsers have no client certificate.
|
||||||
|
/// A certificate that is presented is always verified.
|
||||||
|
mandatory: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CustomClientCertVerifier {
|
impl CustomClientCertVerifier {
|
||||||
pub(crate) fn try_new(cert: &str) -> anyhow::Result<Self> {
|
pub(crate) fn try_new(cert: &str, mandatory: bool) -> anyhow::Result<Self> {
|
||||||
// We add the certificate of the server itself just so that no "empty" error is returned.
|
// We add the certificate of the server itself just so that no "empty" error is returned.
|
||||||
// We don't care about the authority of the certificate, just that it is valid.
|
// We don't care about the authority of the certificate, just that it is valid.
|
||||||
let mut root_cert_store = RootCertStore::empty();
|
let mut root_cert_store = RootCertStore::empty();
|
||||||
@@ -22,6 +27,7 @@ impl CustomClientCertVerifier {
|
|||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
inner: WebPkiClientVerifier::builder(Arc::new(root_cert_store)).build()?,
|
inner: WebPkiClientVerifier::builder(Arc::new(root_cert_store)).build()?,
|
||||||
|
mandatory,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -38,7 +44,7 @@ impl ClientCertVerifier for CustomClientCertVerifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn client_auth_mandatory(&self) -> bool {
|
fn client_auth_mandatory(&self) -> bool {
|
||||||
true
|
self.mandatory
|
||||||
}
|
}
|
||||||
|
|
||||||
fn root_hint_subjects(&self) -> &[DistinguishedName] {
|
fn root_hint_subjects(&self) -> &[DistinguishedName] {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ pub use peer_ip::PeerIp;
|
|||||||
use crate::crypto::cert::{fingerprint_from_cert_der, public_key_from_cert_der};
|
use crate::crypto::cert::{fingerprint_from_cert_der, public_key_from_cert_der};
|
||||||
use crate::http::server::internal::{InternalConfig, InternalState};
|
use crate::http::server::internal::{InternalConfig, InternalState};
|
||||||
use crate::http::server::v2::ServerEventV2;
|
use crate::http::server::v2::ServerEventV2;
|
||||||
use crate::http::server::web::WebSendConfig;
|
use crate::http::server::web::{WebConfig, WebI18n};
|
||||||
use crate::http::state::ClientInfo;
|
use crate::http::state::ClientInfo;
|
||||||
use common::client_cert_verifier::CustomClientCertVerifier;
|
use common::client_cert_verifier::CustomClientCertVerifier;
|
||||||
use common::error::AppError;
|
use common::error::AppError;
|
||||||
@@ -63,9 +63,15 @@ pub struct AppState {
|
|||||||
/// Information about server's device.
|
/// Information about server's device.
|
||||||
info: Arc<Mutex<ClientInfo>>,
|
info: Arc<Mutex<ClientInfo>>,
|
||||||
|
|
||||||
/// State for serving web pages.
|
/// State for serving the download page (web send).
|
||||||
web: Option<Arc<WebPageState>>,
|
web: Option<Arc<WebPageState>>,
|
||||||
|
|
||||||
|
/// Whether the upload page is served (when the download page is not active).
|
||||||
|
web_upload: bool,
|
||||||
|
|
||||||
|
/// Translations for the web pages, served via `/i18n.json`.
|
||||||
|
web_i18n: Option<Arc<WebI18n>>,
|
||||||
|
|
||||||
/// State for application-internal endpoints.
|
/// State for application-internal endpoints.
|
||||||
internal: Option<Arc<InternalState>>,
|
internal: Option<Arc<InternalState>>,
|
||||||
|
|
||||||
@@ -84,7 +90,7 @@ impl AppState {
|
|||||||
info: Arc<Mutex<ClientInfo>>,
|
info: Arc<Mutex<ClientInfo>>,
|
||||||
internal_config: Option<InternalConfig>,
|
internal_config: Option<InternalConfig>,
|
||||||
v2_config: Option<ServerConfigV2>,
|
v2_config: Option<ServerConfigV2>,
|
||||||
web_send_config: Option<WebSendConfig>,
|
web_config: Option<WebConfig>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let v2 = v2_config.map(|config| {
|
let v2 = v2_config.map(|config| {
|
||||||
Arc::new(V2State {
|
Arc::new(V2State {
|
||||||
@@ -95,12 +101,21 @@ impl AppState {
|
|||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
let web = web_send_config.map(|config| Arc::new(WebPageState::new(config)));
|
let (web, web_upload, web_i18n) = match web_config {
|
||||||
|
Some(config) => (
|
||||||
|
config.send.map(|send| Arc::new(WebPageState::new(send))),
|
||||||
|
config.upload,
|
||||||
|
Some(Arc::new(config.i18n)),
|
||||||
|
),
|
||||||
|
None => (None, false, None),
|
||||||
|
};
|
||||||
let internal = internal_config.map(|config| Arc::new(InternalState::new(config)));
|
let internal = internal_config.map(|config| Arc::new(InternalState::new(config)));
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
info,
|
info,
|
||||||
web,
|
web,
|
||||||
|
web_upload,
|
||||||
|
web_i18n,
|
||||||
internal,
|
internal,
|
||||||
received_nonce_map: Arc::new(Mutex::new(LruCache::new(
|
received_nonce_map: Arc::new(Mutex::new(LruCache::new(
|
||||||
NonZeroUsize::new(200).unwrap(),
|
NonZeroUsize::new(200).unwrap(),
|
||||||
@@ -200,12 +215,12 @@ pub async fn start_with_port(
|
|||||||
info: ClientInfo,
|
info: ClientInfo,
|
||||||
internal_config: Option<InternalConfig>,
|
internal_config: Option<InternalConfig>,
|
||||||
v2_config: Option<ServerConfigV2>,
|
v2_config: Option<ServerConfigV2>,
|
||||||
web_send_config: Option<WebSendConfig>,
|
web_config: Option<WebConfig>,
|
||||||
stop_rx: oneshot::Receiver<()>,
|
stop_rx: oneshot::Receiver<()>,
|
||||||
) -> anyhow::Result<ServerHandle> {
|
) -> anyhow::Result<ServerHandle> {
|
||||||
let ipv4_socket_addr = SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), port);
|
let ipv4_socket_addr = SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), port);
|
||||||
let info = Arc::new(Mutex::new(info));
|
let info = Arc::new(Mutex::new(info));
|
||||||
let state = AppState::new(info.clone(), internal_config, v2_config, web_send_config);
|
let state = AppState::new(info.clone(), internal_config, v2_config, web_config);
|
||||||
|
|
||||||
let ipv4_listener = tokio::net::TcpListener::bind(ipv4_socket_addr).await?;
|
let ipv4_listener = tokio::net::TcpListener::bind(ipv4_socket_addr).await?;
|
||||||
// With port 0, the IPv6 listener must reuse the port the IPv4 listener got.
|
// With port 0, the IPv6 listener must reuse the port the IPv4 listener got.
|
||||||
@@ -293,10 +308,16 @@ async fn start_server_with_listener(
|
|||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
let _ = rustls::crypto::ring::default_provider().install_default();
|
let _ = rustls::crypto::ring::default_provider().install_default();
|
||||||
|
|
||||||
|
// Browsers have no client certificate, so presenting one is optional while
|
||||||
|
// the web pages are served. A certificate that is presented is still verified.
|
||||||
|
let mandatory_client_auth = app_state.web.is_none() && !app_state.web_upload;
|
||||||
|
|
||||||
let tls_acceptor = match tls_config {
|
let tls_acceptor = match tls_config {
|
||||||
Some(tls_config) => Some(create_tls_config(&tls_config).inspect_err(|err| {
|
Some(tls_config) => Some(
|
||||||
|
create_tls_config(&tls_config, mandatory_client_auth).inspect_err(|err| {
|
||||||
tracing::error!("failed to create tls config: {err:#}");
|
tracing::error!("failed to create tls config: {err:#}");
|
||||||
})?),
|
})?,
|
||||||
|
),
|
||||||
None => None,
|
None => None,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -343,11 +364,13 @@ async fn serve_connection(
|
|||||||
let (_, server_connection) = tls_stream.get_ref();
|
let (_, server_connection) = tls_stream.get_ref();
|
||||||
RequestClientInfo {
|
RequestClientInfo {
|
||||||
ip: PeerIp::from_remote_addr(&remote_addr),
|
ip: PeerIp::from_remote_addr(&remote_addr),
|
||||||
|
// No certificate when client auth is optional (web pages served)
|
||||||
|
// and the client (e.g. a browser) did not present one.
|
||||||
cert: server_connection
|
cert: server_connection
|
||||||
.deref()
|
.deref()
|
||||||
.deref()
|
.deref()
|
||||||
.peer_certificates()
|
.peer_certificates()
|
||||||
.map(|cert| cert.get(0).unwrap().to_vec()),
|
.and_then(|certs| certs.first().map(|cert| cert.to_vec())),
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -386,7 +409,10 @@ async fn serve_connection(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_tls_config(tls_config: &TlsConfig) -> anyhow::Result<tokio_rustls::TlsAcceptor> {
|
fn create_tls_config(
|
||||||
|
tls_config: &TlsConfig,
|
||||||
|
mandatory_client_auth: bool,
|
||||||
|
) -> anyhow::Result<tokio_rustls::TlsAcceptor> {
|
||||||
let config = {
|
let config = {
|
||||||
let certs = vec![CertificateDer::from_pem_slice(&tls_config.cert.as_bytes())?];
|
let certs = vec![CertificateDer::from_pem_slice(&tls_config.cert.as_bytes())?];
|
||||||
let key = PrivateKeyDer::from_pem_slice(&tls_config.private_key.as_bytes())?;
|
let key = PrivateKeyDer::from_pem_slice(&tls_config.private_key.as_bytes())?;
|
||||||
@@ -394,6 +420,7 @@ fn create_tls_config(tls_config: &TlsConfig) -> anyhow::Result<tokio_rustls::Tls
|
|||||||
rustls::ServerConfig::builder()
|
rustls::ServerConfig::builder()
|
||||||
.with_client_cert_verifier(Arc::new(CustomClientCertVerifier::try_new(
|
.with_client_cert_verifier(Arc::new(CustomClientCertVerifier::try_new(
|
||||||
&tls_config.cert,
|
&tls_config.cert,
|
||||||
|
mandatory_client_auth,
|
||||||
)?))
|
)?))
|
||||||
.with_single_cert(certs, key)?
|
.with_single_cert(certs, key)?
|
||||||
};
|
};
|
||||||
@@ -456,7 +483,7 @@ async fn handle_request_inner(mut req: Request<Incoming>) -> Result<Response<Box
|
|||||||
|
|
||||||
match (req.method(), req.uri().path()) {
|
match (req.method(), req.uri().path()) {
|
||||||
(&Method::GET, "/") => Ok(web::index(&state)),
|
(&Method::GET, "/") => Ok(web::index(&state)),
|
||||||
(&Method::GET, "/main.js") => Ok(web::main_js(&state)),
|
(&Method::GET, "/download.js") => Ok(web::download_js(&state)),
|
||||||
(&Method::GET, "/i18n.json") => web::i18n(&state),
|
(&Method::GET, "/i18n.json") => web::i18n(&state),
|
||||||
(&Method::POST, "/api/localsend/v2/prepare-download") => {
|
(&Method::POST, "/api/localsend/v2/prepare-download") => {
|
||||||
web::prepare_download(req, state, client_info).await
|
web::prepare_download(req, state, client_info).await
|
||||||
|
|||||||
@@ -67,8 +67,9 @@ pub enum WebSendEvent {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const INDEX_HTML: &str = include_str!("../../../assets/web/index.html");
|
const DOWNLOAD_HTML: &str = include_str!("../../../assets/web/download.html");
|
||||||
const MAIN_JS: &str = include_str!("../../../assets/web/main.js");
|
const DOWNLOAD_JS: &str = include_str!("../../../assets/web/download.js");
|
||||||
|
const UPLOAD_HTML: &str = include_str!("../../../assets/web/upload.html");
|
||||||
const ERROR_403_HTML: &str = include_str!("../../../assets/web/error-403.html");
|
const ERROR_403_HTML: &str = include_str!("../../../assets/web/error-403.html");
|
||||||
|
|
||||||
/// Characters that are percent-encoded in the content-disposition file name.
|
/// Characters that are percent-encoded in the content-disposition file name.
|
||||||
@@ -84,6 +85,21 @@ const FILE_NAME_ENCODE_SET: &AsciiSet = &NON_ALPHANUMERIC
|
|||||||
.remove(b'(')
|
.remove(b'(')
|
||||||
.remove(b')');
|
.remove(b')');
|
||||||
|
|
||||||
|
/// Configuration for the web pages served to browsers.
|
||||||
|
pub struct WebConfig {
|
||||||
|
/// Enables web send (the download page): files offered for download by web browsers.
|
||||||
|
/// `None` disables the download page and the download API.
|
||||||
|
pub send: Option<WebSendConfig>,
|
||||||
|
|
||||||
|
/// Serves the upload page so web browsers can upload files
|
||||||
|
/// via the v2 `prepare-upload`/`upload` endpoints.
|
||||||
|
/// Ignored when [`WebConfig::send`] is set: the download page takes precedence at `/`.
|
||||||
|
pub upload: bool,
|
||||||
|
|
||||||
|
/// Translations for the web pages, served via `/i18n.json`.
|
||||||
|
pub i18n: WebI18n,
|
||||||
|
}
|
||||||
|
|
||||||
/// Configuration for web send (download API): files offered for download by web browsers.
|
/// Configuration for web send (download API): files offered for download by web browsers.
|
||||||
///
|
///
|
||||||
/// Web send can be enabled independently of the v2/v3 protocol endpoints.
|
/// Web send can be enabled independently of the v2/v3 protocol endpoints.
|
||||||
@@ -97,28 +113,27 @@ pub struct WebSendConfig {
|
|||||||
/// Optional PIN that web clients must provide via the `pin` query parameter.
|
/// Optional PIN that web clients must provide via the `pin` query parameter.
|
||||||
pub pin: Option<String>,
|
pub pin: Option<String>,
|
||||||
|
|
||||||
/// Translations for the web page, served via `/i18n.json`.
|
|
||||||
pub i18n: WebSendI18n,
|
|
||||||
|
|
||||||
/// Channel on which the server emits events that must be handled by the application.
|
/// Channel on which the server emits events that must be handled by the application.
|
||||||
pub event_tx: mpsc::Sender<WebSendEvent>,
|
pub event_tx: mpsc::Sender<WebSendEvent>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Translations for the web page, served via `/i18n.json`.
|
/// Translations for the web pages, served via `/i18n.json`.
|
||||||
#[derive(Clone, Debug, Serialize)]
|
#[derive(Clone, Debug, Serialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct WebSendI18n {
|
pub struct WebI18n {
|
||||||
pub waiting: String,
|
pub waiting: String,
|
||||||
pub enter_pin: String,
|
pub enter_pin: String,
|
||||||
pub invalid_pin: String,
|
pub invalid_pin: String,
|
||||||
pub too_many_attempts: String,
|
pub too_many_attempts: String,
|
||||||
pub rejected: String,
|
pub rejected: String,
|
||||||
|
pub upload_rejected: String,
|
||||||
|
pub busy: String,
|
||||||
pub files: String,
|
pub files: String,
|
||||||
pub file_name: String,
|
pub file_name: String,
|
||||||
pub size: String,
|
pub size: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for WebSendI18n {
|
impl Default for WebI18n {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
waiting: "Waiting for response…".to_string(),
|
waiting: "Waiting for response…".to_string(),
|
||||||
@@ -126,6 +141,8 @@ impl Default for WebSendI18n {
|
|||||||
invalid_pin: "Invalid PIN".to_string(),
|
invalid_pin: "Invalid PIN".to_string(),
|
||||||
too_many_attempts: "Too many attempts".to_string(),
|
too_many_attempts: "Too many attempts".to_string(),
|
||||||
rejected: "Rejected".to_string(),
|
rejected: "Rejected".to_string(),
|
||||||
|
upload_rejected: "The recipient has rejected the request.".to_string(),
|
||||||
|
busy: "The recipient is busy with another request.".to_string(),
|
||||||
files: "Files".to_string(),
|
files: "Files".to_string(),
|
||||||
file_name: "File name".to_string(),
|
file_name: "File name".to_string(),
|
||||||
size: "Size".to_string(),
|
size: "Size".to_string(),
|
||||||
@@ -141,9 +158,6 @@ pub(crate) struct WebPageState {
|
|||||||
/// Optional PIN required for prepare-download requests.
|
/// Optional PIN required for prepare-download requests.
|
||||||
pub(crate) pin: Option<String>,
|
pub(crate) pin: Option<String>,
|
||||||
|
|
||||||
/// Translations served via `/i18n.json`.
|
|
||||||
pub(crate) i18n: WebSendI18n,
|
|
||||||
|
|
||||||
/// Channel on which server events are emitted to the application.
|
/// Channel on which server events are emitted to the application.
|
||||||
pub(crate) event_tx: mpsc::Sender<WebSendEvent>,
|
pub(crate) event_tx: mpsc::Sender<WebSendEvent>,
|
||||||
|
|
||||||
@@ -159,7 +173,6 @@ impl WebPageState {
|
|||||||
Self {
|
Self {
|
||||||
files: config.files,
|
files: config.files,
|
||||||
pin: config.pin,
|
pin: config.pin,
|
||||||
i18n: config.i18n,
|
|
||||||
event_tx: config.event_tx,
|
event_tx: config.event_tx,
|
||||||
sessions: Mutex::new(HashMap::new()),
|
sessions: Mutex::new(HashMap::new()),
|
||||||
pin_attempts: Mutex::new(LruCache::new(NonZeroUsize::new(200).unwrap())),
|
pin_attempts: Mutex::new(LruCache::new(NonZeroUsize::new(200).unwrap())),
|
||||||
@@ -177,25 +190,37 @@ pub(crate) struct WebSendSession {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn index(state: &AppState) -> Response<BoxedBody> {
|
pub(crate) fn index(state: &AppState) -> Response<BoxedBody> {
|
||||||
match &state.web {
|
if state.web.is_some() {
|
||||||
Some(_) => html_response(StatusCode::OK, INDEX_HTML, "text/html; charset=utf-8"),
|
html_response(StatusCode::OK, DOWNLOAD_HTML, "text/html; charset=utf-8")
|
||||||
None => error_403_page(),
|
} else if state.web_upload {
|
||||||
|
html_response(StatusCode::OK, UPLOAD_HTML, "text/html; charset=utf-8")
|
||||||
|
} else {
|
||||||
|
error_403_page()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn main_js(state: &AppState) -> Response<BoxedBody> {
|
pub(crate) fn download_js(state: &AppState) -> Response<BoxedBody> {
|
||||||
match &state.web {
|
match &state.web {
|
||||||
Some(_) => html_response(StatusCode::OK, MAIN_JS, "text/javascript; charset=utf-8"),
|
Some(_) => html_response(
|
||||||
|
StatusCode::OK,
|
||||||
|
DOWNLOAD_JS,
|
||||||
|
"text/javascript; charset=utf-8",
|
||||||
|
),
|
||||||
None => error_403_page(),
|
None => error_403_page(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn i18n(state: &AppState) -> Result<Response<BoxedBody>, AppError> {
|
pub(crate) fn i18n(state: &AppState) -> Result<Response<BoxedBody>, AppError> {
|
||||||
let web = require_web(state)?;
|
let Some(i18n) = &state.web_i18n else {
|
||||||
|
return Err(AppError::Message(
|
||||||
|
StatusCode::FORBIDDEN,
|
||||||
|
"Web pages not initialized.".to_string(),
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
Ok(JsonResponse {
|
Ok(JsonResponse {
|
||||||
status: StatusCode::OK,
|
status: StatusCode::OK,
|
||||||
body: &web.i18n,
|
body: i18n.as_ref(),
|
||||||
}
|
}
|
||||||
.into_response())
|
.into_response())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ use localsend::http::client::{ClientError, LsHttpClientV2};
|
|||||||
use localsend::http::dto_v2::{PrepareUploadRequestDtoV2, RegisterDtoV2};
|
use localsend::http::dto_v2::{PrepareUploadRequestDtoV2, RegisterDtoV2};
|
||||||
use localsend::http::server::common::save::FileUploadTarget;
|
use localsend::http::server::common::save::FileUploadTarget;
|
||||||
use localsend::http::server::v2::{PrepareUploadDecisionV2, ServerEventV2};
|
use localsend::http::server::v2::{PrepareUploadDecisionV2, ServerEventV2};
|
||||||
|
use localsend::http::server::web::{WebConfig, WebI18n};
|
||||||
use localsend::http::server::{start_with_port, ServerConfigV2, TlsConfig};
|
use localsend::http::server::{start_with_port, ServerConfigV2, TlsConfig};
|
||||||
use localsend::http::state::ClientInfo;
|
use localsend::http::state::ClientInfo;
|
||||||
use localsend::model::discovery::ProtocolType;
|
use localsend::model::discovery::ProtocolType;
|
||||||
@@ -49,6 +50,12 @@ struct TestServer {
|
|||||||
|
|
||||||
/// Starts a test server over TLS using the given identity.
|
/// Starts a test server over TLS using the given identity.
|
||||||
async fn start_tls_server(identity: &Identity) -> TestServer {
|
async fn start_tls_server(identity: &Identity) -> TestServer {
|
||||||
|
start_tls_server_with_web(identity, None).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Starts a test server over TLS, optionally with the web pages enabled
|
||||||
|
/// (which makes the client certificate optional).
|
||||||
|
async fn start_tls_server_with_web(identity: &Identity, web: Option<WebConfig>) -> TestServer {
|
||||||
let _ = tracing_subscriber::fmt().with_test_writer().try_init();
|
let _ = tracing_subscriber::fmt().with_test_writer().try_init();
|
||||||
let port = free_port();
|
let port = free_port();
|
||||||
let prepare_uploads: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(Vec::new()));
|
let prepare_uploads: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(Vec::new()));
|
||||||
@@ -118,7 +125,7 @@ async fn start_tls_server(identity: &Identity) -> TestServer {
|
|||||||
pin: None,
|
pin: None,
|
||||||
event_tx,
|
event_tx,
|
||||||
}),
|
}),
|
||||||
None,
|
web,
|
||||||
stop_rx,
|
stop_rx,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
@@ -365,6 +372,70 @@ async fn test_upload_body_not_sent_on_fingerprint_mismatch() {
|
|||||||
assert!(server.received.lock().await.is_empty());
|
assert!(server.received.lock().await.is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Without the web pages, the client certificate stays mandatory: a client
|
||||||
|
/// without one (e.g. a browser) must fail the handshake.
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_client_without_cert_rejected() {
|
||||||
|
let server_identity = generate_identity();
|
||||||
|
let server = start_tls_server(&server_identity).await;
|
||||||
|
let client = LsHttpClientV2::try_new_without_cert().unwrap();
|
||||||
|
|
||||||
|
let result = client
|
||||||
|
.info(ProtocolType::Https, "127.0.0.1", server.port)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
matches!(result, Err(ClientError::Reqwest(_))),
|
||||||
|
"expected the handshake to fail, got {:?}",
|
||||||
|
result.err()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// With the web pages served, the client certificate is optional so that
|
||||||
|
/// browsers can connect; a presented certificate is still verified (mTLS
|
||||||
|
/// clients keep working).
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_client_without_cert_allowed_in_web_mode() {
|
||||||
|
let server_identity = generate_identity();
|
||||||
|
let sender = generate_identity();
|
||||||
|
let server = start_tls_server_with_web(
|
||||||
|
&server_identity,
|
||||||
|
Some(WebConfig {
|
||||||
|
send: None,
|
||||||
|
upload: true,
|
||||||
|
i18n: WebI18n::default(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
// A browser-like client: no client certificate, self-signed server cert accepted.
|
||||||
|
let browser = localsend::reqwest::Client::builder()
|
||||||
|
.use_rustls_tls()
|
||||||
|
.danger_accept_invalid_certs(true)
|
||||||
|
.build()
|
||||||
|
.unwrap();
|
||||||
|
let response = browser
|
||||||
|
.get(format!("https://127.0.0.1:{}/", server.port))
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect("browser without client certificate should be able to connect");
|
||||||
|
assert_eq!(response.status().as_u16(), 200);
|
||||||
|
assert!(response.text().await.unwrap().contains("prepare-upload"));
|
||||||
|
|
||||||
|
// A LocalSend peer still authenticates with its certificate.
|
||||||
|
let client = client(&sender, Some(&server_identity.fingerprint));
|
||||||
|
let response = client
|
||||||
|
.register(
|
||||||
|
ProtocolType::Https,
|
||||||
|
"127.0.0.1",
|
||||||
|
server.port,
|
||||||
|
sender_info(&sender.fingerprint),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("register with client certificate should succeed");
|
||||||
|
assert!(response.public_key.is_some());
|
||||||
|
}
|
||||||
|
|
||||||
/// Discovery has no fingerprint to pin yet, so any valid certificate is
|
/// Discovery has no fingerprint to pin yet, so any valid certificate is
|
||||||
/// accepted and the public key is read from the response.
|
/// accepted and the public key is read from the response.
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use bytes::Bytes;
|
|||||||
use localsend::http::client::{ClientError, LsHttpClientV2};
|
use localsend::http::client::{ClientError, LsHttpClientV2};
|
||||||
use localsend::http::server::v2::ServerEventV2;
|
use localsend::http::server::v2::ServerEventV2;
|
||||||
use localsend::http::server::web::WebSendConfig;
|
use localsend::http::server::web::WebSendConfig;
|
||||||
use localsend::http::server::web::{WebSendEvent, WebSendI18n};
|
use localsend::http::server::web::{WebConfig, WebI18n, WebSendEvent};
|
||||||
use localsend::http::server::{start_with_port, ServerConfigV2};
|
use localsend::http::server::{start_with_port, ServerConfigV2};
|
||||||
use localsend::http::state::ClientInfo;
|
use localsend::http::state::ClientInfo;
|
||||||
use localsend::model::discovery::ProtocolType;
|
use localsend::model::discovery::ProtocolType;
|
||||||
@@ -118,9 +118,13 @@ async fn start_test_server(
|
|||||||
let (stop_tx, stop_rx) = oneshot::channel::<()>();
|
let (stop_tx, stop_rx) = oneshot::channel::<()>();
|
||||||
|
|
||||||
// Web send is configured independently of the v2 endpoints.
|
// Web send is configured independently of the v2 endpoints.
|
||||||
let web_send = web_send.map(|mut config| {
|
let web_config = web_send.map(|mut config| {
|
||||||
config.event_tx = web_event_tx;
|
config.event_tx = web_event_tx;
|
||||||
config
|
WebConfig {
|
||||||
|
send: Some(config),
|
||||||
|
upload: false,
|
||||||
|
i18n: WebI18n::default(),
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
start_with_port(
|
start_with_port(
|
||||||
@@ -138,7 +142,7 @@ async fn start_test_server(
|
|||||||
pin: None,
|
pin: None,
|
||||||
event_tx: v2_event_tx,
|
event_tx: v2_event_tx,
|
||||||
}),
|
}),
|
||||||
web_send,
|
web_config,
|
||||||
stop_rx,
|
stop_rx,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
@@ -242,7 +246,6 @@ fn web_send_config(
|
|||||||
WebSendConfig {
|
WebSendConfig {
|
||||||
files,
|
files,
|
||||||
pin,
|
pin,
|
||||||
i18n: WebSendI18n::default(),
|
|
||||||
event_tx,
|
event_tx,
|
||||||
},
|
},
|
||||||
contents,
|
contents,
|
||||||
@@ -271,7 +274,7 @@ async fn test_web_page() {
|
|||||||
assert!(response.text().await.unwrap().contains("LocalSend"));
|
assert!(response.text().await.unwrap().contains("LocalSend"));
|
||||||
|
|
||||||
let response = client
|
let response = client
|
||||||
.get(format!("{base_url}/main.js"))
|
.get(format!("{base_url}/download.js"))
|
||||||
.send()
|
.send()
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -326,6 +329,69 @@ async fn test_web_page_disabled() {
|
|||||||
assert!(!info.download);
|
assert!(!info.download);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_upload_page() {
|
||||||
|
let _ = tracing_subscriber::fmt().with_test_writer().try_init();
|
||||||
|
let port = free_port();
|
||||||
|
let (v2_event_tx, _v2_event_rx) = mpsc::channel::<ServerEventV2>(16);
|
||||||
|
let (_stop_tx, stop_rx) = oneshot::channel::<()>();
|
||||||
|
|
||||||
|
start_with_port(
|
||||||
|
port,
|
||||||
|
None, // plain HTTP
|
||||||
|
ClientInfo {
|
||||||
|
alias: "Test Server".to_string(),
|
||||||
|
version: "2.1".to_string(),
|
||||||
|
device_model: Some("Rust".to_string()),
|
||||||
|
device_type: None,
|
||||||
|
token: "server-fingerprint".to_string(),
|
||||||
|
},
|
||||||
|
None,
|
||||||
|
Some(ServerConfigV2 {
|
||||||
|
pin: None,
|
||||||
|
event_tx: v2_event_tx,
|
||||||
|
}),
|
||||||
|
Some(WebConfig {
|
||||||
|
send: None,
|
||||||
|
upload: true,
|
||||||
|
i18n: WebI18n::default(),
|
||||||
|
}),
|
||||||
|
stop_rx,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("Failed to start server");
|
||||||
|
|
||||||
|
wait_until_reachable(port).await;
|
||||||
|
|
||||||
|
let client = localsend::reqwest::Client::new();
|
||||||
|
let base_url = format!("http://127.0.0.1:{port}");
|
||||||
|
|
||||||
|
// The upload page is served at `/` because web send is not active.
|
||||||
|
let response = client.get(&base_url).send().await.unwrap();
|
||||||
|
assert_eq!(response.status().as_u16(), 200);
|
||||||
|
let body = response.text().await.unwrap();
|
||||||
|
assert!(body.contains("LocalSend"));
|
||||||
|
assert!(body.contains("prepare-upload"));
|
||||||
|
|
||||||
|
let response = client
|
||||||
|
.get(format!("{base_url}/i18n.json"))
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(response.status().as_u16(), 200);
|
||||||
|
let i18n = response.json::<HashMap<String, String>>().await.unwrap();
|
||||||
|
assert!(i18n.contains_key("busy"));
|
||||||
|
assert!(i18n.contains_key("uploadRejected"));
|
||||||
|
|
||||||
|
// The download page assets stay disabled without web send.
|
||||||
|
let response = client
|
||||||
|
.get(format!("{base_url}/download.js"))
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(response.status().as_u16(), 403);
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_full_download_flow() {
|
async fn test_full_download_flow() {
|
||||||
let (config, contents, disk_path, disk_content) = web_send_config(None);
|
let (config, contents, disk_path, disk_content) = web_send_config(None);
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
|
|||||||
import 'package:localsend_isolates/rust/api/model.dart';
|
import 'package:localsend_isolates/rust/api/model.dart';
|
||||||
import 'package:localsend_isolates/rust/frb_generated.dart';
|
import 'package:localsend_isolates/rust/frb_generated.dart';
|
||||||
|
|
||||||
// These functions are ignored because they are not marked as `pub`: `rs_stored_device`
|
// These functions are ignored because they are not marked as `pub`: `rs_stored_device`, `stop`
|
||||||
|
// These types are ignored because they are neither used by any `pub` functions nor (for structs and enums) marked `#[frb(unignore)]`: `DiscoveryInstance`
|
||||||
|
|
||||||
/// Starts the discovery: binds the UDP multicast sockets on all usable
|
/// Starts the discovery: binds the UDP multicast sockets on all usable
|
||||||
/// network interfaces, answers announcements of other devices with an HTTP
|
/// network interfaces, answers announcements of other devices with an HTTP
|
||||||
@@ -88,6 +89,9 @@ abstract class RsDiscovery implements RustOpaqueInterface {
|
|||||||
|
|
||||||
/// Emits a [RsStoredDevice] for every device confirmation until the
|
/// Emits a [RsStoredDevice] for every device confirmation until the
|
||||||
/// discovery is stopped. Can only be listened to once.
|
/// discovery is stopped. Can only be listened to once.
|
||||||
|
///
|
||||||
|
/// Also returns when the Dart side of the stream is gone (e.g. after a
|
||||||
|
/// hot restart), so this call does not keep the discovery alive forever.
|
||||||
Stream<RsStoredDevice> listen();
|
Stream<RsStoredDevice> listen();
|
||||||
|
|
||||||
/// The reason the multicast sockets could not be bound, when they could
|
/// The reason the multicast sockets could not be bound, when they could
|
||||||
|
|||||||
@@ -10,13 +10,15 @@ import 'package:localsend_isolates/rust/frb_generated.dart';
|
|||||||
|
|
||||||
part 'server.freezed.dart';
|
part 'server.freezed.dart';
|
||||||
|
|
||||||
// These functions are ignored because they are not marked as `pub`: `handle_server_event`, `handle_web_event`, `recv_opt`, `resolve_file_content`, `resolve_upload_target`
|
// These functions are ignored because they are not marked as `pub`: `handle_server_event`, `handle_web_event`, `recv_opt`, `resolve_file_content`, `resolve_upload_target`, `stop`
|
||||||
|
// These types are ignored because they are neither used by any `pub` functions nor (for structs and enums) marked `#[frb(unignore)]`: `ServerInstance`
|
||||||
|
|
||||||
/// Starts the HTTP server on the given port (IPv4 and IPv6).
|
/// Starts the HTTP server on the given port (IPv4 and IPv6).
|
||||||
/// The server runs until [RsHttpServer::stop] is called.
|
/// The server runs until [RsHttpServer::stop] is called.
|
||||||
///
|
///
|
||||||
/// Passing [web_send] additionally enables the web send (download API) so that
|
/// Passing [web] additionally serves the web pages: the download page when
|
||||||
/// web browsers can download the offered files.
|
/// [WebParams::send] is set (so web browsers can download the offered files)
|
||||||
|
/// or the upload page when [WebParams::upload] is enabled.
|
||||||
///
|
///
|
||||||
/// Passing [show_token] enables the internal `show` endpoint that lets another
|
/// Passing [show_token] enables the internal `show` endpoint that lets another
|
||||||
/// application instance request this one to show itself (emitted as
|
/// application instance request this one to show itself (emitted as
|
||||||
@@ -32,7 +34,7 @@ Future<RsHttpServer> startServer({
|
|||||||
DeviceType? deviceType,
|
DeviceType? deviceType,
|
||||||
required String fingerprint,
|
required String fingerprint,
|
||||||
String? pin,
|
String? pin,
|
||||||
WebSendParams? webSend,
|
WebParams? web,
|
||||||
String? showToken,
|
String? showToken,
|
||||||
}) => RustLib.instance.api.crateApiServerStartServer(
|
}) => RustLib.instance.api.crateApiServerStartServer(
|
||||||
port: port,
|
port: port,
|
||||||
@@ -43,7 +45,7 @@ Future<RsHttpServer> startServer({
|
|||||||
deviceType: deviceType,
|
deviceType: deviceType,
|
||||||
fingerprint: fingerprint,
|
fingerprint: fingerprint,
|
||||||
pin: pin,
|
pin: pin,
|
||||||
webSend: webSend,
|
web: web,
|
||||||
showToken: showToken,
|
showToken: showToken,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -77,6 +79,9 @@ abstract class RsHttpServer implements RustOpaqueInterface {
|
|||||||
///
|
///
|
||||||
/// The v2 protocol, the web send (download API), and the internal endpoint
|
/// The v2 protocol, the web send (download API), and the internal endpoint
|
||||||
/// events are all emitted on the same stream.
|
/// events are all emitted on the same stream.
|
||||||
|
///
|
||||||
|
/// Also returns when the Dart side of the stream is gone (e.g. after a
|
||||||
|
/// hot restart), so this call does not keep the server alive forever.
|
||||||
Stream<RsServerEvent> listen();
|
Stream<RsServerEvent> listen();
|
||||||
|
|
||||||
/// Answers the pending [RsServerEvent::WebFileDownload] event with the source
|
/// Answers the pending [RsServerEvent::WebFileDownload] event with the source
|
||||||
@@ -266,22 +271,26 @@ class TlsConfig {
|
|||||||
identical(this, other) || other is TlsConfig && runtimeType == other.runtimeType && cert == other.cert && privateKey == other.privateKey;
|
identical(this, other) || other is TlsConfig && runtimeType == other.runtimeType && cert == other.cert && privateKey == other.privateKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
class WebSendI18n {
|
class WebI18n {
|
||||||
final String waiting;
|
final String waiting;
|
||||||
final String enterPin;
|
final String enterPin;
|
||||||
final String invalidPin;
|
final String invalidPin;
|
||||||
final String tooManyAttempts;
|
final String tooManyAttempts;
|
||||||
final String rejected;
|
final String rejected;
|
||||||
|
final String uploadRejected;
|
||||||
|
final String busy;
|
||||||
final String files;
|
final String files;
|
||||||
final String fileName;
|
final String fileName;
|
||||||
final String size;
|
final String size;
|
||||||
|
|
||||||
const WebSendI18n({
|
const WebI18n({
|
||||||
required this.waiting,
|
required this.waiting,
|
||||||
required this.enterPin,
|
required this.enterPin,
|
||||||
required this.invalidPin,
|
required this.invalidPin,
|
||||||
required this.tooManyAttempts,
|
required this.tooManyAttempts,
|
||||||
required this.rejected,
|
required this.rejected,
|
||||||
|
required this.uploadRejected,
|
||||||
|
required this.busy,
|
||||||
required this.files,
|
required this.files,
|
||||||
required this.fileName,
|
required this.fileName,
|
||||||
required this.size,
|
required this.size,
|
||||||
@@ -294,6 +303,8 @@ class WebSendI18n {
|
|||||||
invalidPin.hashCode ^
|
invalidPin.hashCode ^
|
||||||
tooManyAttempts.hashCode ^
|
tooManyAttempts.hashCode ^
|
||||||
rejected.hashCode ^
|
rejected.hashCode ^
|
||||||
|
uploadRejected.hashCode ^
|
||||||
|
busy.hashCode ^
|
||||||
files.hashCode ^
|
files.hashCode ^
|
||||||
fileName.hashCode ^
|
fileName.hashCode ^
|
||||||
size.hashCode;
|
size.hashCode;
|
||||||
@@ -301,22 +312,53 @@ class WebSendI18n {
|
|||||||
@override
|
@override
|
||||||
bool operator ==(Object other) =>
|
bool operator ==(Object other) =>
|
||||||
identical(this, other) ||
|
identical(this, other) ||
|
||||||
other is WebSendI18n &&
|
other is WebI18n &&
|
||||||
runtimeType == other.runtimeType &&
|
runtimeType == other.runtimeType &&
|
||||||
waiting == other.waiting &&
|
waiting == other.waiting &&
|
||||||
enterPin == other.enterPin &&
|
enterPin == other.enterPin &&
|
||||||
invalidPin == other.invalidPin &&
|
invalidPin == other.invalidPin &&
|
||||||
tooManyAttempts == other.tooManyAttempts &&
|
tooManyAttempts == other.tooManyAttempts &&
|
||||||
rejected == other.rejected &&
|
rejected == other.rejected &&
|
||||||
|
uploadRejected == other.uploadRejected &&
|
||||||
|
busy == other.busy &&
|
||||||
files == other.files &&
|
files == other.files &&
|
||||||
fileName == other.fileName &&
|
fileName == other.fileName &&
|
||||||
size == other.size;
|
size == other.size;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Configuration for the web pages served to browsers. When omitted, the web
|
||||||
|
/// pages respond with 403 and only the v2 endpoints run.
|
||||||
|
class WebParams {
|
||||||
|
/// Enables web send (the download page): files offered for download by web
|
||||||
|
/// browsers. `null` disables the download page and the download API.
|
||||||
|
final WebSendParams? send;
|
||||||
|
|
||||||
|
/// Serves the upload page so web browsers can upload files via the v2
|
||||||
|
/// `prepare-upload`/`upload` endpoints. Ignored when [WebParams::send] is
|
||||||
|
/// set: the download page takes precedence at `/`.
|
||||||
|
final bool upload;
|
||||||
|
|
||||||
|
/// Translations for the web pages, served via `/i18n.json`.
|
||||||
|
final WebI18n i18N;
|
||||||
|
|
||||||
|
const WebParams({
|
||||||
|
this.send,
|
||||||
|
required this.upload,
|
||||||
|
required this.i18N,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => send.hashCode ^ upload.hashCode ^ i18N.hashCode;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) =>
|
||||||
|
identical(this, other) ||
|
||||||
|
other is WebParams && runtimeType == other.runtimeType && send == other.send && upload == other.upload && i18N == other.i18N;
|
||||||
|
}
|
||||||
|
|
||||||
/// Configuration for web send: files offered for download by web browsers.
|
/// Configuration for web send: files offered for download by web browsers.
|
||||||
///
|
///
|
||||||
/// Web send can be enabled independently of the v2 protocol endpoints. When
|
/// Web send can be enabled independently of the v2 protocol endpoints.
|
||||||
/// omitted, the download API responds with 403 and only the v2 endpoints run.
|
|
||||||
class WebSendParams {
|
class WebSendParams {
|
||||||
/// The metadata of the files offered for download, mapped by file ID.
|
/// The metadata of the files offered for download, mapped by file ID.
|
||||||
/// The content is requested per download via [RsServerEvent::WebFileDownload].
|
/// The content is requested per download via [RsServerEvent::WebFileDownload].
|
||||||
@@ -325,20 +367,15 @@ class WebSendParams {
|
|||||||
/// Optional PIN that web clients must provide via the `pin` query parameter.
|
/// Optional PIN that web clients must provide via the `pin` query parameter.
|
||||||
final String? pin;
|
final String? pin;
|
||||||
|
|
||||||
/// Translations for the web page, served via `/i18n.json`.
|
|
||||||
final WebSendI18n i18N;
|
|
||||||
|
|
||||||
const WebSendParams({
|
const WebSendParams({
|
||||||
required this.files,
|
required this.files,
|
||||||
this.pin,
|
this.pin,
|
||||||
required this.i18N,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode => files.hashCode ^ pin.hashCode ^ i18N.hashCode;
|
int get hashCode => files.hashCode ^ pin.hashCode;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) =>
|
bool operator ==(Object other) =>
|
||||||
identical(this, other) ||
|
identical(this, other) || other is WebSendParams && runtimeType == other.runtimeType && files == other.files && pin == other.pin;
|
||||||
other is WebSendParams && runtimeType == other.runtimeType && files == other.files && pin == other.pin && i18N == other.i18N;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -302,7 +302,7 @@ abstract class RustLibApi extends BaseApi {
|
|||||||
DeviceType? deviceType,
|
DeviceType? deviceType,
|
||||||
required String fingerprint,
|
required String fingerprint,
|
||||||
String? pin,
|
String? pin,
|
||||||
WebSendParams? webSend,
|
WebParams? web,
|
||||||
String? showToken,
|
String? showToken,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -2043,7 +2043,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||||||
DeviceType? deviceType,
|
DeviceType? deviceType,
|
||||||
required String fingerprint,
|
required String fingerprint,
|
||||||
String? pin,
|
String? pin,
|
||||||
WebSendParams? webSend,
|
WebParams? web,
|
||||||
String? showToken,
|
String? showToken,
|
||||||
}) {
|
}) {
|
||||||
return handler.executeNormal(
|
return handler.executeNormal(
|
||||||
@@ -2058,7 +2058,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||||||
sse_encode_opt_box_autoadd_device_type(deviceType, serializer);
|
sse_encode_opt_box_autoadd_device_type(deviceType, serializer);
|
||||||
sse_encode_String(fingerprint, serializer);
|
sse_encode_String(fingerprint, serializer);
|
||||||
sse_encode_opt_String(pin, serializer);
|
sse_encode_opt_String(pin, serializer);
|
||||||
sse_encode_opt_box_autoadd_web_send_params(webSend, serializer);
|
sse_encode_opt_box_autoadd_web_params(web, serializer);
|
||||||
sse_encode_opt_String(showToken, serializer);
|
sse_encode_opt_String(showToken, serializer);
|
||||||
pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 55, port: port_);
|
pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 55, port: port_);
|
||||||
},
|
},
|
||||||
@@ -2067,7 +2067,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||||||
decodeErrorData: sse_decode_AnyhowException,
|
decodeErrorData: sse_decode_AnyhowException,
|
||||||
),
|
),
|
||||||
constMeta: kCrateApiServerStartServerConstMeta,
|
constMeta: kCrateApiServerStartServerConstMeta,
|
||||||
argValues: [port, tls, alias, version, deviceModel, deviceType, fingerprint, pin, webSend, showToken],
|
argValues: [port, tls, alias, version, deviceModel, deviceType, fingerprint, pin, web, showToken],
|
||||||
apiImpl: this,
|
apiImpl: this,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -2075,7 +2075,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||||||
|
|
||||||
TaskConstMeta get kCrateApiServerStartServerConstMeta => const TaskConstMeta(
|
TaskConstMeta get kCrateApiServerStartServerConstMeta => const TaskConstMeta(
|
||||||
debugName: 'start_server',
|
debugName: 'start_server',
|
||||||
argNames: ['port', 'tls', 'alias', 'version', 'deviceModel', 'deviceType', 'fingerprint', 'pin', 'webSend', 'showToken'],
|
argNames: ['port', 'tls', 'alias', 'version', 'deviceModel', 'deviceType', 'fingerprint', 'pin', 'web', 'showToken'],
|
||||||
);
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -2627,6 +2627,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||||||
return raw as int;
|
return raw as int;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@protected
|
||||||
|
WebParams dco_decode_box_autoadd_web_params(dynamic raw) {
|
||||||
|
// Codec=Dco (DartCObject based), see doc to use other codecs
|
||||||
|
return dco_decode_web_params(raw);
|
||||||
|
}
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
WebSendParams dco_decode_box_autoadd_web_send_params(dynamic raw) {
|
WebSendParams dco_decode_box_autoadd_web_send_params(dynamic raw) {
|
||||||
// Codec=Dco (DartCObject based), see doc to use other codecs
|
// Codec=Dco (DartCObject based), see doc to use other codecs
|
||||||
@@ -2864,6 +2870,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||||||
return raw == null ? null : dco_decode_box_autoadd_u_32(raw);
|
return raw == null ? null : dco_decode_box_autoadd_u_32(raw);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@protected
|
||||||
|
WebParams? dco_decode_opt_box_autoadd_web_params(dynamic raw) {
|
||||||
|
// Codec=Dco (DartCObject based), see doc to use other codecs
|
||||||
|
return raw == null ? null : dco_decode_box_autoadd_web_params(raw);
|
||||||
|
}
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
WebSendParams? dco_decode_opt_box_autoadd_web_send_params(dynamic raw) {
|
WebSendParams? dco_decode_opt_box_autoadd_web_send_params(dynamic raw) {
|
||||||
// Codec=Dco (DartCObject based), see doc to use other codecs
|
// Codec=Dco (DartCObject based), see doc to use other codecs
|
||||||
@@ -3315,19 +3327,33 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
WebSendI18n dco_decode_web_send_i_18_n(dynamic raw) {
|
WebI18n dco_decode_web_i_18_n(dynamic raw) {
|
||||||
// Codec=Dco (DartCObject based), see doc to use other codecs
|
// Codec=Dco (DartCObject based), see doc to use other codecs
|
||||||
final arr = raw as List<dynamic>;
|
final arr = raw as List<dynamic>;
|
||||||
if (arr.length != 8) throw Exception('unexpected arr length: expect 8 but see ${arr.length}');
|
if (arr.length != 10) throw Exception('unexpected arr length: expect 10 but see ${arr.length}');
|
||||||
return WebSendI18n(
|
return WebI18n(
|
||||||
waiting: dco_decode_String(arr[0]),
|
waiting: dco_decode_String(arr[0]),
|
||||||
enterPin: dco_decode_String(arr[1]),
|
enterPin: dco_decode_String(arr[1]),
|
||||||
invalidPin: dco_decode_String(arr[2]),
|
invalidPin: dco_decode_String(arr[2]),
|
||||||
tooManyAttempts: dco_decode_String(arr[3]),
|
tooManyAttempts: dco_decode_String(arr[3]),
|
||||||
rejected: dco_decode_String(arr[4]),
|
rejected: dco_decode_String(arr[4]),
|
||||||
files: dco_decode_String(arr[5]),
|
uploadRejected: dco_decode_String(arr[5]),
|
||||||
fileName: dco_decode_String(arr[6]),
|
busy: dco_decode_String(arr[6]),
|
||||||
size: dco_decode_String(arr[7]),
|
files: dco_decode_String(arr[7]),
|
||||||
|
fileName: dco_decode_String(arr[8]),
|
||||||
|
size: dco_decode_String(arr[9]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@protected
|
||||||
|
WebParams dco_decode_web_params(dynamic raw) {
|
||||||
|
// Codec=Dco (DartCObject based), see doc to use other codecs
|
||||||
|
final arr = raw as List<dynamic>;
|
||||||
|
if (arr.length != 3) throw Exception('unexpected arr length: expect 3 but see ${arr.length}');
|
||||||
|
return WebParams(
|
||||||
|
send: dco_decode_opt_box_autoadd_web_send_params(arr[0]),
|
||||||
|
upload: dco_decode_bool(arr[1]),
|
||||||
|
i18N: dco_decode_web_i_18_n(arr[2]),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3335,11 +3361,10 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||||||
WebSendParams dco_decode_web_send_params(dynamic raw) {
|
WebSendParams dco_decode_web_send_params(dynamic raw) {
|
||||||
// Codec=Dco (DartCObject based), see doc to use other codecs
|
// Codec=Dco (DartCObject based), see doc to use other codecs
|
||||||
final arr = raw as List<dynamic>;
|
final arr = raw as List<dynamic>;
|
||||||
if (arr.length != 3) throw Exception('unexpected arr length: expect 3 but see ${arr.length}');
|
if (arr.length != 2) throw Exception('unexpected arr length: expect 2 but see ${arr.length}');
|
||||||
return WebSendParams(
|
return WebSendParams(
|
||||||
files: dco_decode_Map_String_file_dto_None(arr[0]),
|
files: dco_decode_Map_String_file_dto_None(arr[0]),
|
||||||
pin: dco_decode_opt_String(arr[1]),
|
pin: dco_decode_opt_String(arr[1]),
|
||||||
i18N: dco_decode_web_send_i_18_n(arr[2]),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3844,6 +3869,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||||||
return (sse_decode_u_32(deserializer));
|
return (sse_decode_u_32(deserializer));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@protected
|
||||||
|
WebParams sse_decode_box_autoadd_web_params(SseDeserializer deserializer) {
|
||||||
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
return (sse_decode_web_params(deserializer));
|
||||||
|
}
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
WebSendParams sse_decode_box_autoadd_web_send_params(SseDeserializer deserializer) {
|
WebSendParams sse_decode_box_autoadd_web_send_params(SseDeserializer deserializer) {
|
||||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
@@ -4166,6 +4197,17 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@protected
|
||||||
|
WebParams? sse_decode_opt_box_autoadd_web_params(SseDeserializer deserializer) {
|
||||||
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
|
||||||
|
if (sse_decode_bool(deserializer)) {
|
||||||
|
return (sse_decode_box_autoadd_web_params(deserializer));
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
WebSendParams? sse_decode_opt_box_autoadd_web_send_params(SseDeserializer deserializer) {
|
WebSendParams? sse_decode_opt_box_autoadd_web_send_params(SseDeserializer deserializer) {
|
||||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
@@ -4610,35 +4652,47 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
WebSendI18n sse_decode_web_send_i_18_n(SseDeserializer deserializer) {
|
WebI18n sse_decode_web_i_18_n(SseDeserializer deserializer) {
|
||||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
var var_waiting = sse_decode_String(deserializer);
|
var var_waiting = sse_decode_String(deserializer);
|
||||||
var var_enterPin = sse_decode_String(deserializer);
|
var var_enterPin = sse_decode_String(deserializer);
|
||||||
var var_invalidPin = sse_decode_String(deserializer);
|
var var_invalidPin = sse_decode_String(deserializer);
|
||||||
var var_tooManyAttempts = sse_decode_String(deserializer);
|
var var_tooManyAttempts = sse_decode_String(deserializer);
|
||||||
var var_rejected = sse_decode_String(deserializer);
|
var var_rejected = sse_decode_String(deserializer);
|
||||||
|
var var_uploadRejected = sse_decode_String(deserializer);
|
||||||
|
var var_busy = sse_decode_String(deserializer);
|
||||||
var var_files = sse_decode_String(deserializer);
|
var var_files = sse_decode_String(deserializer);
|
||||||
var var_fileName = sse_decode_String(deserializer);
|
var var_fileName = sse_decode_String(deserializer);
|
||||||
var var_size = sse_decode_String(deserializer);
|
var var_size = sse_decode_String(deserializer);
|
||||||
return WebSendI18n(
|
return WebI18n(
|
||||||
waiting: var_waiting,
|
waiting: var_waiting,
|
||||||
enterPin: var_enterPin,
|
enterPin: var_enterPin,
|
||||||
invalidPin: var_invalidPin,
|
invalidPin: var_invalidPin,
|
||||||
tooManyAttempts: var_tooManyAttempts,
|
tooManyAttempts: var_tooManyAttempts,
|
||||||
rejected: var_rejected,
|
rejected: var_rejected,
|
||||||
|
uploadRejected: var_uploadRejected,
|
||||||
|
busy: var_busy,
|
||||||
files: var_files,
|
files: var_files,
|
||||||
fileName: var_fileName,
|
fileName: var_fileName,
|
||||||
size: var_size,
|
size: var_size,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@protected
|
||||||
|
WebParams sse_decode_web_params(SseDeserializer deserializer) {
|
||||||
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
var var_send = sse_decode_opt_box_autoadd_web_send_params(deserializer);
|
||||||
|
var var_upload = sse_decode_bool(deserializer);
|
||||||
|
var var_i18N = sse_decode_web_i_18_n(deserializer);
|
||||||
|
return WebParams(send: var_send, upload: var_upload, i18N: var_i18N);
|
||||||
|
}
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
WebSendParams sse_decode_web_send_params(SseDeserializer deserializer) {
|
WebSendParams sse_decode_web_send_params(SseDeserializer deserializer) {
|
||||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
var var_files = sse_decode_Map_String_file_dto_None(deserializer);
|
var var_files = sse_decode_Map_String_file_dto_None(deserializer);
|
||||||
var var_pin = sse_decode_opt_String(deserializer);
|
var var_pin = sse_decode_opt_String(deserializer);
|
||||||
var var_i18N = sse_decode_web_send_i_18_n(deserializer);
|
return WebSendParams(files: var_files, pin: var_pin);
|
||||||
return WebSendParams(files: var_files, pin: var_pin, i18N: var_i18N);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
@@ -5253,6 +5307,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||||||
sse_encode_u_32(self, serializer);
|
sse_encode_u_32(self, serializer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_box_autoadd_web_params(WebParams self, SseSerializer serializer) {
|
||||||
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
sse_encode_web_params(self, serializer);
|
||||||
|
}
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_box_autoadd_web_send_params(WebSendParams self, SseSerializer serializer) {
|
void sse_encode_box_autoadd_web_send_params(WebSendParams self, SseSerializer serializer) {
|
||||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
@@ -5530,6 +5590,16 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_opt_box_autoadd_web_params(WebParams? self, SseSerializer serializer) {
|
||||||
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
|
||||||
|
sse_encode_bool(self != null, serializer);
|
||||||
|
if (self != null) {
|
||||||
|
sse_encode_box_autoadd_web_params(self, serializer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_opt_box_autoadd_web_send_params(WebSendParams? self, SseSerializer serializer) {
|
void sse_encode_opt_box_autoadd_web_send_params(WebSendParams? self, SseSerializer serializer) {
|
||||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
@@ -5892,24 +5962,33 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_web_send_i_18_n(WebSendI18n self, SseSerializer serializer) {
|
void sse_encode_web_i_18_n(WebI18n self, SseSerializer serializer) {
|
||||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
sse_encode_String(self.waiting, serializer);
|
sse_encode_String(self.waiting, serializer);
|
||||||
sse_encode_String(self.enterPin, serializer);
|
sse_encode_String(self.enterPin, serializer);
|
||||||
sse_encode_String(self.invalidPin, serializer);
|
sse_encode_String(self.invalidPin, serializer);
|
||||||
sse_encode_String(self.tooManyAttempts, serializer);
|
sse_encode_String(self.tooManyAttempts, serializer);
|
||||||
sse_encode_String(self.rejected, serializer);
|
sse_encode_String(self.rejected, serializer);
|
||||||
|
sse_encode_String(self.uploadRejected, serializer);
|
||||||
|
sse_encode_String(self.busy, serializer);
|
||||||
sse_encode_String(self.files, serializer);
|
sse_encode_String(self.files, serializer);
|
||||||
sse_encode_String(self.fileName, serializer);
|
sse_encode_String(self.fileName, serializer);
|
||||||
sse_encode_String(self.size, serializer);
|
sse_encode_String(self.size, serializer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_web_params(WebParams self, SseSerializer serializer) {
|
||||||
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
sse_encode_opt_box_autoadd_web_send_params(self.send, serializer);
|
||||||
|
sse_encode_bool(self.upload, serializer);
|
||||||
|
sse_encode_web_i_18_n(self.i18N, serializer);
|
||||||
|
}
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_web_send_params(WebSendParams self, SseSerializer serializer) {
|
void sse_encode_web_send_params(WebSendParams self, SseSerializer serializer) {
|
||||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
sse_encode_Map_String_file_dto_None(self.files, serializer);
|
sse_encode_Map_String_file_dto_None(self.files, serializer);
|
||||||
sse_encode_opt_String(self.pin, serializer);
|
sse_encode_opt_String(self.pin, serializer);
|
||||||
sse_encode_web_send_i_18_n(self.i18N, serializer);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
@@ -6104,6 +6183,9 @@ class RsDiscoveryImpl extends RustOpaque implements RsDiscovery {
|
|||||||
|
|
||||||
/// Emits a [RsStoredDevice] for every device confirmation until the
|
/// Emits a [RsStoredDevice] for every device confirmation until the
|
||||||
/// discovery is stopped. Can only be listened to once.
|
/// discovery is stopped. Can only be listened to once.
|
||||||
|
///
|
||||||
|
/// Also returns when the Dart side of the stream is gone (e.g. after a
|
||||||
|
/// hot restart), so this call does not keep the discovery alive forever.
|
||||||
Stream<RsStoredDevice> listen() => RustLib.instance.api.crateApiDiscoveryRsDiscoveryListen(
|
Stream<RsStoredDevice> listen() => RustLib.instance.api.crateApiDiscoveryRsDiscoveryListen(
|
||||||
that: this,
|
that: this,
|
||||||
);
|
);
|
||||||
@@ -6259,6 +6341,9 @@ class RsHttpServerImpl extends RustOpaque implements RsHttpServer {
|
|||||||
///
|
///
|
||||||
/// The v2 protocol, the web send (download API), and the internal endpoint
|
/// The v2 protocol, the web send (download API), and the internal endpoint
|
||||||
/// events are all emitted on the same stream.
|
/// events are all emitted on the same stream.
|
||||||
|
///
|
||||||
|
/// Also returns when the Dart side of the stream is gone (e.g. after a
|
||||||
|
/// hot restart), so this call does not keep the server alive forever.
|
||||||
Stream<RsServerEvent> listen() => RustLib.instance.api.crateApiServerRsHttpServerListen(
|
Stream<RsServerEvent> listen() => RustLib.instance.api.crateApiServerRsHttpServerListen(
|
||||||
that: this,
|
that: this,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -279,6 +279,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||||||
@protected
|
@protected
|
||||||
int dco_decode_box_autoadd_u_32(dynamic raw);
|
int dco_decode_box_autoadd_u_32(dynamic raw);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
WebParams dco_decode_box_autoadd_web_params(dynamic raw);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
WebSendParams dco_decode_box_autoadd_web_send_params(dynamic raw);
|
WebSendParams dco_decode_box_autoadd_web_send_params(dynamic raw);
|
||||||
|
|
||||||
@@ -376,6 +379,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||||||
@protected
|
@protected
|
||||||
int? dco_decode_opt_box_autoadd_u_32(dynamic raw);
|
int? dco_decode_opt_box_autoadd_u_32(dynamic raw);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
WebParams? dco_decode_opt_box_autoadd_web_params(dynamic raw);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
WebSendParams? dco_decode_opt_box_autoadd_web_send_params(dynamic raw);
|
WebSendParams? dco_decode_opt_box_autoadd_web_send_params(dynamic raw);
|
||||||
|
|
||||||
@@ -482,7 +488,10 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||||||
BigInt dco_decode_usize(dynamic raw);
|
BigInt dco_decode_usize(dynamic raw);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
WebSendI18n dco_decode_web_send_i_18_n(dynamic raw);
|
WebI18n dco_decode_web_i_18_n(dynamic raw);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
WebParams dco_decode_web_params(dynamic raw);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
WebSendParams dco_decode_web_send_params(dynamic raw);
|
WebSendParams dco_decode_web_send_params(dynamic raw);
|
||||||
@@ -728,6 +737,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||||||
@protected
|
@protected
|
||||||
int sse_decode_box_autoadd_u_32(SseDeserializer deserializer);
|
int sse_decode_box_autoadd_u_32(SseDeserializer deserializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
WebParams sse_decode_box_autoadd_web_params(SseDeserializer deserializer);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
WebSendParams sse_decode_box_autoadd_web_send_params(SseDeserializer deserializer);
|
WebSendParams sse_decode_box_autoadd_web_send_params(SseDeserializer deserializer);
|
||||||
|
|
||||||
@@ -827,6 +839,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||||||
@protected
|
@protected
|
||||||
int? sse_decode_opt_box_autoadd_u_32(SseDeserializer deserializer);
|
int? sse_decode_opt_box_autoadd_u_32(SseDeserializer deserializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
WebParams? sse_decode_opt_box_autoadd_web_params(SseDeserializer deserializer);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
WebSendParams? sse_decode_opt_box_autoadd_web_send_params(SseDeserializer deserializer);
|
WebSendParams? sse_decode_opt_box_autoadd_web_send_params(SseDeserializer deserializer);
|
||||||
|
|
||||||
@@ -933,7 +948,10 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||||||
BigInt sse_decode_usize(SseDeserializer deserializer);
|
BigInt sse_decode_usize(SseDeserializer deserializer);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
WebSendI18n sse_decode_web_send_i_18_n(SseDeserializer deserializer);
|
WebI18n sse_decode_web_i_18_n(SseDeserializer deserializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
WebParams sse_decode_web_params(SseDeserializer deserializer);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
WebSendParams sse_decode_web_send_params(SseDeserializer deserializer);
|
WebSendParams sse_decode_web_send_params(SseDeserializer deserializer);
|
||||||
@@ -1224,6 +1242,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||||||
@protected
|
@protected
|
||||||
void sse_encode_box_autoadd_u_32(int self, SseSerializer serializer);
|
void sse_encode_box_autoadd_u_32(int self, SseSerializer serializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_box_autoadd_web_params(WebParams self, SseSerializer serializer);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_box_autoadd_web_send_params(WebSendParams self, SseSerializer serializer);
|
void sse_encode_box_autoadd_web_send_params(WebSendParams self, SseSerializer serializer);
|
||||||
|
|
||||||
@@ -1323,6 +1344,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||||||
@protected
|
@protected
|
||||||
void sse_encode_opt_box_autoadd_u_32(int? self, SseSerializer serializer);
|
void sse_encode_opt_box_autoadd_u_32(int? self, SseSerializer serializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_opt_box_autoadd_web_params(WebParams? self, SseSerializer serializer);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_opt_box_autoadd_web_send_params(WebSendParams? self, SseSerializer serializer);
|
void sse_encode_opt_box_autoadd_web_send_params(WebSendParams? self, SseSerializer serializer);
|
||||||
|
|
||||||
@@ -1430,7 +1454,10 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||||||
void sse_encode_usize(BigInt self, SseSerializer serializer);
|
void sse_encode_usize(BigInt self, SseSerializer serializer);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_web_send_i_18_n(WebSendI18n self, SseSerializer serializer);
|
void sse_encode_web_i_18_n(WebI18n self, SseSerializer serializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_web_params(WebParams self, SseSerializer serializer);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_web_send_params(WebSendParams self, SseSerializer serializer);
|
void sse_encode_web_send_params(WebSendParams self, SseSerializer serializer);
|
||||||
|
|||||||
@@ -281,6 +281,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||||||
@protected
|
@protected
|
||||||
int dco_decode_box_autoadd_u_32(dynamic raw);
|
int dco_decode_box_autoadd_u_32(dynamic raw);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
WebParams dco_decode_box_autoadd_web_params(dynamic raw);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
WebSendParams dco_decode_box_autoadd_web_send_params(dynamic raw);
|
WebSendParams dco_decode_box_autoadd_web_send_params(dynamic raw);
|
||||||
|
|
||||||
@@ -378,6 +381,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||||||
@protected
|
@protected
|
||||||
int? dco_decode_opt_box_autoadd_u_32(dynamic raw);
|
int? dco_decode_opt_box_autoadd_u_32(dynamic raw);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
WebParams? dco_decode_opt_box_autoadd_web_params(dynamic raw);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
WebSendParams? dco_decode_opt_box_autoadd_web_send_params(dynamic raw);
|
WebSendParams? dco_decode_opt_box_autoadd_web_send_params(dynamic raw);
|
||||||
|
|
||||||
@@ -484,7 +490,10 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||||||
BigInt dco_decode_usize(dynamic raw);
|
BigInt dco_decode_usize(dynamic raw);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
WebSendI18n dco_decode_web_send_i_18_n(dynamic raw);
|
WebI18n dco_decode_web_i_18_n(dynamic raw);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
WebParams dco_decode_web_params(dynamic raw);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
WebSendParams dco_decode_web_send_params(dynamic raw);
|
WebSendParams dco_decode_web_send_params(dynamic raw);
|
||||||
@@ -730,6 +739,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||||||
@protected
|
@protected
|
||||||
int sse_decode_box_autoadd_u_32(SseDeserializer deserializer);
|
int sse_decode_box_autoadd_u_32(SseDeserializer deserializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
WebParams sse_decode_box_autoadd_web_params(SseDeserializer deserializer);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
WebSendParams sse_decode_box_autoadd_web_send_params(SseDeserializer deserializer);
|
WebSendParams sse_decode_box_autoadd_web_send_params(SseDeserializer deserializer);
|
||||||
|
|
||||||
@@ -829,6 +841,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||||||
@protected
|
@protected
|
||||||
int? sse_decode_opt_box_autoadd_u_32(SseDeserializer deserializer);
|
int? sse_decode_opt_box_autoadd_u_32(SseDeserializer deserializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
WebParams? sse_decode_opt_box_autoadd_web_params(SseDeserializer deserializer);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
WebSendParams? sse_decode_opt_box_autoadd_web_send_params(SseDeserializer deserializer);
|
WebSendParams? sse_decode_opt_box_autoadd_web_send_params(SseDeserializer deserializer);
|
||||||
|
|
||||||
@@ -935,7 +950,10 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||||||
BigInt sse_decode_usize(SseDeserializer deserializer);
|
BigInt sse_decode_usize(SseDeserializer deserializer);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
WebSendI18n sse_decode_web_send_i_18_n(SseDeserializer deserializer);
|
WebI18n sse_decode_web_i_18_n(SseDeserializer deserializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
WebParams sse_decode_web_params(SseDeserializer deserializer);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
WebSendParams sse_decode_web_send_params(SseDeserializer deserializer);
|
WebSendParams sse_decode_web_send_params(SseDeserializer deserializer);
|
||||||
@@ -1226,6 +1244,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||||||
@protected
|
@protected
|
||||||
void sse_encode_box_autoadd_u_32(int self, SseSerializer serializer);
|
void sse_encode_box_autoadd_u_32(int self, SseSerializer serializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_box_autoadd_web_params(WebParams self, SseSerializer serializer);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_box_autoadd_web_send_params(WebSendParams self, SseSerializer serializer);
|
void sse_encode_box_autoadd_web_send_params(WebSendParams self, SseSerializer serializer);
|
||||||
|
|
||||||
@@ -1325,6 +1346,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||||||
@protected
|
@protected
|
||||||
void sse_encode_opt_box_autoadd_u_32(int? self, SseSerializer serializer);
|
void sse_encode_opt_box_autoadd_u_32(int? self, SseSerializer serializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_opt_box_autoadd_web_params(WebParams? self, SseSerializer serializer);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_opt_box_autoadd_web_send_params(WebSendParams? self, SseSerializer serializer);
|
void sse_encode_opt_box_autoadd_web_send_params(WebSendParams? self, SseSerializer serializer);
|
||||||
|
|
||||||
@@ -1432,7 +1456,10 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||||||
void sse_encode_usize(BigInt self, SseSerializer serializer);
|
void sse_encode_usize(BigInt self, SseSerializer serializer);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_web_send_i_18_n(WebSendI18n self, SseSerializer serializer);
|
void sse_encode_web_i_18_n(WebI18n self, SseSerializer serializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_web_params(WebParams self, SseSerializer serializer);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_web_send_params(WebSendParams self, SseSerializer serializer);
|
void sse_encode_web_send_params(WebSendParams self, SseSerializer serializer);
|
||||||
|
|||||||
@@ -30,9 +30,9 @@ class HttpServerStartTask implements BaseHttpServerTask {
|
|||||||
/// Optional PIN that senders must provide to start an upload session.
|
/// Optional PIN that senders must provide to start an upload session.
|
||||||
final String? pin;
|
final String? pin;
|
||||||
|
|
||||||
/// Enables web send (download API) so web browsers can download the offered files.
|
/// Serves the web pages: the download page (web send) and/or the upload page.
|
||||||
/// `null` disables web send.
|
/// `null` disables the web pages.
|
||||||
final WebSendParams? webSend;
|
final WebParams? web;
|
||||||
|
|
||||||
/// Enables the internal `show` endpoint, guarded by this token, that lets another
|
/// Enables the internal `show` endpoint, guarded by this token, that lets another
|
||||||
/// application instance request this one to show itself. `null` disables it.
|
/// application instance request this one to show itself. `null` disables it.
|
||||||
@@ -40,7 +40,7 @@ class HttpServerStartTask implements BaseHttpServerTask {
|
|||||||
|
|
||||||
HttpServerStartTask({
|
HttpServerStartTask({
|
||||||
required this.pin,
|
required this.pin,
|
||||||
required this.webSend,
|
required this.web,
|
||||||
required this.showToken,
|
required this.showToken,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -398,7 +398,7 @@ Future<void> setupHttpServerIsolate(
|
|||||||
deviceType: syncState.deviceInfo.deviceType.toRust(),
|
deviceType: syncState.deviceInfo.deviceType.toRust(),
|
||||||
fingerprint: syncState.securityContext.certificateHash,
|
fingerprint: syncState.securityContext.certificateHash,
|
||||||
pin: startTask.pin,
|
pin: startTask.pin,
|
||||||
webSend: startTask.webSend,
|
web: startTask.web,
|
||||||
showToken: startTask.showToken,
|
showToken: startTask.showToken,
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:localsend_isolates/model/device.dart';
|
import 'package:localsend_isolates/model/device.dart';
|
||||||
import 'package:localsend_isolates/rust/api/server.dart' show WebSendParams;
|
import 'package:localsend_isolates/rust/api/server.dart' show WebParams;
|
||||||
import 'package:localsend_isolates/src/isolate/child/discovery_isolate.dart';
|
import 'package:localsend_isolates/src/isolate/child/discovery_isolate.dart';
|
||||||
import 'package:localsend_isolates/src/isolate/child/server_isolate.dart';
|
import 'package:localsend_isolates/src/isolate/child/server_isolate.dart';
|
||||||
import 'package:localsend_isolates/src/isolate/child/upload_isolate.dart';
|
import 'package:localsend_isolates/src/isolate/child/upload_isolate.dart';
|
||||||
@@ -256,9 +256,9 @@ class IsolateHttpUploadCancelAction extends ReduxAction<IsolateController, Paren
|
|||||||
class IsolateHttpServerStartAction extends ReduxActionWithResult<IsolateController, ParentIsolateState, Stream<HttpServerEvent>> {
|
class IsolateHttpServerStartAction extends ReduxActionWithResult<IsolateController, ParentIsolateState, Stream<HttpServerEvent>> {
|
||||||
final String? pin;
|
final String? pin;
|
||||||
|
|
||||||
/// Enables web send (download API) so web browsers can download the offered files.
|
/// Serves the web pages: the download page (web send) and/or the upload page.
|
||||||
/// `null` disables web send.
|
/// `null` disables the web pages.
|
||||||
final WebSendParams? webSend;
|
final WebParams? web;
|
||||||
|
|
||||||
/// Enables the internal `show` endpoint, guarded by this token, that lets another
|
/// Enables the internal `show` endpoint, guarded by this token, that lets another
|
||||||
/// application instance request this one to show itself. `null` disables it.
|
/// application instance request this one to show itself. `null` disables it.
|
||||||
@@ -266,7 +266,7 @@ class IsolateHttpServerStartAction extends ReduxActionWithResult<IsolateControll
|
|||||||
|
|
||||||
IsolateHttpServerStartAction({
|
IsolateHttpServerStartAction({
|
||||||
required this.pin,
|
required this.pin,
|
||||||
required this.webSend,
|
required this.web,
|
||||||
required this.showToken,
|
required this.showToken,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -282,7 +282,7 @@ class IsolateHttpServerStartAction extends ReduxActionWithResult<IsolateControll
|
|||||||
connection.sendWrappedTaskAndListenStream(
|
connection.sendWrappedTaskAndListenStream(
|
||||||
task: HttpServerStartTask(
|
task: HttpServerStartTask(
|
||||||
pin: pin,
|
pin: pin,
|
||||||
webSend: webSend,
|
web: web,
|
||||||
showToken: showToken,
|
showToken: showToken,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ class HttpServerService {
|
|||||||
required DeviceType? deviceType,
|
required DeviceType? deviceType,
|
||||||
required String fingerprint,
|
required String fingerprint,
|
||||||
required String? pin,
|
required String? pin,
|
||||||
required WebSendParams? webSend,
|
required WebParams? web,
|
||||||
required String? showToken,
|
required String? showToken,
|
||||||
}) async {
|
}) async {
|
||||||
if (_server != null) {
|
if (_server != null) {
|
||||||
@@ -38,7 +38,7 @@ class HttpServerService {
|
|||||||
deviceType: deviceType,
|
deviceType: deviceType,
|
||||||
fingerprint: fingerprint,
|
fingerprint: fingerprint,
|
||||||
pin: pin,
|
pin: pin,
|
||||||
webSend: webSend,
|
web: web,
|
||||||
showToken: showToken,
|
showToken: showToken,
|
||||||
);
|
);
|
||||||
_server = server;
|
_server = server;
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ dependencies:
|
|||||||
mime: 2.0.0
|
mime: 2.0.0
|
||||||
path: 1.9.1
|
path: 1.9.1
|
||||||
pool: 1.5.2
|
pool: 1.5.2
|
||||||
refena_flutter: 3.2.1
|
refena_flutter: ^3.4.0
|
||||||
rust_lib_localsend_app:
|
rust_lib_localsend_app:
|
||||||
path: rust_builder
|
path: rust_builder
|
||||||
typed_isolates:
|
typed_isolates:
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ use localsend::http::server::common::save::FileUploadTarget;
|
|||||||
use localsend::http::server::internal::{InternalConfig, InternalEvent};
|
use localsend::http::server::internal::{InternalConfig, InternalEvent};
|
||||||
pub use localsend::http::server::v2::SessionEndReasonV2;
|
pub use localsend::http::server::v2::SessionEndReasonV2;
|
||||||
use localsend::http::server::v2::{PrepareUploadDecisionV2, ServerEventV2};
|
use localsend::http::server::v2::{PrepareUploadDecisionV2, ServerEventV2};
|
||||||
pub use localsend::http::server::web::WebSendI18n;
|
pub use localsend::http::server::web::WebI18n;
|
||||||
use localsend::http::server::web::{WebSendConfig, WebSendEvent};
|
use localsend::http::server::web::{WebConfig, WebSendConfig, WebSendEvent};
|
||||||
use localsend::http::state::ClientInfo;
|
use localsend::http::state::ClientInfo;
|
||||||
use localsend::model::discovery::DeviceType;
|
use localsend::model::discovery::DeviceType;
|
||||||
use localsend::model::discovery::ProtocolType;
|
use localsend::model::discovery::ProtocolType;
|
||||||
@@ -134,10 +134,25 @@ impl ServerInstance {
|
|||||||
/// binding again.
|
/// binding again.
|
||||||
static RUNNING_SERVER: Mutex<Option<Arc<ServerInstance>>> = Mutex::const_new(None);
|
static RUNNING_SERVER: Mutex<Option<Arc<ServerInstance>>> = Mutex::const_new(None);
|
||||||
|
|
||||||
|
/// Configuration for the web pages served to browsers. When omitted, the web
|
||||||
|
/// pages respond with 403 and only the v2 endpoints run.
|
||||||
|
pub struct WebParams {
|
||||||
|
/// Enables web send (the download page): files offered for download by web
|
||||||
|
/// browsers. `null` disables the download page and the download API.
|
||||||
|
pub send: Option<WebSendParams>,
|
||||||
|
|
||||||
|
/// Serves the upload page so web browsers can upload files via the v2
|
||||||
|
/// `prepare-upload`/`upload` endpoints. Ignored when [WebParams::send] is
|
||||||
|
/// set: the download page takes precedence at `/`.
|
||||||
|
pub upload: bool,
|
||||||
|
|
||||||
|
/// Translations for the web pages, served via `/i18n.json`.
|
||||||
|
pub i18n: WebI18n,
|
||||||
|
}
|
||||||
|
|
||||||
/// Configuration for web send: files offered for download by web browsers.
|
/// Configuration for web send: files offered for download by web browsers.
|
||||||
///
|
///
|
||||||
/// Web send can be enabled independently of the v2 protocol endpoints. When
|
/// Web send can be enabled independently of the v2 protocol endpoints.
|
||||||
/// omitted, the download API responds with 403 and only the v2 endpoints run.
|
|
||||||
pub struct WebSendParams {
|
pub struct WebSendParams {
|
||||||
/// The metadata of the files offered for download, mapped by file ID.
|
/// The metadata of the files offered for download, mapped by file ID.
|
||||||
/// The content is requested per download via [RsServerEvent::WebFileDownload].
|
/// The content is requested per download via [RsServerEvent::WebFileDownload].
|
||||||
@@ -145,16 +160,14 @@ pub struct WebSendParams {
|
|||||||
|
|
||||||
/// Optional PIN that web clients must provide via the `pin` query parameter.
|
/// Optional PIN that web clients must provide via the `pin` query parameter.
|
||||||
pub pin: Option<String>,
|
pub pin: Option<String>,
|
||||||
|
|
||||||
/// Translations for the web page, served via `/i18n.json`.
|
|
||||||
pub i18n: WebSendI18n,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Starts the HTTP server on the given port (IPv4 and IPv6).
|
/// Starts the HTTP server on the given port (IPv4 and IPv6).
|
||||||
/// The server runs until [RsHttpServer::stop] is called.
|
/// The server runs until [RsHttpServer::stop] is called.
|
||||||
///
|
///
|
||||||
/// Passing [web_send] additionally enables the web send (download API) so that
|
/// Passing [web] additionally serves the web pages: the download page when
|
||||||
/// web browsers can download the offered files.
|
/// [WebParams::send] is set (so web browsers can download the offered files)
|
||||||
|
/// or the upload page when [WebParams::upload] is enabled.
|
||||||
///
|
///
|
||||||
/// Passing [show_token] enables the internal `show` endpoint that lets another
|
/// Passing [show_token] enables the internal `show` endpoint that lets another
|
||||||
/// application instance request this one to show itself (emitted as
|
/// application instance request this one to show itself (emitted as
|
||||||
@@ -170,7 +183,7 @@ pub async fn start_server(
|
|||||||
device_type: Option<DeviceType>,
|
device_type: Option<DeviceType>,
|
||||||
fingerprint: String,
|
fingerprint: String,
|
||||||
pin: Option<String>,
|
pin: Option<String>,
|
||||||
web_send: Option<WebSendParams>,
|
web: Option<WebParams>,
|
||||||
show_token: Option<String>,
|
show_token: Option<String>,
|
||||||
) -> anyhow::Result<RsHttpServer> {
|
) -> anyhow::Result<RsHttpServer> {
|
||||||
// Stop a server left over from before a hot restart (its Dart owner died
|
// Stop a server left over from before a hot restart (its Dart owner died
|
||||||
@@ -183,19 +196,29 @@ pub async fn start_server(
|
|||||||
let (event_tx, event_rx) = mpsc::channel::<ServerEventV2>(16);
|
let (event_tx, event_rx) = mpsc::channel::<ServerEventV2>(16);
|
||||||
let (stop_tx, stop_rx) = oneshot::channel::<()>();
|
let (stop_tx, stop_rx) = oneshot::channel::<()>();
|
||||||
|
|
||||||
let (web_send_config, web_event_rx) = match web_send {
|
let (web_config, web_event_rx) = match web {
|
||||||
Some(web_send) => {
|
Some(web) => {
|
||||||
|
let (send_config, web_event_rx) = match web.send {
|
||||||
|
Some(send) => {
|
||||||
let (web_event_tx, web_event_rx) = mpsc::channel::<WebSendEvent>(16);
|
let (web_event_tx, web_event_rx) = mpsc::channel::<WebSendEvent>(16);
|
||||||
let config = WebSendConfig {
|
let config = WebSendConfig {
|
||||||
files: web_send.files,
|
files: send.files,
|
||||||
pin: web_send.pin,
|
pin: send.pin,
|
||||||
i18n: web_send.i18n,
|
|
||||||
event_tx: web_event_tx,
|
event_tx: web_event_tx,
|
||||||
};
|
};
|
||||||
(Some(config), Some(web_event_rx))
|
(Some(config), Some(web_event_rx))
|
||||||
}
|
}
|
||||||
None => (None, None),
|
None => (None, None),
|
||||||
};
|
};
|
||||||
|
let config = WebConfig {
|
||||||
|
send: send_config,
|
||||||
|
upload: web.upload,
|
||||||
|
i18n: web.i18n,
|
||||||
|
};
|
||||||
|
(Some(config), web_event_rx)
|
||||||
|
}
|
||||||
|
None => (None, None),
|
||||||
|
};
|
||||||
|
|
||||||
let (internal_config, internal_event_rx) = match show_token {
|
let (internal_config, internal_event_rx) = match show_token {
|
||||||
Some(show_token) => {
|
Some(show_token) => {
|
||||||
@@ -221,7 +244,7 @@ pub async fn start_server(
|
|||||||
},
|
},
|
||||||
internal_config,
|
internal_config,
|
||||||
Some(ServerConfigV2 { pin, event_tx }),
|
Some(ServerConfigV2 { pin, event_tx }),
|
||||||
web_send_config,
|
web_config,
|
||||||
stop_rx,
|
stop_rx,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
@@ -698,13 +721,15 @@ fn resolve_file_content(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[frb(mirror(WebSendI18n))]
|
#[frb(mirror(WebI18n))]
|
||||||
pub struct _WebSendI18n {
|
pub struct _WebI18n {
|
||||||
pub waiting: String,
|
pub waiting: String,
|
||||||
pub enter_pin: String,
|
pub enter_pin: String,
|
||||||
pub invalid_pin: String,
|
pub invalid_pin: String,
|
||||||
pub too_many_attempts: String,
|
pub too_many_attempts: String,
|
||||||
pub rejected: String,
|
pub rejected: String,
|
||||||
|
pub upload_rejected: String,
|
||||||
|
pub busy: String,
|
||||||
pub files: String,
|
pub files: String,
|
||||||
pub file_name: String,
|
pub file_name: String,
|
||||||
pub size: String,
|
pub size: String,
|
||||||
|
|||||||
@@ -3201,8 +3201,7 @@ fn wire__crate__api__server__start_server_impl(
|
|||||||
<Option<crate::api::model::DeviceType>>::sse_decode(&mut deserializer);
|
<Option<crate::api::model::DeviceType>>::sse_decode(&mut deserializer);
|
||||||
let api_fingerprint = <String>::sse_decode(&mut deserializer);
|
let api_fingerprint = <String>::sse_decode(&mut deserializer);
|
||||||
let api_pin = <Option<String>>::sse_decode(&mut deserializer);
|
let api_pin = <Option<String>>::sse_decode(&mut deserializer);
|
||||||
let api_web_send =
|
let api_web = <Option<crate::api::server::WebParams>>::sse_decode(&mut deserializer);
|
||||||
<Option<crate::api::server::WebSendParams>>::sse_decode(&mut deserializer);
|
|
||||||
let api_show_token = <Option<String>>::sse_decode(&mut deserializer);
|
let api_show_token = <Option<String>>::sse_decode(&mut deserializer);
|
||||||
deserializer.end();
|
deserializer.end();
|
||||||
move |context| async move {
|
move |context| async move {
|
||||||
@@ -3217,7 +3216,7 @@ fn wire__crate__api__server__start_server_impl(
|
|||||||
api_device_type,
|
api_device_type,
|
||||||
api_fingerprint,
|
api_fingerprint,
|
||||||
api_pin,
|
api_pin,
|
||||||
api_web_send,
|
api_web,
|
||||||
api_show_token,
|
api_show_token,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
@@ -3383,15 +3382,17 @@ const _: fn() = || {
|
|||||||
let _: String = TlsConfig.private_key;
|
let _: String = TlsConfig.private_key;
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
let WebSendI18n = None::<crate::api::server::WebSendI18n>.unwrap();
|
let WebI18n = None::<crate::api::server::WebI18n>.unwrap();
|
||||||
let _: String = WebSendI18n.waiting;
|
let _: String = WebI18n.waiting;
|
||||||
let _: String = WebSendI18n.enter_pin;
|
let _: String = WebI18n.enter_pin;
|
||||||
let _: String = WebSendI18n.invalid_pin;
|
let _: String = WebI18n.invalid_pin;
|
||||||
let _: String = WebSendI18n.too_many_attempts;
|
let _: String = WebI18n.too_many_attempts;
|
||||||
let _: String = WebSendI18n.rejected;
|
let _: String = WebI18n.rejected;
|
||||||
let _: String = WebSendI18n.files;
|
let _: String = WebI18n.upload_rejected;
|
||||||
let _: String = WebSendI18n.file_name;
|
let _: String = WebI18n.busy;
|
||||||
let _: String = WebSendI18n.size;
|
let _: String = WebI18n.files;
|
||||||
|
let _: String = WebI18n.file_name;
|
||||||
|
let _: String = WebI18n.size;
|
||||||
}
|
}
|
||||||
match None::<crate::api::webrtc::WsServerMessage>.unwrap() {
|
match None::<crate::api::webrtc::WsServerMessage>.unwrap() {
|
||||||
crate::api::webrtc::WsServerMessage::Hello { client, peers } => {
|
crate::api::webrtc::WsServerMessage::Hello { client, peers } => {
|
||||||
@@ -4234,6 +4235,17 @@ impl SseDecode for Option<u32> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl SseDecode for Option<crate::api::server::WebParams> {
|
||||||
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
|
||||||
|
if (<bool>::sse_decode(deserializer)) {
|
||||||
|
return Some(<crate::api::server::WebParams>::sse_decode(deserializer));
|
||||||
|
} else {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl SseDecode for Option<crate::api::server::WebSendParams> {
|
impl SseDecode for Option<crate::api::server::WebSendParams> {
|
||||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
|
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
|
||||||
@@ -4812,7 +4824,7 @@ impl SseDecode for usize {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SseDecode for crate::api::server::WebSendI18n {
|
impl SseDecode for crate::api::server::WebI18n {
|
||||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
|
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
|
||||||
let mut var_waiting = <String>::sse_decode(deserializer);
|
let mut var_waiting = <String>::sse_decode(deserializer);
|
||||||
@@ -4820,15 +4832,19 @@ impl SseDecode for crate::api::server::WebSendI18n {
|
|||||||
let mut var_invalidPin = <String>::sse_decode(deserializer);
|
let mut var_invalidPin = <String>::sse_decode(deserializer);
|
||||||
let mut var_tooManyAttempts = <String>::sse_decode(deserializer);
|
let mut var_tooManyAttempts = <String>::sse_decode(deserializer);
|
||||||
let mut var_rejected = <String>::sse_decode(deserializer);
|
let mut var_rejected = <String>::sse_decode(deserializer);
|
||||||
|
let mut var_uploadRejected = <String>::sse_decode(deserializer);
|
||||||
|
let mut var_busy = <String>::sse_decode(deserializer);
|
||||||
let mut var_files = <String>::sse_decode(deserializer);
|
let mut var_files = <String>::sse_decode(deserializer);
|
||||||
let mut var_fileName = <String>::sse_decode(deserializer);
|
let mut var_fileName = <String>::sse_decode(deserializer);
|
||||||
let mut var_size = <String>::sse_decode(deserializer);
|
let mut var_size = <String>::sse_decode(deserializer);
|
||||||
return crate::api::server::WebSendI18n {
|
return crate::api::server::WebI18n {
|
||||||
waiting: var_waiting,
|
waiting: var_waiting,
|
||||||
enter_pin: var_enterPin,
|
enter_pin: var_enterPin,
|
||||||
invalid_pin: var_invalidPin,
|
invalid_pin: var_invalidPin,
|
||||||
too_many_attempts: var_tooManyAttempts,
|
too_many_attempts: var_tooManyAttempts,
|
||||||
rejected: var_rejected,
|
rejected: var_rejected,
|
||||||
|
upload_rejected: var_uploadRejected,
|
||||||
|
busy: var_busy,
|
||||||
files: var_files,
|
files: var_files,
|
||||||
file_name: var_fileName,
|
file_name: var_fileName,
|
||||||
size: var_size,
|
size: var_size,
|
||||||
@@ -4836,6 +4852,20 @@ impl SseDecode for crate::api::server::WebSendI18n {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl SseDecode for crate::api::server::WebParams {
|
||||||
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
|
||||||
|
let mut var_send = <Option<crate::api::server::WebSendParams>>::sse_decode(deserializer);
|
||||||
|
let mut var_upload = <bool>::sse_decode(deserializer);
|
||||||
|
let mut var_i18N = <crate::api::server::WebI18n>::sse_decode(deserializer);
|
||||||
|
return crate::api::server::WebParams {
|
||||||
|
send: var_send,
|
||||||
|
upload: var_upload,
|
||||||
|
i18n: var_i18N,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl SseDecode for crate::api::server::WebSendParams {
|
impl SseDecode for crate::api::server::WebSendParams {
|
||||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
|
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
|
||||||
@@ -4844,11 +4874,9 @@ impl SseDecode for crate::api::server::WebSendParams {
|
|||||||
deserializer,
|
deserializer,
|
||||||
);
|
);
|
||||||
let mut var_pin = <Option<String>>::sse_decode(deserializer);
|
let mut var_pin = <Option<String>>::sse_decode(deserializer);
|
||||||
let mut var_i18N = <crate::api::server::WebSendI18n>::sse_decode(deserializer);
|
|
||||||
return crate::api::server::WebSendParams {
|
return crate::api::server::WebSendParams {
|
||||||
files: var_files,
|
files: var_files,
|
||||||
pin: var_pin,
|
pin: var_pin,
|
||||||
i18n: var_i18N,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6139,7 +6167,7 @@ impl flutter_rust_bridge::IntoIntoDart<FrbWrapper<crate::api::server::TlsConfig>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Codec=Dco (DartCObject based), see doc to use other codecs
|
// Codec=Dco (DartCObject based), see doc to use other codecs
|
||||||
impl flutter_rust_bridge::IntoDart for FrbWrapper<crate::api::server::WebSendI18n> {
|
impl flutter_rust_bridge::IntoDart for FrbWrapper<crate::api::server::WebI18n> {
|
||||||
fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi {
|
fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi {
|
||||||
[
|
[
|
||||||
self.0.waiting.into_into_dart().into_dart(),
|
self.0.waiting.into_into_dart().into_dart(),
|
||||||
@@ -6147,6 +6175,8 @@ impl flutter_rust_bridge::IntoDart for FrbWrapper<crate::api::server::WebSendI18
|
|||||||
self.0.invalid_pin.into_into_dart().into_dart(),
|
self.0.invalid_pin.into_into_dart().into_dart(),
|
||||||
self.0.too_many_attempts.into_into_dart().into_dart(),
|
self.0.too_many_attempts.into_into_dart().into_dart(),
|
||||||
self.0.rejected.into_into_dart().into_dart(),
|
self.0.rejected.into_into_dart().into_dart(),
|
||||||
|
self.0.upload_rejected.into_into_dart().into_dart(),
|
||||||
|
self.0.busy.into_into_dart().into_dart(),
|
||||||
self.0.files.into_into_dart().into_dart(),
|
self.0.files.into_into_dart().into_dart(),
|
||||||
self.0.file_name.into_into_dart().into_dart(),
|
self.0.file_name.into_into_dart().into_dart(),
|
||||||
self.0.size.into_into_dart().into_dart(),
|
self.0.size.into_into_dart().into_dart(),
|
||||||
@@ -6155,23 +6185,41 @@ impl flutter_rust_bridge::IntoDart for FrbWrapper<crate::api::server::WebSendI18
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive
|
impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive
|
||||||
for FrbWrapper<crate::api::server::WebSendI18n>
|
for FrbWrapper<crate::api::server::WebI18n>
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
impl flutter_rust_bridge::IntoIntoDart<FrbWrapper<crate::api::server::WebSendI18n>>
|
impl flutter_rust_bridge::IntoIntoDart<FrbWrapper<crate::api::server::WebI18n>>
|
||||||
for crate::api::server::WebSendI18n
|
for crate::api::server::WebI18n
|
||||||
{
|
{
|
||||||
fn into_into_dart(self) -> FrbWrapper<crate::api::server::WebSendI18n> {
|
fn into_into_dart(self) -> FrbWrapper<crate::api::server::WebI18n> {
|
||||||
self.into()
|
self.into()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Codec=Dco (DartCObject based), see doc to use other codecs
|
// Codec=Dco (DartCObject based), see doc to use other codecs
|
||||||
|
impl flutter_rust_bridge::IntoDart for crate::api::server::WebParams {
|
||||||
|
fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi {
|
||||||
|
[
|
||||||
|
self.send.into_into_dart().into_dart(),
|
||||||
|
self.upload.into_into_dart().into_dart(),
|
||||||
|
self.i18n.into_into_dart().into_dart(),
|
||||||
|
]
|
||||||
|
.into_dart()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive for crate::api::server::WebParams {}
|
||||||
|
impl flutter_rust_bridge::IntoIntoDart<crate::api::server::WebParams>
|
||||||
|
for crate::api::server::WebParams
|
||||||
|
{
|
||||||
|
fn into_into_dart(self) -> crate::api::server::WebParams {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Codec=Dco (DartCObject based), see doc to use other codecs
|
||||||
impl flutter_rust_bridge::IntoDart for crate::api::server::WebSendParams {
|
impl flutter_rust_bridge::IntoDart for crate::api::server::WebSendParams {
|
||||||
fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi {
|
fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi {
|
||||||
[
|
[
|
||||||
self.files.into_into_dart().into_dart(),
|
self.files.into_into_dart().into_dart(),
|
||||||
self.pin.into_into_dart().into_dart(),
|
self.pin.into_into_dart().into_dart(),
|
||||||
self.i18n.into_into_dart().into_dart(),
|
|
||||||
]
|
]
|
||||||
.into_dart()
|
.into_dart()
|
||||||
}
|
}
|
||||||
@@ -6914,6 +6962,16 @@ impl SseEncode for Option<u32> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl SseEncode for Option<crate::api::server::WebParams> {
|
||||||
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
|
||||||
|
<bool>::sse_encode(self.is_some(), serializer);
|
||||||
|
if let Some(value) = self {
|
||||||
|
<crate::api::server::WebParams>::sse_encode(value, serializer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl SseEncode for Option<crate::api::server::WebSendParams> {
|
impl SseEncode for Option<crate::api::server::WebSendParams> {
|
||||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
|
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
|
||||||
@@ -7377,7 +7435,7 @@ impl SseEncode for usize {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SseEncode for crate::api::server::WebSendI18n {
|
impl SseEncode for crate::api::server::WebI18n {
|
||||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
|
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
|
||||||
<String>::sse_encode(self.waiting, serializer);
|
<String>::sse_encode(self.waiting, serializer);
|
||||||
@@ -7385,12 +7443,23 @@ impl SseEncode for crate::api::server::WebSendI18n {
|
|||||||
<String>::sse_encode(self.invalid_pin, serializer);
|
<String>::sse_encode(self.invalid_pin, serializer);
|
||||||
<String>::sse_encode(self.too_many_attempts, serializer);
|
<String>::sse_encode(self.too_many_attempts, serializer);
|
||||||
<String>::sse_encode(self.rejected, serializer);
|
<String>::sse_encode(self.rejected, serializer);
|
||||||
|
<String>::sse_encode(self.upload_rejected, serializer);
|
||||||
|
<String>::sse_encode(self.busy, serializer);
|
||||||
<String>::sse_encode(self.files, serializer);
|
<String>::sse_encode(self.files, serializer);
|
||||||
<String>::sse_encode(self.file_name, serializer);
|
<String>::sse_encode(self.file_name, serializer);
|
||||||
<String>::sse_encode(self.size, serializer);
|
<String>::sse_encode(self.size, serializer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl SseEncode for crate::api::server::WebParams {
|
||||||
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
|
||||||
|
<Option<crate::api::server::WebSendParams>>::sse_encode(self.send, serializer);
|
||||||
|
<bool>::sse_encode(self.upload, serializer);
|
||||||
|
<crate::api::server::WebI18n>::sse_encode(self.i18n, serializer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl SseEncode for crate::api::server::WebSendParams {
|
impl SseEncode for crate::api::server::WebSendParams {
|
||||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
|
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
|
||||||
@@ -7398,7 +7467,6 @@ impl SseEncode for crate::api::server::WebSendParams {
|
|||||||
self.files, serializer,
|
self.files, serializer,
|
||||||
);
|
);
|
||||||
<Option<String>>::sse_encode(self.pin, serializer);
|
<Option<String>>::sse_encode(self.pin, serializer);
|
||||||
<crate::api::server::WebSendI18n>::sse_encode(self.i18n, serializer);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user