mirror of
https://github.com/localsend/localsend.git
synced 2026-08-07 07:14:52 +00:00
feat: quick save mode
This commit is contained in:
+2
-1
@@ -1,5 +1,6 @@
|
||||
## 1.5.0 (2023-01-)
|
||||
## 1.5.0 (2023-01-09)
|
||||
|
||||
- feat: quick save mode
|
||||
- feat: accept requests partially
|
||||
- feat: set destination directory during accept phase
|
||||
- feat: rename incoming files
|
||||
|
||||
@@ -18,11 +18,13 @@
|
||||
"files": "Files",
|
||||
"finished": "Finished",
|
||||
"hide": "Hide",
|
||||
"no": "No",
|
||||
"off": "Off",
|
||||
"offline": "Offline",
|
||||
"on": "On",
|
||||
"online": "Online",
|
||||
"open": "Open",
|
||||
"queue": "Queue",
|
||||
"quickSave": "Quick Save",
|
||||
"renamed": "Umbenannt",
|
||||
"reset": "Reset",
|
||||
"restart": "Restart",
|
||||
@@ -31,8 +33,7 @@
|
||||
"stop": "Stop",
|
||||
"save": "Save",
|
||||
"unchanged": "Unchanged",
|
||||
"unknown": "Unknown",
|
||||
"yes": "Yes"
|
||||
"unknown": "Unknown"
|
||||
},
|
||||
"receiveTab": {
|
||||
"title": "Receive",
|
||||
@@ -79,6 +80,7 @@
|
||||
},
|
||||
"receive": {
|
||||
"title": "Receive",
|
||||
"quickSave": "@:general.quickSave",
|
||||
"destination": "Destination",
|
||||
"saveToGallery": "Save media to gallery"
|
||||
},
|
||||
@@ -160,6 +162,10 @@
|
||||
"padZero": "Pad with zeros",
|
||||
"sortBeforeCount": "Sort alphabetically beforehand",
|
||||
"random": "Random"
|
||||
},
|
||||
"quickSaveNotice": {
|
||||
"title": "@:general.quickSave",
|
||||
"content": "File requests are automatically accepted. Be aware that everyone in the local network can send you files."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,11 +18,13 @@
|
||||
"files": "Dateien",
|
||||
"finished": "Abgeschlossen",
|
||||
"hide": "Verstecken",
|
||||
"no": "Nein",
|
||||
"off": "Aus",
|
||||
"offline": "Offline",
|
||||
"on": "An",
|
||||
"online": "Online",
|
||||
"open": "Öffnen",
|
||||
"queue": "Warteschlange",
|
||||
"quickSave": "Quick Save",
|
||||
"renamed": "Umbenannt",
|
||||
"reset": "Zurücksetzen",
|
||||
"restart": "Neustarten",
|
||||
@@ -31,8 +33,7 @@
|
||||
"stop": "Stop",
|
||||
"save": "Speichern",
|
||||
"unchanged": "Unverändert",
|
||||
"unknown": "Unbekannt",
|
||||
"yes": "Ja"
|
||||
"unknown": "Unbekannt"
|
||||
},
|
||||
"receiveTab": {
|
||||
"title": "Empfangen",
|
||||
@@ -79,6 +80,7 @@
|
||||
},
|
||||
"receive": {
|
||||
"title": "Empfangen",
|
||||
"quickSave": "@:general.quickSave",
|
||||
"destination": "Ziel-Ordner",
|
||||
"saveToGallery": "Medien in die Gallerie speichern"
|
||||
},
|
||||
@@ -160,6 +162,10 @@
|
||||
"padZero": "Mit Nullen auffüllen",
|
||||
"sortBeforeCount": "Vorher alphabetisch sortieren",
|
||||
"random": "Zufall"
|
||||
},
|
||||
"quickSaveNotice": {
|
||||
"title": "@:general.quickSave",
|
||||
"content": "Dateianfragen werden automatisch akzeptiert. Beachte, dass jeder im lokalen Netzwerk dir Dateien senden kann."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,5 +13,6 @@ class Settings with _$Settings {
|
||||
required int port,
|
||||
required String? destination, // null = default
|
||||
required bool saveToGallery, // only Android, iOS
|
||||
required bool quickSave, // automatically accept file requests
|
||||
}) = _Settings;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ import 'package:localsend_app/provider/network/server_provider.dart';
|
||||
import 'package:localsend_app/provider/settings_provider.dart';
|
||||
import 'package:localsend_app/util/ip_helper.dart';
|
||||
import 'package:localsend_app/widget/animations/initial_fade_transition.dart';
|
||||
import 'package:localsend_app/widget/custom_icon_button.dart';
|
||||
import 'package:localsend_app/widget/dialogs/quick_save_notice.dart';
|
||||
import 'package:localsend_app/widget/responsive_list_view.dart';
|
||||
import 'package:localsend_app/widget/rotating_widget.dart';
|
||||
|
||||
@@ -30,61 +32,90 @@ class _ReceiveTagState extends ConsumerState<ReceiveTab> with AutomaticKeepAlive
|
||||
final settings = ref.watch(settingsProvider);
|
||||
final networkInfo = ref.watch(networkInfoProvider);
|
||||
final serverState = ref.watch(serverProvider);
|
||||
return Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: ResponsiveListView.defaultMaxWidth),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Expanded(
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
child: Center(
|
||||
return Stack(
|
||||
children: [
|
||||
Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: ResponsiveListView.defaultMaxWidth),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(15),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
InitialFadeTransition(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
delay: const Duration(milliseconds: 200),
|
||||
child: RotatingWidget(
|
||||
duration: const Duration(seconds: 15),
|
||||
spinning: serverState != null,
|
||||
child: SizedBox(
|
||||
height: 200,
|
||||
child: Assets.img.logo512.image(height: 200),
|
||||
Expanded(
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
InitialFadeTransition(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
delay: const Duration(milliseconds: 200),
|
||||
child: RotatingWidget(
|
||||
duration: const Duration(seconds: 15),
|
||||
spinning: serverState != null,
|
||||
child: SizedBox(
|
||||
height: 200,
|
||||
child: Assets.img.logo512.image(height: 200),
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(serverState?.alias ?? settings.alias, style: const TextStyle(fontSize: 48)),
|
||||
InitialFadeTransition(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
delay: const Duration(milliseconds: 500),
|
||||
child: Text(
|
||||
serverState == null ? t.general.offline : networkInfo?.localIps.map((ip) => '#${ip.visualId}').toSet().join(' ') ?? '?',
|
||||
style: const TextStyle(fontSize: 24),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(serverState?.alias ?? settings.alias, style: const TextStyle(fontSize: 48)),
|
||||
InitialFadeTransition(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
delay: const Duration(milliseconds: 500),
|
||||
child: Text(
|
||||
serverState == null ? t.general.offline : networkInfo?.localIps.map((ip) => '#${ip.visualId}').toSet().join(' ') ?? '?',
|
||||
style: const TextStyle(fontSize: 24),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 20, top: 10),
|
||||
child: Center(
|
||||
child: ElevatedButton(
|
||||
style: settings.quickSave ? ElevatedButton.styleFrom(
|
||||
backgroundColor: Theme.of(context).buttonTheme.colorScheme!.primary,
|
||||
foregroundColor: Theme.of(context).buttonTheme.colorScheme!.onPrimary,
|
||||
) : null,
|
||||
onPressed: () {
|
||||
ref.read(settingsProvider.notifier).setQuickSave(!settings.quickSave);
|
||||
if (!settings.quickSave) {
|
||||
QuickSaveNotice.open(context);
|
||||
}
|
||||
},
|
||||
child: Text('${t.general.quickSave}: ${settings.quickSave ? t.general.on : t.general.off}'),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 15),
|
||||
],
|
||||
),
|
||||
Center(
|
||||
child: TextButton(
|
||||
key: ValueKey('toggle-$_advanced'),
|
||||
onPressed: () {
|
||||
setState(() => _advanced = !_advanced);
|
||||
},
|
||||
child: Text(_advanced ? t.general.hide : t.general.advanced),
|
||||
),
|
||||
),
|
||||
AnimatedCrossFade(
|
||||
crossFadeState: _advanced ? CrossFadeState.showSecond : CrossFadeState.showFirst,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
firstChild: Container(),
|
||||
secondChild: Card(
|
||||
),
|
||||
),
|
||||
),
|
||||
AnimatedCrossFade(
|
||||
crossFadeState: _advanced ? CrossFadeState.showSecond : CrossFadeState.showFirst,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
firstChild: Container(),
|
||||
secondChild: Align(
|
||||
alignment: Alignment.topRight,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(15),
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 400),
|
||||
child: Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(15),
|
||||
child: Table(
|
||||
@@ -134,11 +165,22 @@ class _ReceiveTagState extends ConsumerState<ReceiveTab> with AutomaticKeepAlive
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 15),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.topRight,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: CustomIconButton(
|
||||
onPressed: () {
|
||||
setState(() => _advanced = !_advanced);
|
||||
},
|
||||
child: const Icon(Icons.info),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import 'package:localsend_app/util/platform_check.dart';
|
||||
import 'package:localsend_app/util/sleep.dart';
|
||||
import 'package:localsend_app/util/snackbar.dart';
|
||||
import 'package:localsend_app/widget/custom_dropdown_button.dart';
|
||||
import 'package:localsend_app/widget/dialogs/quick_save_notice.dart';
|
||||
import 'package:localsend_app/widget/local_send_logo.dart';
|
||||
import 'package:localsend_app/widget/responsive_list_view.dart';
|
||||
import 'package:routerino/routerino.dart';
|
||||
@@ -105,6 +106,27 @@ class _SettingsTabState extends ConsumerState<SettingsTab> {
|
||||
_SettingsSection(
|
||||
title: t.settingsTab.receive.title,
|
||||
children: [
|
||||
_SettingsEntry(
|
||||
label: t.settingsTab.receive.quickSave,
|
||||
child: CustomDropdownButton<bool>(
|
||||
value: settings.quickSave,
|
||||
items: [false, true].map((b) {
|
||||
return DropdownMenuItem(
|
||||
value: b,
|
||||
alignment: Alignment.center,
|
||||
child: Text(b ? t.general.on : t.general.off),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (b) async {
|
||||
if (b != null) {
|
||||
await ref.read(settingsProvider.notifier).setQuickSave(b);
|
||||
if (b && mounted) {
|
||||
QuickSaveNotice.open(context);
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
if (checkPlatform([TargetPlatform.windows, TargetPlatform.macOS, TargetPlatform.linux]))
|
||||
_SettingsEntry(
|
||||
label: t.settingsTab.receive.destination,
|
||||
@@ -125,7 +147,10 @@ class _SettingsTabState extends ConsumerState<SettingsTab> {
|
||||
await ref.read(settingsProvider.notifier).setDestination(directory);
|
||||
}
|
||||
},
|
||||
child: Text(settings.destination ?? '(Downloads)'),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 5),
|
||||
child: Text(settings.destination ?? '(Downloads)'),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (checkPlatformWithGallery())
|
||||
@@ -137,7 +162,7 @@ class _SettingsTabState extends ConsumerState<SettingsTab> {
|
||||
return DropdownMenuItem(
|
||||
value: b,
|
||||
alignment: Alignment.center,
|
||||
child: Text(b ? t.general.yes : t.general.no),
|
||||
child: Text(b ? t.general.on : t.general.off),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (b) async {
|
||||
|
||||
@@ -12,6 +12,7 @@ import 'package:localsend_app/model/server/receive_state.dart';
|
||||
import 'package:localsend_app/model/server/receiving_file.dart';
|
||||
import 'package:localsend_app/model/server/server_state.dart';
|
||||
import 'package:localsend_app/model/session_status.dart';
|
||||
import 'package:localsend_app/pages/progress_page.dart';
|
||||
import 'package:localsend_app/pages/receive_page.dart';
|
||||
import 'package:localsend_app/provider/device_info_provider.dart';
|
||||
import 'package:localsend_app/provider/persistence_provider.dart';
|
||||
@@ -106,7 +107,8 @@ class ServerNotifier extends StateNotifier<ServerState?> {
|
||||
return Response.badRequest();
|
||||
}
|
||||
|
||||
String? destinationDir = _ref.read(settingsProvider).destination;
|
||||
final settings = _ref.read(settingsProvider);
|
||||
String? destinationDir = settings.destination;
|
||||
if (destinationDir == null) {
|
||||
switch (defaultTargetPlatform) {
|
||||
case TargetPlatform.android:
|
||||
@@ -149,19 +151,28 @@ class ServerNotifier extends StateNotifier<ServerState?> {
|
||||
),
|
||||
);
|
||||
|
||||
// ignore: use_build_context_synchronously
|
||||
Routerino.context.push(() => const ReceivePage());
|
||||
final Map<String, String>? selection;
|
||||
if (settings.quickSave) {
|
||||
// accept all files
|
||||
selection = {
|
||||
for (final f in dto.files.values)
|
||||
f.id: f.fileName,
|
||||
};
|
||||
} else {
|
||||
// ignore: use_build_context_synchronously
|
||||
Routerino.context.push(() => const ReceivePage());
|
||||
|
||||
// Delayed response (waiting for user's decision)
|
||||
final result = await streamController.stream.first;
|
||||
// Delayed response (waiting for user's decision)
|
||||
selection = await streamController.stream.first;
|
||||
}
|
||||
|
||||
if (state?.receiveState == null) {
|
||||
// somehow this state is already disposed
|
||||
return Response.internalServerError();
|
||||
}
|
||||
|
||||
if (result != null) {
|
||||
if (result.isEmpty) {
|
||||
if (selection != null) {
|
||||
if (selection.isEmpty) {
|
||||
// nothing selected, send this to sender and close session
|
||||
closeSession();
|
||||
return Response.ok(
|
||||
@@ -176,7 +187,7 @@ class ServerNotifier extends StateNotifier<ServerState?> {
|
||||
status: SessionStatus.sending,
|
||||
files: Map.fromEntries(
|
||||
receiveState.files.values.map((entry) {
|
||||
final desiredName = result[entry.file.id];
|
||||
final desiredName = selection![entry.file.id];
|
||||
return MapEntry(
|
||||
entry.file.id,
|
||||
ReceivingFile(
|
||||
@@ -193,6 +204,12 @@ class ServerNotifier extends StateNotifier<ServerState?> {
|
||||
responseHandler: null,
|
||||
),
|
||||
);
|
||||
|
||||
if (settings.quickSave) {
|
||||
// ignore: use_build_context_synchronously
|
||||
Routerino.context.pushImmediately(() => const ProgressPage());
|
||||
}
|
||||
|
||||
return Response.ok(
|
||||
jsonEncode({
|
||||
for (final file in state!.receiveState!.files.values.where((f) => f.token != null)) file.file.id: file.token,
|
||||
@@ -295,6 +312,14 @@ class ServerNotifier extends StateNotifier<ServerState?> {
|
||||
endTime: DateTime.now().millisecondsSinceEpoch,
|
||||
),
|
||||
);
|
||||
if (_ref.read(settingsProvider).quickSave) {
|
||||
// close the session after return of the response
|
||||
Future.delayed(Duration.zero, () {
|
||||
closeSession();
|
||||
// ignore: use_build_context_synchronously
|
||||
Routerino.context.popUntilRoot();
|
||||
});
|
||||
}
|
||||
print('Received all files.');
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ const _localeKey = 'ls_locale';
|
||||
const _portKey = 'ls_port';
|
||||
const _destinationKey = 'ls_destination';
|
||||
const _saveToGallery = 'ls_save_to_gallery';
|
||||
const _quickSave = 'ls_quick_save';
|
||||
|
||||
const defaultPort = 53317;
|
||||
|
||||
@@ -101,4 +102,12 @@ class PersistenceService {
|
||||
Future<void> setSaveToGallery(bool saveToGallery) async {
|
||||
await _prefs.setBool(_saveToGallery, saveToGallery);
|
||||
}
|
||||
|
||||
bool isQuickSave() {
|
||||
return _prefs.getBool(_quickSave) ?? false;
|
||||
}
|
||||
|
||||
Future<void> setQuickSave(bool quickSave) async {
|
||||
await _prefs.setBool(_quickSave, quickSave);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ class SettingsNotifier extends StateNotifier<Settings> {
|
||||
port: service.getPort(),
|
||||
destination: service.getDestination(),
|
||||
saveToGallery: service.isSaveToGallery(),
|
||||
quickSave: service.isQuickSave(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -66,4 +67,11 @@ class SettingsNotifier extends StateNotifier<Settings> {
|
||||
saveToGallery: saveToGallery,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> setQuickSave(bool quickSave) async {
|
||||
await _service.setQuickSave(quickSave);
|
||||
state = state.copyWith(
|
||||
quickSave: quickSave,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:localsend_app/gen/strings.g.dart';
|
||||
import 'package:localsend_app/util/platform_check.dart';
|
||||
import 'package:localsend_app/widget/dialogs/custom_bottom_sheet.dart';
|
||||
import 'package:routerino/routerino.dart';
|
||||
|
||||
class QuickSaveNotice extends StatelessWidget {
|
||||
const QuickSaveNotice({Key? key}) : super(key: key);
|
||||
|
||||
static void open(BuildContext context) {
|
||||
if (checkPlatformIsDesktop()) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (_) => AlertDialog(
|
||||
title: Text(t.dialogs.quickSaveNotice.title),
|
||||
content: Text(t.dialogs.quickSaveNotice.content),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
context.pop();
|
||||
},
|
||||
child: Text(t.general.close),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
} else {
|
||||
context.pushBottomSheet(() => const QuickSaveNotice());
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CustomBottomSheet(
|
||||
title: t.dialogs.quickSaveNotice.title,
|
||||
description: t.dialogs.quickSaveNotice.content,
|
||||
child: Center(
|
||||
child: ElevatedButton(
|
||||
onPressed: () => context.popUntilRoot(),
|
||||
child: Text(t.general.close),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user