feat: quick save mode

This commit is contained in:
Tien Do Nam
2023-01-09 00:57:55 +01:00
parent 39967a0f11
commit 071d7af5ed
10 changed files with 234 additions and 66 deletions
+2 -1
View File
@@ -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
+9 -3
View File
@@ -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."
}
}
}
+9 -3
View File
@@ -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."
}
}
}
+1
View File
@@ -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;
}
+91 -49
View File
@@ -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),
),
),
),
],
);
}
}
+27 -2
View File
@@ -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 {
+33 -8
View File
@@ -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.');
}
+9
View File
@@ -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);
}
}
+8
View File
@@ -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,
);
}
}
+45
View File
@@ -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),
),
),
);
}
}