mirror of
https://github.com/localsend/localsend.git
synced 2026-08-07 07:14:52 +00:00
perf: extract file status state
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
import 'package:localsend_app/provider/file_status_provider.dart';
|
||||
import 'package:localsend_app/provider/file_transfer_provider.dart';
|
||||
import 'package:localsend_app/provider/local_ip_provider.dart';
|
||||
import 'package:localsend_app/provider/logging/discovery_logs_provider.dart';
|
||||
import 'package:localsend_app/provider/progress_provider.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:refena_flutter/refena_flutter.dart';
|
||||
import 'package:refena_inspector_client/refena_inspector_client.dart';
|
||||
@@ -27,11 +26,7 @@ class CustomRefenaObserver extends RefenaMultiObserver {
|
||||
|
||||
bool _exclude(RefenaEvent event) {
|
||||
return switch (event) {
|
||||
ChangeEvent() =>
|
||||
event.notifier is DiscoveryLogger ||
|
||||
event.notifier is LocalIpService ||
|
||||
event.notifier is ProgressNotifier ||
|
||||
event.notifier is FileStatusNotifier,
|
||||
ChangeEvent() => event.notifier is DiscoveryLogger || event.notifier is LocalIpService || event.notifier is FileTransferNotifier,
|
||||
ActionDispatchedEvent() => event.action.runtimeType.toString() == '_FetchLocalIpAction',
|
||||
ActionFinishedEvent() => event.action.runtimeType.toString() == '_FetchLocalIpAction',
|
||||
_ => false,
|
||||
|
||||
@@ -2,7 +2,6 @@ import 'dart:typed_data';
|
||||
|
||||
import 'package:dart_mappable/dart_mappable.dart';
|
||||
import 'package:localsend_isolates/model/dto/file_dto.dart';
|
||||
import 'package:localsend_isolates/model/file_status.dart';
|
||||
import 'package:wechat_assets_picker/wechat_assets_picker.dart';
|
||||
|
||||
part 'sending_file.mapper.dart';
|
||||
@@ -10,17 +9,15 @@ part 'sending_file.mapper.dart';
|
||||
@MappableClass()
|
||||
class SendingFile with SendingFileMappable {
|
||||
final FileDto file;
|
||||
final FileStatus status;
|
||||
final String? token;
|
||||
final Uint8List? thumbnail;
|
||||
final AssetEntity? asset; // for thumbnails
|
||||
final String? path; // android, iOS, desktop
|
||||
final List<int>? bytes; // web
|
||||
final String? errorMessage; // when status == failed
|
||||
final String? errorMessage; // when failed; the live status is tracked in fileTransferProvider
|
||||
|
||||
const SendingFile({
|
||||
required this.file,
|
||||
required this.status,
|
||||
required this.token,
|
||||
required this.thumbnail,
|
||||
required this.asset,
|
||||
@@ -32,6 +29,6 @@ class SendingFile with SendingFileMappable {
|
||||
/// Custom toString() to avoid printing the bytes.
|
||||
@override
|
||||
String toString() {
|
||||
return 'SendingFile(file: $file, status: $status, token: $token, thumbnail: ${thumbnail != null ? thumbnail!.length : 'null'}, asset: $asset, path: $path, bytes: ${bytes != null ? bytes!.length : 'null'}, errorMessage: $errorMessage)';
|
||||
return 'SendingFile(file: $file, token: $token, thumbnail: ${thumbnail != null ? thumbnail!.length : 'null'}, asset: $asset, path: $path, bytes: ${bytes != null ? bytes!.length : 'null'}, errorMessage: $errorMessage)';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,11 +24,6 @@ class SendingFileMapper extends ClassMapperBase<SendingFile> {
|
||||
|
||||
static FileDto _$file(SendingFile v) => v.file;
|
||||
static const Field<SendingFile, FileDto> _f$file = Field('file', _$file);
|
||||
static FileStatus _$status(SendingFile v) => v.status;
|
||||
static const Field<SendingFile, FileStatus> _f$status = Field(
|
||||
'status',
|
||||
_$status,
|
||||
);
|
||||
static String? _$token(SendingFile v) => v.token;
|
||||
static const Field<SendingFile, String> _f$token = Field('token', _$token);
|
||||
static Uint8List? _$thumbnail(SendingFile v) => v.thumbnail;
|
||||
@@ -54,7 +49,6 @@ class SendingFileMapper extends ClassMapperBase<SendingFile> {
|
||||
@override
|
||||
final MappableFields<SendingFile> fields = const {
|
||||
#file: _f$file,
|
||||
#status: _f$status,
|
||||
#token: _f$token,
|
||||
#thumbnail: _f$thumbnail,
|
||||
#asset: _f$asset,
|
||||
@@ -66,7 +60,6 @@ class SendingFileMapper extends ClassMapperBase<SendingFile> {
|
||||
static SendingFile _instantiate(DecodingData data) {
|
||||
return SendingFile(
|
||||
file: data.dec(_f$file),
|
||||
status: data.dec(_f$status),
|
||||
token: data.dec(_f$token),
|
||||
thumbnail: data.dec(_f$thumbnail),
|
||||
asset: data.dec(_f$asset),
|
||||
@@ -139,7 +132,6 @@ abstract class SendingFileCopyWith<$R, $In extends SendingFile, $Out>
|
||||
ListCopyWith<$R, int, ObjectCopyWith<$R, int, int>>? get bytes;
|
||||
$R call({
|
||||
FileDto? file,
|
||||
FileStatus? status,
|
||||
String? token,
|
||||
Uint8List? thumbnail,
|
||||
AssetEntity? asset,
|
||||
@@ -170,7 +162,6 @@ class _SendingFileCopyWithImpl<$R, $Out>
|
||||
@override
|
||||
$R call({
|
||||
FileDto? file,
|
||||
FileStatus? status,
|
||||
Object? token = $none,
|
||||
Object? thumbnail = $none,
|
||||
Object? asset = $none,
|
||||
@@ -180,7 +171,6 @@ class _SendingFileCopyWithImpl<$R, $Out>
|
||||
}) => $apply(
|
||||
FieldCopyWithData({
|
||||
if (file != null) #file: file,
|
||||
if (status != null) #status: status,
|
||||
if (token != $none) #token: token,
|
||||
if (thumbnail != $none) #thumbnail: thumbnail,
|
||||
if (asset != $none) #asset: asset,
|
||||
@@ -192,7 +182,6 @@ class _SendingFileCopyWithImpl<$R, $Out>
|
||||
@override
|
||||
SendingFile $make(CopyWithData data) => SendingFile(
|
||||
file: data.get(#file, or: $value.file),
|
||||
status: data.get(#status, or: $value.status),
|
||||
token: data.get(#token, or: $value.token),
|
||||
thumbnail: data.get(#thumbnail, or: $value.thumbnail),
|
||||
asset: data.get(#asset, or: $value.asset),
|
||||
|
||||
@@ -1,22 +1,19 @@
|
||||
import 'package:dart_mappable/dart_mappable.dart';
|
||||
import 'package:localsend_isolates/model/dto/file_dto.dart';
|
||||
import 'package:localsend_isolates/model/file_status.dart';
|
||||
|
||||
part 'receiving_file.mapper.dart';
|
||||
|
||||
@MappableClass()
|
||||
class ReceivingFile with ReceivingFileMappable {
|
||||
final FileDto file;
|
||||
final FileStatus status;
|
||||
final String? token;
|
||||
final String? desiredName; // not null when accepted
|
||||
final String? path; // when finished
|
||||
final bool savedToGallery; // when finished
|
||||
final String? errorMessage; // when status == failed
|
||||
final String? errorMessage; // when failed; the live status is tracked in fileTransferProvider
|
||||
|
||||
const ReceivingFile({
|
||||
required this.file,
|
||||
required this.status,
|
||||
required this.token,
|
||||
required this.desiredName,
|
||||
required this.path,
|
||||
|
||||
@@ -24,11 +24,6 @@ class ReceivingFileMapper extends ClassMapperBase<ReceivingFile> {
|
||||
|
||||
static FileDto _$file(ReceivingFile v) => v.file;
|
||||
static const Field<ReceivingFile, FileDto> _f$file = Field('file', _$file);
|
||||
static FileStatus _$status(ReceivingFile v) => v.status;
|
||||
static const Field<ReceivingFile, FileStatus> _f$status = Field(
|
||||
'status',
|
||||
_$status,
|
||||
);
|
||||
static String? _$token(ReceivingFile v) => v.token;
|
||||
static const Field<ReceivingFile, String> _f$token = Field('token', _$token);
|
||||
static String? _$desiredName(ReceivingFile v) => v.desiredName;
|
||||
@@ -52,7 +47,6 @@ class ReceivingFileMapper extends ClassMapperBase<ReceivingFile> {
|
||||
@override
|
||||
final MappableFields<ReceivingFile> fields = const {
|
||||
#file: _f$file,
|
||||
#status: _f$status,
|
||||
#token: _f$token,
|
||||
#desiredName: _f$desiredName,
|
||||
#path: _f$path,
|
||||
@@ -63,7 +57,6 @@ class ReceivingFileMapper extends ClassMapperBase<ReceivingFile> {
|
||||
static ReceivingFile _instantiate(DecodingData data) {
|
||||
return ReceivingFile(
|
||||
file: data.dec(_f$file),
|
||||
status: data.dec(_f$status),
|
||||
token: data.dec(_f$token),
|
||||
desiredName: data.dec(_f$desiredName),
|
||||
path: data.dec(_f$path),
|
||||
@@ -136,7 +129,6 @@ abstract class ReceivingFileCopyWith<$R, $In extends ReceivingFile, $Out>
|
||||
implements ClassCopyWith<$R, $In, $Out> {
|
||||
$R call({
|
||||
FileDto? file,
|
||||
FileStatus? status,
|
||||
String? token,
|
||||
String? desiredName,
|
||||
String? path,
|
||||
@@ -157,7 +149,6 @@ class _ReceivingFileCopyWithImpl<$R, $Out>
|
||||
@override
|
||||
$R call({
|
||||
FileDto? file,
|
||||
FileStatus? status,
|
||||
Object? token = $none,
|
||||
Object? desiredName = $none,
|
||||
Object? path = $none,
|
||||
@@ -166,7 +157,6 @@ class _ReceivingFileCopyWithImpl<$R, $Out>
|
||||
}) => $apply(
|
||||
FieldCopyWithData({
|
||||
if (file != null) #file: file,
|
||||
if (status != null) #status: status,
|
||||
if (token != $none) #token: token,
|
||||
if (desiredName != $none) #desiredName: desiredName,
|
||||
if (path != $none) #path: path,
|
||||
@@ -177,7 +167,6 @@ class _ReceivingFileCopyWithImpl<$R, $Out>
|
||||
@override
|
||||
ReceivingFile $make(CopyWithData data) => ReceivingFile(
|
||||
file: data.get(#file, or: $value.file),
|
||||
status: data.get(#status, or: $value.status),
|
||||
token: data.get(#token, or: $value.token),
|
||||
desiredName: data.get(#desiredName, or: $value.desiredName),
|
||||
path: data.get(#path, or: $value.path),
|
||||
|
||||
@@ -7,9 +7,9 @@ import 'package:localsend_app/config/theme.dart';
|
||||
import 'package:localsend_app/gen/strings.g.dart';
|
||||
import 'package:localsend_app/model/state/server/receive_session_state.dart';
|
||||
import 'package:localsend_app/pages/web_share_page.dart';
|
||||
import 'package:localsend_app/provider/file_transfer_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/progress_provider.dart';
|
||||
import 'package:localsend_app/provider/settings_provider.dart';
|
||||
import 'package:localsend_app/util/native/open_file.dart';
|
||||
import 'package:localsend_app/util/native/open_folder.dart';
|
||||
@@ -82,10 +82,8 @@ class _ProgressPageState extends State<ProgressPage> with Refena {
|
||||
// each enable() acquires a new inhibit cookie while disable() only releases one, so re-calling
|
||||
// enable() every 30s leaks inhibit locks that keep the screen awake indefinitely (issue #3209).
|
||||
_wakelockPlusTimer = Timer.periodic(const Duration(seconds: 30), (timer) {
|
||||
final finished =
|
||||
ref.read(serverProvider)?.session?.files.values.map((e) => e.status).isFinishedOrSkipped ??
|
||||
ref.read(sendProvider)[widget.sessionId]?.files.values.map((e) => e.status).isFinishedOrSkipped ??
|
||||
true;
|
||||
// an empty iterable (session already removed) also counts as finished
|
||||
final finished = ref.read(fileTransferProvider).getStatuses(widget.sessionId).isFinishedOrSkipped;
|
||||
if (finished) {
|
||||
timer.cancel();
|
||||
try {
|
||||
@@ -97,10 +95,8 @@ class _ProgressPageState extends State<ProgressPage> with Refena {
|
||||
|
||||
if (ref.read(settingsProvider).autoFinish) {
|
||||
_finishTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
|
||||
final finished =
|
||||
ref.read(serverProvider)?.session?.files.values.map((e) => e.status).isFinishedOrSkipped ??
|
||||
ref.read(sendProvider)[widget.sessionId]?.files.values.map((e) => e.status).isFinishedOrSkipped ??
|
||||
true;
|
||||
// an empty iterable (session already removed) also counts as finished
|
||||
final finished = ref.read(fileTransferProvider).getStatuses(widget.sessionId).isFinishedOrSkipped;
|
||||
if (finished) {
|
||||
if (_finishCounter == 1) {
|
||||
timer.cancel();
|
||||
@@ -118,17 +114,20 @@ class _ProgressPageState extends State<ProgressPage> with Refena {
|
||||
final receiveSession = ref.read(serverProvider)?.session;
|
||||
if (receiveSession != null) {
|
||||
_files = receiveSession.files.values.map((f) => f.file).toList();
|
||||
|
||||
// We previously used f.token != null here, but this may not work on very fast networks.
|
||||
_selectedFiles = receiveSession.files.values.where((f) => f.status != FileStatus.skipped).map((f) => f.file.id).toSet();
|
||||
} else {
|
||||
final sendSession = ref.read(sendProvider)[widget.sessionId];
|
||||
if (sendSession != null) {
|
||||
_files = sendSession.files.values.map((f) => f.file).toList();
|
||||
_selectedFiles = sendSession.files.values.where((f) => f.status != FileStatus.skipped).map((f) => f.file.id).toSet();
|
||||
}
|
||||
}
|
||||
|
||||
// We previously used f.token != null here, but this may not work on very fast networks.
|
||||
final transferNotifier = ref.read(fileTransferProvider);
|
||||
_selectedFiles = _files
|
||||
.where((f) => transferNotifier.getStatus(sessionId: widget.sessionId, fileId: f.id) != FileStatus.skipped)
|
||||
.map((f) => f.id)
|
||||
.toSet();
|
||||
|
||||
_totalBytes = _files.where((f) => _selectedFiles.contains(f.id)).fold(0, (prev, curr) => prev + curr.size);
|
||||
});
|
||||
});
|
||||
@@ -191,13 +190,15 @@ class _ProgressPageState extends State<ProgressPage> with Refena {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final progressNotifier = ref.watch(progressProvider);
|
||||
final transferNotifier = ref.watch(fileTransferProvider);
|
||||
final currBytes = _files.fold<int>(
|
||||
0,
|
||||
(prev, curr) => prev + ((progressNotifier.getProgress(sessionId: widget.sessionId, fileId: curr.id) * curr.size).round()),
|
||||
(prev, curr) => prev + ((transferNotifier.getProgress(sessionId: widget.sessionId, fileId: curr.id) * curr.size).round()),
|
||||
);
|
||||
|
||||
final receiveSession = ref.watch(serverProvider.select((s) => s?.session));
|
||||
// No select: comparing the selected session runs the dart_mappable deep equality
|
||||
// over the whole files map on every state change.
|
||||
final receiveSession = ref.watch(serverProvider)?.session;
|
||||
final sendSession = ref.watch(sendProvider)[widget.sessionId];
|
||||
|
||||
final SessionState? commonSessionState = receiveSession ?? sendSession;
|
||||
@@ -235,8 +236,7 @@ class _ProgressPageState extends State<ProgressPage> with Refena {
|
||||
speedInBytes = null;
|
||||
}
|
||||
|
||||
final fileStatusMap = receiveSession?.files.map((k, f) => MapEntry(k, f.status)) ?? sendSession!.files.map((k, f) => MapEntry(k, f.status));
|
||||
final finishedCount = fileStatusMap.values.where((s) => s == FileStatus.finished).length;
|
||||
final finishedCount = transferNotifier.getStatuses(widget.sessionId).where((s) => s == FileStatus.finished).length;
|
||||
|
||||
return PopScope(
|
||||
onPopInvokedWithResult: (didPop, result) {
|
||||
@@ -321,7 +321,7 @@ class _ProgressPageState extends State<ProgressPage> with Refena {
|
||||
final file = _files[index - 2];
|
||||
final String fileName = receiveSession?.files[file.id]?.desiredName ?? file.fileName;
|
||||
|
||||
final fileStatus = fileStatusMap[file.id]!;
|
||||
final fileStatus = transferNotifier.getStatus(sessionId: widget.sessionId, fileId: file.id);
|
||||
final savedToGallery = receiveSession?.files[file.id]?.savedToGallery ?? false;
|
||||
|
||||
final String? filePath;
|
||||
@@ -393,7 +393,7 @@ class _ProgressPageState extends State<ProgressPage> with Refena {
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 5),
|
||||
child: CustomProgressBar(
|
||||
progress: progressNotifier.getProgress(sessionId: widget.sessionId, fileId: file.id),
|
||||
progress: transferNotifier.getProgress(sessionId: widget.sessionId, fileId: file.id),
|
||||
),
|
||||
)
|
||||
else
|
||||
|
||||
@@ -6,8 +6,8 @@ import 'package:localsend_app/gen/strings.g.dart';
|
||||
import 'package:localsend_app/model/state/send/send_session_state.dart';
|
||||
import 'package:localsend_app/provider/device_info_provider.dart';
|
||||
import 'package:localsend_app/provider/favorites_provider.dart';
|
||||
import 'package:localsend_app/provider/file_transfer_provider.dart';
|
||||
import 'package:localsend_app/provider/network/send_provider.dart';
|
||||
import 'package:localsend_app/provider/progress_provider.dart';
|
||||
import 'package:localsend_app/util/favorites.dart';
|
||||
import 'package:localsend_app/util/native/taskbar_helper.dart';
|
||||
import 'package:localsend_app/widget/animations/initial_fade_transition.dart';
|
||||
@@ -35,7 +35,7 @@ class SendPage extends StatefulWidget {
|
||||
State<SendPage> createState() => _SendPageState();
|
||||
}
|
||||
|
||||
double _hashProgress(SendSessionState sendState, ProgressNotifier progressNotifier) {
|
||||
double _hashProgress(SendSessionState sendState, FileTransferNotifier transferNotifier) {
|
||||
final files = sendState.files.values;
|
||||
final totalBytes = files.fold<int>(0, (prev, curr) => prev + curr.file.size);
|
||||
if (totalBytes == 0) {
|
||||
@@ -43,7 +43,7 @@ double _hashProgress(SendSessionState sendState, ProgressNotifier progressNotifi
|
||||
}
|
||||
final hashedBytes = files.fold<double>(
|
||||
0,
|
||||
(prev, curr) => prev + progressNotifier.getProgress(sessionId: sendState.sessionId, fileId: curr.file.id) * curr.file.size,
|
||||
(prev, curr) => prev + transferNotifier.getProgress(sessionId: sendState.sessionId, fileId: curr.file.id) * curr.file.size,
|
||||
);
|
||||
return (hashedBytes / totalBytes).clamp(0, 1);
|
||||
}
|
||||
@@ -160,7 +160,7 @@ class _SendPageState extends State<SendPage> with Refena {
|
||||
SizedBox(
|
||||
width: 200,
|
||||
child: LinearProgressIndicator(
|
||||
value: _hashProgress(sendState, ref.watch(progressProvider)),
|
||||
value: _hashProgress(sendState, ref.watch(fileTransferProvider)),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -7,10 +7,10 @@ import 'package:localsend_app/pages/selected_files_page.dart';
|
||||
import 'package:localsend_app/pages/tabs/send_tab_vm.dart';
|
||||
import 'package:localsend_app/pages/troubleshoot_page.dart';
|
||||
import 'package:localsend_app/provider/animation_provider.dart';
|
||||
import 'package:localsend_app/provider/file_transfer_provider.dart';
|
||||
import 'package:localsend_app/provider/network/nearby_devices_provider.dart';
|
||||
import 'package:localsend_app/provider/network/scan_facade.dart';
|
||||
import 'package:localsend_app/provider/network/send_provider.dart';
|
||||
import 'package:localsend_app/provider/progress_provider.dart';
|
||||
import 'package:localsend_app/provider/selection/selected_sending_files_provider.dart';
|
||||
import 'package:localsend_app/provider/settings_provider.dart';
|
||||
import 'package:localsend_app/util/favorites.dart';
|
||||
@@ -533,10 +533,10 @@ class _MultiSendDeviceListTile extends StatelessWidget {
|
||||
final double? progress;
|
||||
if (session != null) {
|
||||
final files = session.files.values.where((f) => f.token != null);
|
||||
final progressNotifier = ref.watch(progressProvider);
|
||||
final transferNotifier = ref.watch(fileTransferProvider);
|
||||
final currBytes = files.fold<int>(
|
||||
0,
|
||||
(prev, curr) => prev + ((progressNotifier.getProgress(sessionId: session.sessionId, fileId: curr.file.id) * curr.file.size).round()),
|
||||
(prev, curr) => prev + ((transferNotifier.getProgress(sessionId: session.sessionId, fileId: curr.file.id) * curr.file.size).round()),
|
||||
);
|
||||
final totalBytes = files.fold<int>(0, (prev, curr) => prev + curr.file.size);
|
||||
progress = totalBytes == 0 ? 0 : currBytes / totalBytes;
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import 'package:localsend_isolates/model/file_status.dart';
|
||||
import 'package:refena_flutter/refena_flutter.dart';
|
||||
|
||||
/// A provider holding the live per-file transfer state (status and progress).
|
||||
/// It is implemented as [ChangeNotifier] for performance reasons:
|
||||
/// a status or progress update does not need to copy the whole session state.
|
||||
final fileTransferProvider = ChangeNotifierProvider((ref) => FileTransferNotifier());
|
||||
|
||||
class FileTransfer {
|
||||
FileStatus status;
|
||||
double progress; // 0..1
|
||||
|
||||
FileTransfer(this.status) : progress = 0;
|
||||
|
||||
@override
|
||||
String toString() => '($status, $progress)';
|
||||
}
|
||||
|
||||
class FileTransferNotifier extends ChangeNotifier {
|
||||
final _sessionMap = <String, Map<String, FileTransfer>>{}; // session id -> (file id -> live transfer state)
|
||||
|
||||
void setStatus({required String sessionId, required String fileId, required FileStatus status}) {
|
||||
_sessionMap.putIfAbsent(sessionId, () => {}).putIfAbsent(fileId, () => FileTransfer(status)).status = status;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Sets the status of multiple files at once, notifying listeners only once.
|
||||
void setStatuses({required String sessionId, required Map<String, FileStatus> statuses}) {
|
||||
final files = _sessionMap.putIfAbsent(sessionId, () => {});
|
||||
for (final entry in statuses.entries) {
|
||||
files.putIfAbsent(entry.key, () => FileTransfer(entry.value)).status = entry.value;
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void setProgress({required String sessionId, required String fileId, required double progress}) {
|
||||
_sessionMap.putIfAbsent(sessionId, () => {}).putIfAbsent(fileId, () => FileTransfer(FileStatus.queue)).progress = progress;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
FileStatus getStatus({required String sessionId, required String fileId}) {
|
||||
return _sessionMap[sessionId]?[fileId]?.status ?? FileStatus.queue;
|
||||
}
|
||||
|
||||
Iterable<FileStatus> getStatuses(String sessionId) {
|
||||
return _sessionMap[sessionId]?.values.map((file) => file.status) ?? const [];
|
||||
}
|
||||
|
||||
double getProgress({required String sessionId, required String fileId}) {
|
||||
return _sessionMap[sessionId]?[fileId]?.progress ?? 0.0;
|
||||
}
|
||||
|
||||
void removeSession(String sessionId) {
|
||||
_sessionMap.remove(sessionId);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void removeAllSessions() {
|
||||
_sessionMap.clear();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Only for debug purposes
|
||||
Map<String, Map<String, FileTransfer>> getData() {
|
||||
return _sessionMap;
|
||||
}
|
||||
}
|
||||
@@ -9,8 +9,8 @@ import 'package:localsend_app/pages/home_page.dart';
|
||||
import 'package:localsend_app/pages/progress_page.dart';
|
||||
import 'package:localsend_app/pages/send_page.dart';
|
||||
import 'package:localsend_app/provider/device_info_provider.dart';
|
||||
import 'package:localsend_app/provider/file_transfer_provider.dart';
|
||||
import 'package:localsend_app/provider/http_provider.dart';
|
||||
import 'package:localsend_app/provider/progress_provider.dart';
|
||||
import 'package:localsend_app/provider/selection/selected_sending_files_provider.dart';
|
||||
import 'package:localsend_app/provider/settings_provider.dart';
|
||||
import 'package:localsend_app/widget/dialogs/pin_dialog.dart';
|
||||
@@ -62,6 +62,24 @@ class SendNotifier extends Notifier<Map<String, SendSessionState>> {
|
||||
return {};
|
||||
}
|
||||
|
||||
/// The debug observer stringifies the state on every change,
|
||||
/// so large file maps must be summarized to keep transfers responsive in debug mode.
|
||||
@override
|
||||
String describeState(Map<String, SendSessionState> state) {
|
||||
if (state.values.every((session) => session.files.length <= 10)) {
|
||||
return state.toString();
|
||||
}
|
||||
return state.map((sessionId, session) {
|
||||
if (session.files.length <= 10) {
|
||||
return MapEntry(sessionId, session.toString());
|
||||
}
|
||||
return MapEntry(
|
||||
sessionId,
|
||||
session.copyWith(files: {}).toString().replaceFirst('files: {}', 'files: <${session.files.length} files>'),
|
||||
);
|
||||
}).toString();
|
||||
}
|
||||
|
||||
/// Starts a session.
|
||||
/// If [background] is true, then the session closes itself on success and no pages will be open
|
||||
/// If [background] is false, then this method will open pages by itself and waits for user input to close the session.
|
||||
@@ -108,7 +126,6 @@ class SendNotifier extends Notifier<Map<String, SendSessionState>> {
|
||||
)
|
||||
: null,
|
||||
),
|
||||
status: FileStatus.queue,
|
||||
token: null,
|
||||
thumbnail: file.thumbnail,
|
||||
asset: file.asset,
|
||||
@@ -127,6 +144,13 @@ class SendNotifier extends Notifier<Map<String, SendSessionState>> {
|
||||
),
|
||||
);
|
||||
|
||||
ref
|
||||
.notifier(fileTransferProvider)
|
||||
.setStatuses(
|
||||
sessionId: sessionId,
|
||||
statuses: {for (final f in selectedFiles) f.id: FileStatus.queue},
|
||||
);
|
||||
|
||||
if (!background) {
|
||||
// ignore: use_build_context_synchronously, unawaited_futures
|
||||
Routerino.context.push(
|
||||
@@ -154,7 +178,7 @@ class SendNotifier extends Notifier<Map<String, SendSessionState>> {
|
||||
return;
|
||||
}
|
||||
ref
|
||||
.notifier(progressProvider)
|
||||
.notifier(fileTransferProvider)
|
||||
.setProgress(
|
||||
sessionId: sessionId,
|
||||
fileId: id,
|
||||
@@ -178,7 +202,7 @@ class SendNotifier extends Notifier<Map<String, SendSessionState>> {
|
||||
|
||||
// Also set for files whose hashing failed, so the progress bar stays
|
||||
// consistent with the files that are left.
|
||||
ref.notifier(progressProvider).setProgress(sessionId: sessionId, fileId: id, progress: 1);
|
||||
ref.notifier(fileTransferProvider).setProgress(sessionId: sessionId, fileId: id, progress: 1);
|
||||
state = state.updateSession(
|
||||
sessionId: sessionId,
|
||||
state: (s) => s?.copyWith(hashedFileCount: s.hashedFileCount + 1),
|
||||
@@ -371,12 +395,17 @@ class SendNotifier extends Notifier<Map<String, SendSessionState>> {
|
||||
|
||||
final sendingFiles = {
|
||||
for (final file in requestState.files.values)
|
||||
file.file.id: fileMap.containsKey(file.file.id) ? file.copyWith(token: fileMap[file.file.id]) : file.copyWith(status: FileStatus.skipped),
|
||||
file.file.id: fileMap.containsKey(file.file.id) ? file.copyWith(token: fileMap[file.file.id]) : file,
|
||||
};
|
||||
|
||||
// The hash progress is no longer needed and must not be mistaken for
|
||||
// upload progress, which starts at zero for every file.
|
||||
ref.notifier(progressProvider).removeSession(sessionId);
|
||||
// Recreate the transfer state: the hash progress is no longer needed and must not be
|
||||
// mistaken for upload progress, which starts at zero for every file.
|
||||
final transferNotifier = ref.notifier(fileTransferProvider);
|
||||
transferNotifier.removeSession(sessionId);
|
||||
transferNotifier.setStatuses(
|
||||
sessionId: sessionId,
|
||||
statuses: {for (final file in sendingFiles.values) file.file.id: file.token != null ? FileStatus.queue : FileStatus.skipped},
|
||||
);
|
||||
|
||||
if (state[sessionId]?.background == false) {
|
||||
final background = ref.read(settingsProvider).sendMode == SendMode.multiple;
|
||||
@@ -422,17 +451,17 @@ class SendNotifier extends Notifier<Map<String, SendSessionState>> {
|
||||
return;
|
||||
}
|
||||
|
||||
final progress = ref.read(progressProvider);
|
||||
final transferNotifier = ref.read(fileTransferProvider);
|
||||
int currentBytes = 0;
|
||||
int totalBytes = 0;
|
||||
for (final sendingFile in session.files.values) {
|
||||
if (sendingFile.status == FileStatus.skipped) {
|
||||
if (transferNotifier.getStatus(sessionId: sessionId, fileId: sendingFile.file.id) == FileStatus.skipped) {
|
||||
// not accepted by the receiver
|
||||
continue;
|
||||
}
|
||||
final size = sendingFile.file.size;
|
||||
totalBytes += size;
|
||||
currentBytes += (progress.getProgress(sessionId: sessionId, fileId: sendingFile.file.id) * size).round();
|
||||
currentBytes += (transferNotifier.getProgress(sessionId: sessionId, fileId: sendingFile.file.id) * size).round();
|
||||
}
|
||||
|
||||
TransferNotification.update(
|
||||
@@ -470,7 +499,7 @@ class SendNotifier extends Notifier<Map<String, SendSessionState>> {
|
||||
if (state[sessionId]!.status != SessionStatus.sending) {
|
||||
_logger.info('Transfer was canceled.');
|
||||
} else {
|
||||
final hasError = sessionState.files.values.any((file) => file.status == FileStatus.failed);
|
||||
final hasError = ref.read(fileTransferProvider).getStatuses(sessionId).any((status) => status == FileStatus.failed);
|
||||
if (!hasError && sessionState.background == true) {
|
||||
// close session because everything is fine and it is in background
|
||||
closeSession(sessionId);
|
||||
@@ -515,13 +544,14 @@ class SendNotifier extends Notifier<Map<String, SendSessionState>> {
|
||||
if (isRetry) {
|
||||
_logger.info('Retrying ${file.file.fileName}');
|
||||
|
||||
ref.notifier(fileTransferProvider).setStatus(sessionId: sessionId, fileId: file.file.id, status: FileStatus.queue);
|
||||
state = state.updateSession(
|
||||
sessionId: sessionId,
|
||||
state: (s) => s?.copyWith(
|
||||
status: SessionStatus.sending,
|
||||
files: s.files.map((key, value) {
|
||||
if (key == file.file.id) {
|
||||
return MapEntry(key, value.copyWith(status: FileStatus.queue, errorMessage: null));
|
||||
return MapEntry(key, value.copyWith(errorMessage: null));
|
||||
}
|
||||
return MapEntry(key, value);
|
||||
}),
|
||||
@@ -535,8 +565,7 @@ class SendNotifier extends Notifier<Map<String, SendSessionState>> {
|
||||
);
|
||||
|
||||
if (isRetry) {
|
||||
final state = this.state[sessionId];
|
||||
if (state != null && state.files.values.map((e) => e.status).isFinishedOrError) {
|
||||
if (state[sessionId] != null && ref.read(fileTransferProvider).getStatuses(sessionId).isFinishedOrError) {
|
||||
_finish(sessionId: sessionId);
|
||||
}
|
||||
}
|
||||
@@ -598,13 +627,10 @@ class SendNotifier extends Notifier<Map<String, SendSessionState>> {
|
||||
switch (event) {
|
||||
case HttpUploadFileStartedEvent():
|
||||
_logger.info('Sending ${state[sessionId]?.files[event.fileId]?.file.fileName}');
|
||||
state = state.updateSession(
|
||||
sessionId: sessionId,
|
||||
state: (s) => s?.withFileStatus(event.fileId, FileStatus.sending, null),
|
||||
);
|
||||
ref.notifier(fileTransferProvider).setStatus(sessionId: sessionId, fileId: event.fileId, status: FileStatus.sending);
|
||||
case HttpUploadFileProgressEvent():
|
||||
ref
|
||||
.notifier(progressProvider)
|
||||
.notifier(fileTransferProvider)
|
||||
.setProgress(
|
||||
sessionId: sessionId,
|
||||
fileId: event.fileId,
|
||||
@@ -614,22 +640,20 @@ class SendNotifier extends Notifier<Map<String, SendSessionState>> {
|
||||
case HttpUploadFileFinishedEvent():
|
||||
// set progress to 100% when successfully finished
|
||||
ref
|
||||
.notifier(progressProvider)
|
||||
.notifier(fileTransferProvider)
|
||||
.setProgress(
|
||||
sessionId: sessionId,
|
||||
fileId: event.fileId,
|
||||
progress: 1,
|
||||
);
|
||||
_updateForegroundServiceProgress(sessionId);
|
||||
state = state.updateSession(
|
||||
sessionId: sessionId,
|
||||
state: (s) => s?.withFileStatus(event.fileId, FileStatus.finished, null),
|
||||
);
|
||||
ref.notifier(fileTransferProvider).setStatus(sessionId: sessionId, fileId: event.fileId, status: FileStatus.finished);
|
||||
case HttpUploadFileFailedEvent():
|
||||
_logger.warning('Error while sending file ${state[sessionId]?.files[event.fileId]?.file.fileName}: ${event.error}');
|
||||
ref.notifier(fileTransferProvider).setStatus(sessionId: sessionId, fileId: event.fileId, status: FileStatus.failed);
|
||||
state = state.updateSession(
|
||||
sessionId: sessionId,
|
||||
state: (s) => s?.withFileStatus(event.fileId, FileStatus.failed, event.error),
|
||||
state: (s) => s?.withFileError(event.fileId, event.error),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -637,13 +661,21 @@ class SendNotifier extends Notifier<Map<String, SendSessionState>> {
|
||||
// the whole task failed, mark all files of this task that did not finish as failed
|
||||
_logger.warning('Error while sending files', e, st);
|
||||
final error = e.humanErrorMessage;
|
||||
final fileIds = uploadFiles.map((file) => file.fileId).toSet();
|
||||
final transferNotifier = ref.notifier(fileTransferProvider);
|
||||
final failedFileIds = uploadFiles
|
||||
.map((file) => file.fileId)
|
||||
.where((id) => const {FileStatus.queue, FileStatus.sending}.contains(transferNotifier.getStatus(sessionId: sessionId, fileId: id)))
|
||||
.toSet();
|
||||
transferNotifier.setStatuses(
|
||||
sessionId: sessionId,
|
||||
statuses: {for (final id in failedFileIds) id: FileStatus.failed},
|
||||
);
|
||||
state = state.updateSession(
|
||||
sessionId: sessionId,
|
||||
state: (s) => s?.copyWith(
|
||||
files: s.files.map((key, value) {
|
||||
if (fileIds.contains(key) && (value.status == FileStatus.queue || value.status == FileStatus.sending)) {
|
||||
return MapEntry(key, value.copyWith(status: FileStatus.failed, errorMessage: error));
|
||||
if (failedFileIds.contains(key)) {
|
||||
return MapEntry(key, value.copyWith(errorMessage: error));
|
||||
}
|
||||
return MapEntry(key, value);
|
||||
}),
|
||||
@@ -756,7 +788,7 @@ class SendNotifier extends Notifier<Map<String, SendSessionState>> {
|
||||
}
|
||||
_prepareUploadCancelTokens.clear();
|
||||
state = {};
|
||||
ref.notifier(progressProvider).removeAllSessions();
|
||||
ref.notifier(fileTransferProvider).removeAllSessions();
|
||||
}
|
||||
|
||||
void setBackground(String sessionId, bool background) {
|
||||
@@ -784,19 +816,18 @@ extension on Map<String, SendSessionState> {
|
||||
}
|
||||
|
||||
Map<String, SendSessionState> removeSession(Ref ref, String sessionId) {
|
||||
ref.notifier(progressProvider).removeSession(sessionId);
|
||||
ref.notifier(fileTransferProvider).removeSession(sessionId);
|
||||
return {...this}..remove(sessionId);
|
||||
}
|
||||
}
|
||||
|
||||
extension on SendSessionState {
|
||||
SendSessionState withFileStatus(String fileId, FileStatus status, String? errorMessage) {
|
||||
SendSessionState withFileError(String fileId, String? errorMessage) {
|
||||
return copyWith(
|
||||
files: {...files}
|
||||
..update(
|
||||
fileId,
|
||||
(file) => file.copyWith(
|
||||
status: status,
|
||||
errorMessage: errorMessage,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -11,12 +11,12 @@ 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/favorites_provider.dart';
|
||||
import 'package:localsend_app/provider/file_transfer_provider.dart';
|
||||
import 'package:localsend_app/provider/http_provider.dart';
|
||||
import 'package:localsend_app/provider/logging/discovery_logs_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_utils.dart';
|
||||
import 'package:localsend_app/provider/progress_provider.dart';
|
||||
import 'package:localsend_app/provider/receive_history_provider.dart';
|
||||
import 'package:localsend_app/provider/security_provider.dart';
|
||||
import 'package:localsend_app/provider/selection/selected_receiving_files_provider.dart';
|
||||
@@ -102,7 +102,6 @@ class ReceiveController {
|
||||
for (final file in files.values)
|
||||
file.id: ReceivingFile(
|
||||
file: file,
|
||||
status: FileStatus.queue,
|
||||
token: null,
|
||||
desiredName: null,
|
||||
path: null,
|
||||
@@ -120,6 +119,13 @@ class ReceiveController {
|
||||
),
|
||||
);
|
||||
|
||||
server.ref
|
||||
.notifier(fileTransferProvider)
|
||||
.setStatuses(
|
||||
sessionId: sessionId,
|
||||
statuses: {for (final file in files.values) file.id: FileStatus.queue},
|
||||
);
|
||||
|
||||
bool quickSave = settings.quickSave && server.getState().session?.message == null;
|
||||
final quickSaveFromFavorites = settings.quickSaveFromFavorites && server.getState().session?.message == null;
|
||||
if (quickSaveFromFavorites) {
|
||||
@@ -176,7 +182,9 @@ class ReceiveController {
|
||||
}
|
||||
|
||||
final receiveProvider = ViewProvider((ref) {
|
||||
final session = ref.watch(serverProvider.select((state) => state?.session));
|
||||
// No select: comparing the selected session runs the dart_mappable deep equality
|
||||
// over the whole files map on every state change.
|
||||
final session = ref.watch(serverProvider)?.session;
|
||||
return ReceivePageVm(
|
||||
status: session?.status,
|
||||
sender: session?.sender ?? Device.empty,
|
||||
@@ -247,21 +255,17 @@ class ReceiveController {
|
||||
}
|
||||
|
||||
// begin of actual file transfer
|
||||
server.setState(
|
||||
(oldState) => oldState?.copyWith(
|
||||
session: receiveState.copyWith(
|
||||
files: {...receiveState.files}
|
||||
..update(
|
||||
fileId,
|
||||
(_) => receivingFile.copyWith(
|
||||
status: FileStatus.sending,
|
||||
),
|
||||
),
|
||||
startTime: receiveState.startTime ?? DateTime.now().millisecondsSinceEpoch,
|
||||
status: SessionStatus.sending, // in case it was finishedWithErrors and user retries a failed file
|
||||
server.ref.notifier(fileTransferProvider).setStatus(sessionId: event.sessionId, fileId: fileId, status: FileStatus.sending);
|
||||
if (receiveState.startTime == null || receiveState.status != SessionStatus.sending) {
|
||||
server.setState(
|
||||
(oldState) => oldState?.copyWith(
|
||||
session: receiveState.copyWith(
|
||||
startTime: receiveState.startTime ?? DateTime.now().millisecondsSinceEpoch,
|
||||
status: SessionStatus.sending, // in case it was finishedWithErrors and user retries a failed file
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// The receive progress of a file reported by the server isolate.
|
||||
@@ -272,7 +276,7 @@ class ReceiveController {
|
||||
}
|
||||
|
||||
server.ref
|
||||
.notifier(progressProvider)
|
||||
.notifier(fileTransferProvider)
|
||||
.setProgress(
|
||||
sessionId: event.sessionId,
|
||||
fileId: event.fileId,
|
||||
@@ -290,7 +294,7 @@ class ReceiveController {
|
||||
return;
|
||||
}
|
||||
|
||||
final progress = server.ref.read(progressProvider);
|
||||
final transferNotifier = server.ref.read(fileTransferProvider);
|
||||
int currentBytes = 0;
|
||||
int totalBytes = 0;
|
||||
for (final receivingFile in session.files.values) {
|
||||
@@ -300,7 +304,7 @@ class ReceiveController {
|
||||
}
|
||||
final size = receivingFile.file.size;
|
||||
totalBytes += size;
|
||||
currentBytes += (progress.getProgress(sessionId: session.sessionId, fileId: receivingFile.file.id) * size).round();
|
||||
currentBytes += (transferNotifier.getProgress(sessionId: session.sessionId, fileId: receivingFile.file.id) * size).round();
|
||||
}
|
||||
|
||||
TransferNotification.update(
|
||||
@@ -332,11 +336,11 @@ class ReceiveController {
|
||||
final error = event.error;
|
||||
|
||||
if (error == null) {
|
||||
server.ref.notifier(fileTransferProvider).setStatus(sessionId: event.sessionId, fileId: fileId, status: FileStatus.finished);
|
||||
server.setState(
|
||||
(oldState) => oldState?.copyWith(
|
||||
session: oldState.session?.fileFinished(
|
||||
fileId: fileId,
|
||||
status: FileStatus.finished,
|
||||
path: filePath,
|
||||
savedToGallery: event.savedToGallery,
|
||||
errorMessage: null,
|
||||
@@ -361,11 +365,11 @@ class ReceiveController {
|
||||
),
|
||||
);
|
||||
} else {
|
||||
server.ref.notifier(fileTransferProvider).setStatus(sessionId: event.sessionId, fileId: fileId, status: FileStatus.failed);
|
||||
server.setState(
|
||||
(oldState) => oldState?.copyWith(
|
||||
session: oldState.session?.fileFinished(
|
||||
fileId: fileId,
|
||||
status: FileStatus.failed,
|
||||
path: null,
|
||||
savedToGallery: false,
|
||||
errorMessage: error,
|
||||
@@ -375,7 +379,7 @@ class ReceiveController {
|
||||
}
|
||||
|
||||
server.ref
|
||||
.notifier(progressProvider)
|
||||
.notifier(fileTransferProvider)
|
||||
.setProgress(
|
||||
sessionId: receiveState.sessionId,
|
||||
fileId: fileId,
|
||||
@@ -389,11 +393,12 @@ class ReceiveController {
|
||||
|
||||
_updateForegroundServiceProgress(session);
|
||||
|
||||
if (allowedStates.contains(session.status) && session.files.values.map((e) => e.status).isFinishedOrError) {
|
||||
final statuses = server.ref.read(fileTransferProvider).getStatuses(session.sessionId);
|
||||
if (allowedStates.contains(session.status) && statuses.isFinishedOrError) {
|
||||
// The transfer is over, the process no longer needs to be kept alive for it.
|
||||
TransferNotification.stop(session.sessionId);
|
||||
|
||||
final hasError = session.files.values.any((f) => f.status == FileStatus.failed);
|
||||
final hasError = statuses.any((status) => status == FileStatus.failed);
|
||||
server.setState(
|
||||
(oldState) => oldState?.copyWith(
|
||||
session: oldState.session!.copyWith(
|
||||
@@ -543,7 +548,6 @@ class ReceiveController {
|
||||
entry.file.id,
|
||||
ReceivingFile(
|
||||
file: entry.file,
|
||||
status: desiredName != null ? FileStatus.queue : FileStatus.skipped,
|
||||
token: null,
|
||||
desiredName: desiredName,
|
||||
path: null,
|
||||
@@ -558,6 +562,15 @@ class ReceiveController {
|
||||
},
|
||||
);
|
||||
|
||||
server.ref
|
||||
.notifier(fileTransferProvider)
|
||||
.setStatuses(
|
||||
sessionId: session.sessionId,
|
||||
statuses: {
|
||||
for (final file in session.files.values) file.file.id: fileNameMap.containsKey(file.file.id) ? FileStatus.queue : FileStatus.skipped,
|
||||
},
|
||||
);
|
||||
|
||||
// The storage permission only exists below Android 13 (scoped storage): newer versions
|
||||
// auto-deny the request, but the round trip through the system permission activity
|
||||
// still blocks the UI noticeably.
|
||||
@@ -678,7 +691,7 @@ class ReceiveController {
|
||||
session: null,
|
||||
),
|
||||
);
|
||||
server.ref.notifier(progressProvider).removeSession(sessionId);
|
||||
server.ref.notifier(fileTransferProvider).removeSession(sessionId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -709,7 +722,6 @@ void _cancelBySender(ServerUtils server) {
|
||||
extension on ReceiveSessionState {
|
||||
ReceiveSessionState fileFinished({
|
||||
required String fileId,
|
||||
required FileStatus status,
|
||||
required String? path,
|
||||
required bool savedToGallery,
|
||||
required String? errorMessage,
|
||||
@@ -719,7 +731,6 @@ extension on ReceiveSessionState {
|
||||
..update(
|
||||
fileId,
|
||||
(file) => file.copyWith(
|
||||
status: status,
|
||||
path: path,
|
||||
savedToGallery: savedToGallery,
|
||||
errorMessage: errorMessage,
|
||||
|
||||
@@ -153,7 +153,6 @@ class _InitSessionState extends ReduxAction<WebRTCReceiveService, WebRTCReceiveS
|
||||
// for (final file in files)
|
||||
// file.id: ReceivingFile(
|
||||
// file: file,
|
||||
// status: FileStatus.queue,
|
||||
// token: null,
|
||||
// desiredName: null,
|
||||
// path: null,
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
import 'package:refena_flutter/refena_flutter.dart';
|
||||
|
||||
/// A provider holding the progress of the send process.
|
||||
/// It is implemented as [ChangeNotifier] for performance reasons.
|
||||
final progressProvider = ChangeNotifierProvider((ref) => ProgressNotifier());
|
||||
|
||||
class ProgressNotifier extends ChangeNotifier {
|
||||
final _progressMap = <String, Map<String, double>>{}; // session id -> (file id -> 0..1)
|
||||
|
||||
void setProgress({required String sessionId, required String fileId, required double progress}) {
|
||||
Map<String, double>? progressMap = _progressMap[sessionId];
|
||||
if (progressMap == null) {
|
||||
progressMap = {};
|
||||
_progressMap[sessionId] = progressMap;
|
||||
}
|
||||
progressMap[fileId] = progress;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
double getProgress({required String sessionId, required String fileId}) {
|
||||
return _progressMap[sessionId]?[fileId] ?? 0.0;
|
||||
}
|
||||
|
||||
void removeSession(String sessionId) {
|
||||
_progressMap.remove(sessionId);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void removeAllSessions() {
|
||||
_progressMap.clear();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Only for debug purposes
|
||||
Map<String, Map<String, double>> getData() {
|
||||
return _progressMap;
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@ class VersionData {
|
||||
VersionData({
|
||||
required this.version,
|
||||
required this.buildNumber,
|
||||
}): combinedString = '$version ($buildNumber)';
|
||||
}) : combinedString = '$version ($buildNumber)';
|
||||
}
|
||||
|
||||
final versionProvider = FutureProvider((ref) async {
|
||||
|
||||
Reference in New Issue
Block a user