refactor: migrate to dart_mappable

This commit is contained in:
Tien Do Nam
2023-10-09 23:36:29 +02:00
parent 412d9b04fe
commit 453bc6efbd
33 changed files with 608 additions and 420 deletions
+1 -1
View File
@@ -7,7 +7,7 @@
*.g.dart
*.gen.dart
*.freezed.dart
*.mapper.dart
.refena_inspector/
/secrets
+7
View File
@@ -16,3 +16,10 @@ targets:
- 'bn'
description: |
"LocalSend" is a file sharing app that allows you to send files to other devices on the same network.
dart_mappable_builder:
options:
renameMethods:
fromJson: deserialize
toJson: serialize
fromMap: fromJson
toMap: toJson
+4
View File
@@ -1,9 +1,11 @@
import 'dart:async';
import 'dart:io';
import 'package:dart_mappable/dart_mappable.dart';
import 'package:flutter/widgets.dart';
import 'package:localsend_app/constants.dart';
import 'package:localsend_app/gen/strings.g.dart';
import 'package:localsend_app/model/dto/file_dto.dart';
import 'package:localsend_app/pages/home_page.dart';
import 'package:localsend_app/provider/animation_provider.dart';
import 'package:localsend_app/provider/dio_provider.dart';
@@ -46,6 +48,8 @@ Future<(PersistenceService, bool)> preInit(List<String> args) async {
}
});
MapperContainer.globals.use(const FileDtoMapper());
final persistenceService = await PersistenceService.initialize();
// Register default plural resolver
+21 -13
View File
@@ -1,22 +1,30 @@
import 'dart:typed_data';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:dart_mappable/dart_mappable.dart';
import 'package:localsend_app/model/file_type.dart';
import 'package:wechat_assets_picker/wechat_assets_picker.dart';
part 'cross_file.freezed.dart';
part 'cross_file.mapper.dart';
/// Common file model to avoid any third party libraries in the core logic.
/// This model is used during the file selection phase.
@freezed
class CrossFile with _$CrossFile {
const factory CrossFile({
required String name,
required FileType fileType,
required int size,
required Uint8List? thumbnail,
required AssetEntity? asset, // for thumbnails
required String? path,
required List<int>? bytes, // if type message, then UTF-8 encoded
}) = _CrossFile;
@MappableClass()
class CrossFile with CrossFileMappable {
final String name;
final FileType fileType;
final int size;
final Uint8List? thumbnail;
final AssetEntity? asset; // for thumbnails
final String? path;
final List<int>? bytes; // if type message, then UTF-8 encoded
const CrossFile({
required this.name,
required this.fileType,
required this.size,
required this.thumbnail,
required this.asset,
required this.path,
required this.bytes,
});
}
+26 -15
View File
@@ -1,8 +1,9 @@
import 'package:dart_mappable/dart_mappable.dart';
import 'package:flutter/material.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'device.freezed.dart';
part 'device.mapper.dart';
@MappableEnum(defaultValue: DeviceType.desktop)
enum DeviceType {
mobile(Icons.smartphone),
desktop(Icons.computer),
@@ -17,17 +18,27 @@ enum DeviceType {
/// Internal device model.
/// It gets not serialized.
@freezed
class Device with _$Device {
const factory Device({
required String ip,
required String version,
required int port,
required bool https,
required String fingerprint,
required String alias,
required String? deviceModel,
required DeviceType deviceType,
required bool download,
}) = _Device;
@MappableClass()
class Device with DeviceMappable {
final String ip;
final String version;
final int port;
final bool https;
final String fingerprint;
final String alias;
final String? deviceModel;
final DeviceType deviceType;
final bool download;
const Device({
required this.ip,
required this.version,
required this.port,
required this.https,
required this.fingerprint,
required this.alias,
required this.deviceModel,
required this.deviceType,
required this.download,
});
}
+47 -47
View File
@@ -1,10 +1,9 @@
import 'package:collection/collection.dart';
import 'package:dart_mappable/dart_mappable.dart';
import 'package:localsend_app/model/file_type.dart';
import 'package:mime/mime.dart';
/// The file DTO that is sent between server and client.
/// Custom implementation of freezed & json_serializable to handle legacy enums.
/// The copyWith method is not implemented.
class FileDto {
final String id; // unique inside session
final String fileName;
@@ -29,10 +28,6 @@ class FileDto {
String lookupMime() => lookupMimeType(fileName) ?? 'application/octet-stream';
factory FileDto.fromJson(Map<String, Object?> json) => _parseFileDto(json);
Map<String, dynamic> toJson() => _fileDtoToJson(this);
@override
bool operator ==(Object other) =>
identical(this, other) ||
@@ -50,49 +45,54 @@ class FileDto {
int get hashCode => Object.hash(id, fileName, size, fileType, hash, preview, legacy);
}
/// This deserializer handles both legacy and mime types.
FileDto _parseFileDto(Map<String, Object?> json) {
final String rawFileType = json['fileType'] as String;
final FileType fileType;
if (rawFileType.contains('/')) {
// parse mime
if (rawFileType.startsWith('image/')) {
fileType = FileType.image;
} else if (rawFileType.startsWith('video/')) {
fileType = FileType.video;
} else if (rawFileType == 'application/pdf') {
fileType = FileType.pdf;
} else if (rawFileType.startsWith('text/')) {
fileType = FileType.text;
} else if (rawFileType == 'application/vnd.android.package-archive') {
fileType = FileType.apk;
class FileDtoMapper extends SimpleMapper<FileDto> {
const FileDtoMapper();
@override
FileDto decode(dynamic value) {
final map = value as Map<String, dynamic>;
final String rawFileType = map['fileType'] as String;
final FileType fileType;
if (rawFileType.contains('/')) {
// parse mime
if (rawFileType.startsWith('image/')) {
fileType = FileType.image;
} else if (rawFileType.startsWith('video/')) {
fileType = FileType.video;
} else if (rawFileType == 'application/pdf') {
fileType = FileType.pdf;
} else if (rawFileType.startsWith('text/')) {
fileType = FileType.text;
} else if (rawFileType == 'application/vnd.android.package-archive') {
fileType = FileType.apk;
} else {
fileType = FileType.other;
}
} else {
fileType = FileType.other;
// parse legacy enum to internal internal enum
fileType = FileType.values.firstWhereOrNull((e) => e.name == rawFileType) ?? FileType.other;
}
} else {
// parse legacy enum to internal internal enum
fileType = FileType.values.firstWhereOrNull((e) => e.name == rawFileType) ?? FileType.other;
return FileDto(
id: map['id'] as String,
fileName: map['fileName'] as String,
size: map['size'] as int,
fileType: fileType,
hash: map['hash'] as String?,
preview: map['preview'] as String?,
legacy: false,
);
}
return FileDto(
id: json['id'] as String,
fileName: json['fileName'] as String,
size: json['size'] as int,
fileType: fileType,
hash: json['hash'] as String?,
preview: json['preview'] as String?,
legacy: false,
);
}
/// This serializer checks the legacy flag and serializes the file type accordingly.
Map<String, dynamic> _fileDtoToJson(FileDto instance) {
return {
'id': instance.id,
'fileName': instance.fileName,
'size': instance.size,
'fileType': instance.legacy ? instance.fileType.name : instance.lookupMime(),
if (instance.hash != null) 'hash': instance.hash,
if (instance.preview != null) 'preview': instance.preview,
};
@override
dynamic encode(FileDto self) {
return {
'id': self.id,
'fileName': self.fileName,
'size': self.size,
'fileType': self.legacy ? self.fileType.name : self.lookupMime(),
if (self.hash != null) 'hash': self.hash,
if (self.preview != null) 'preview': self.preview,
};
}
}
+20 -15
View File
@@ -1,23 +1,28 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:dart_mappable/dart_mappable.dart';
import 'package:localsend_app/constants.dart';
import 'package:localsend_app/model/device.dart';
part 'info_dto.freezed.dart';
part 'info_dto.g.dart';
part 'info_dto.mapper.dart';
@freezed
class InfoDto with _$InfoDto {
const factory InfoDto({
required String alias,
required String? version, // v2, format: major.minor
required String? deviceModel,
@JsonKey(unknownEnumValue: DeviceType.desktop) // ignore: invalid_annotation_target
required DeviceType? deviceType,
required String? fingerprint, // v2
required bool? download, // v2
}) = _InfoDto;
@MappableClass()
class InfoDto with InfoDtoMappable {
final String alias;
final String? version; // v2, format: major.minor
final String? deviceModel;
final DeviceType? deviceType;
final String? fingerprint; // v2
final bool? download; // v2
factory InfoDto.fromJson(Map<String, Object?> json) => _$InfoDtoFromJson(json);
const InfoDto({
required this.alias,
required this.version,
required this.deviceModel,
required this.deviceType,
required this.fingerprint,
required this.download,
});
static const fromJson = InfoDtoMapper.fromJson;
}
extension InfoToDeviceExt on InfoDto {
+24 -17
View File
@@ -1,29 +1,36 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:dart_mappable/dart_mappable.dart';
import 'package:localsend_app/constants.dart';
import 'package:localsend_app/model/device.dart';
import 'package:localsend_app/model/dto/multicast_dto.dart';
part 'info_register_dto.freezed.dart';
part 'info_register_dto.g.dart';
part 'info_register_dto.mapper.dart';
/// Used only for /prepare-upload to be compatible with v1.
/// The [fingerprint] does not exist in v1, so it is nullable here.
/// TODO: replace with [RegisterDto] when v1 compatibility is removed
@freezed
class InfoRegisterDto with _$InfoRegisterDto {
const factory InfoRegisterDto({
required String alias,
required String? version, // v2, format: major.minor
required String? deviceModel,
@JsonKey(unknownEnumValue: DeviceType.desktop) // ignore: invalid_annotation_target
required DeviceType? deviceType,
required String? fingerprint,
required int? port, // v2
required ProtocolType? protocol, // v2
required bool? download, // v2
}) = _InfoRegisterDto;
@MappableClass()
class InfoRegisterDto with InfoRegisterDtoMappable {
final String alias;
final String? version; // v2, format: major.minor
final String? deviceModel;
final DeviceType? deviceType;
final String? fingerprint;
final int? port; // v2
final ProtocolType? protocol; // v2
final bool? download; // v2
factory InfoRegisterDto.fromJson(Map<String, Object?> json) => _$InfoRegisterDtoFromJson(json);
const InfoRegisterDto({
required this.alias,
required this.version,
required this.deviceModel,
required this.deviceType,
required this.fingerprint,
required this.port,
required this.protocol,
required this.download,
});
static const fromJson = InfoRegisterDtoMapper.fromJson;
}
extension RegisterDtoExt on InfoRegisterDto {
+29 -18
View File
@@ -1,28 +1,39 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:dart_mappable/dart_mappable.dart';
import 'package:localsend_app/constants.dart';
import 'package:localsend_app/model/device.dart';
part 'multicast_dto.freezed.dart';
part 'multicast_dto.g.dart';
part 'multicast_dto.mapper.dart';
@MappableEnum(defaultValue: ProtocolType.https)
enum ProtocolType { http, https }
@freezed
class MulticastDto with _$MulticastDto {
const factory MulticastDto({
required String alias,
required String? version, // v2, format: major.minor
required String? deviceModel,
required DeviceType? deviceType, // nullable since v2
required String fingerprint,
required int? port, // v2
required ProtocolType? protocol, // v2
required bool? download, // v2
required bool? announcement, // v1
required bool? announce, // v2
}) = _MulticastDto;
@MappableClass()
class MulticastDto with MulticastDtoMappable {
final String alias;
final String? version; // v2, format: major.minor
final String? deviceModel;
final DeviceType? deviceType; // nullable since v2
final String fingerprint;
final int? port; // v2
final ProtocolType? protocol; // v2
final bool? download; // v2
final bool? announcement; // v1
final bool? announce; // v2
factory MulticastDto.fromJson(Map<String, Object?> json) => _$MulticastDtoFromJson(json);
const MulticastDto({
required this.alias,
required this.version,
required this.deviceModel,
required this.deviceType,
required this.fingerprint,
required this.port,
required this.protocol,
required this.download,
required this.announcement,
required this.announce,
});
static const fromJson = MulticastDtoMapper.fromJson;
}
extension InfoToDeviceExt on MulticastDto {
@@ -1,16 +1,18 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:dart_mappable/dart_mappable.dart';
import 'package:localsend_app/model/dto/file_dto.dart';
import 'package:localsend_app/model/dto/info_register_dto.dart';
part 'prepare_upload_request_dto.freezed.dart';
part 'prepare_upload_request_dto.g.dart';
part 'prepare_upload_request_dto.mapper.dart';
@freezed
class PrepareUploadRequestDto with _$PrepareUploadRequestDto {
const factory PrepareUploadRequestDto({
required InfoRegisterDto info,
required Map<String, FileDto> files,
}) = _PrepareUploadRequestDto;
@MappableClass()
class PrepareUploadRequestDto with PrepareUploadRequestDtoMappable {
final InfoRegisterDto info;
final Map<String, FileDto> files;
factory PrepareUploadRequestDto.fromJson(Map<String, Object?> json) => _$PrepareUploadRequestDtoFromJson(json);
const PrepareUploadRequestDto({
required this.info,
required this.files,
});
static const fromJson = PrepareUploadRequestDtoMapper.fromJson;
}
@@ -1,14 +1,16 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:dart_mappable/dart_mappable.dart';
part 'prepare_upload_response_dto.freezed.dart';
part 'prepare_upload_response_dto.g.dart';
part 'prepare_upload_response_dto.mapper.dart';
@freezed
class PrepareUploadResponseDto with _$PrepareUploadResponseDto {
const factory PrepareUploadResponseDto({
required String sessionId,
required Map<String, String> files,
}) = _PrepareUploadResponseDto;
@MappableClass()
class PrepareUploadResponseDto with PrepareUploadResponseDtoMappable {
final String sessionId;
final Map<String, String> files;
factory PrepareUploadResponseDto.fromJson(Map<String, Object?> json) => _$PrepareUploadResponseDtoFromJson(json);
const PrepareUploadResponseDto({
required this.sessionId,
required this.files,
});
static const fromJson = PrepareUploadResponseDtoMapper.fromJson;
}
@@ -1,17 +1,18 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:dart_mappable/dart_mappable.dart';
import 'package:localsend_app/model/dto/file_dto.dart';
import 'package:localsend_app/model/dto/info_dto.dart';
part 'receive_request_response_dto.freezed.dart';
part 'receive_request_response_dto.g.dart';
part 'receive_request_response_dto.mapper.dart';
@freezed
class ReceiveRequestResponseDto with _$ReceiveRequestResponseDto {
const factory ReceiveRequestResponseDto({
required InfoDto info,
required String sessionId,
required Map<String, FileDto> files,
}) = _ReceiveRequestResponseDto;
@MappableClass()
class ReceiveRequestResponseDto with ReceiveRequestResponseDtoMappable {
final InfoDto info;
final String sessionId;
final Map<String, FileDto> files;
factory ReceiveRequestResponseDto.fromJson(Map<String, Object?> json) => _$ReceiveRequestResponseDtoFromJson(json);
const ReceiveRequestResponseDto({
required this.info,
required this.sessionId,
required this.files,
});
}
+24 -17
View File
@@ -1,26 +1,33 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:dart_mappable/dart_mappable.dart';
import 'package:localsend_app/constants.dart';
import 'package:localsend_app/model/device.dart';
import 'package:localsend_app/model/dto/multicast_dto.dart';
part 'register_dto.freezed.dart';
part 'register_dto.g.dart';
part 'register_dto.mapper.dart';
@freezed
class RegisterDto with _$RegisterDto {
const factory RegisterDto({
required String alias,
required String? version, // v2, format: major.minor
required String? deviceModel,
@JsonKey(unknownEnumValue: DeviceType.desktop) // ignore: invalid_annotation_target
required DeviceType? deviceType,
required String fingerprint,
required int? port, // v2
required ProtocolType? protocol, // v2
required bool? download, // v2
}) = _RegisterDto;
@MappableClass()
class RegisterDto with RegisterDtoMappable {
final String alias;
final String? version; // v2, format: major.minor
final String? deviceModel;
final DeviceType? deviceType;
final String fingerprint;
final int? port; // v2
final ProtocolType? protocol; // v2
final bool? download; // v2
factory RegisterDto.fromJson(Map<String, Object?> json) => _$RegisterDtoFromJson(json);
const RegisterDto({
required this.alias,
required this.version,
required this.deviceModel,
required this.deviceType,
required this.fingerprint,
required this.port,
required this.protocol,
required this.download,
});
static const fromJson = RegisterDtoMapper.fromJson;
}
extension RegisterDtoExt on RegisterDto {
+4
View File
@@ -1,7 +1,11 @@
import 'package:dart_mappable/dart_mappable.dart';
import 'package:flutter/material.dart';
part 'file_type.mapper.dart';
/// Categorization of one file.
/// We use this information for a better UX.
@MappableEnum(defaultValue: FileType.other)
enum FileType {
image(Icons.image),
video(Icons.movie),
+11 -8
View File
@@ -1,11 +1,14 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:dart_mappable/dart_mappable.dart';
part 'log_entry.freezed.dart';
part 'log_entry.mapper.dart';
@freezed
class LogEntry with _$LogEntry {
const factory LogEntry({
required DateTime timestamp,
required String log,
}) = _LogEntry;
@MappableClass()
class LogEntry with LogEntryMappable {
final DateTime timestamp;
final String log;
const LogEntry({
required this.timestamp,
required this.log,
});
}
@@ -1,16 +1,20 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:dart_mappable/dart_mappable.dart';
part 'stored_security_context.freezed.dart';
part 'stored_security_context.g.dart';
part 'stored_security_context.mapper.dart';
@freezed
class StoredSecurityContext with _$StoredSecurityContext {
const factory StoredSecurityContext({
required String privateKey,
required String publicKey,
required String certificate,
required String certificateHash,
}) = _StoredSecurityContext;
@MappableClass()
class StoredSecurityContext with StoredSecurityContextMappable {
final String privateKey;
final String publicKey;
final String certificate;
final String certificateHash;
factory StoredSecurityContext.fromJson(Map<String, Object?> json) => _$StoredSecurityContextFromJson(json);
const StoredSecurityContext({
required this.privateKey,
required this.publicKey,
required this.certificate,
required this.certificateHash,
});
static const fromJson = StoredSecurityContextMapper.fromJson;
}
+24 -18
View File
@@ -1,27 +1,31 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:dart_mappable/dart_mappable.dart';
import 'package:intl/intl.dart';
import 'package:localsend_app/gen/strings.g.dart';
import 'package:localsend_app/model/file_type.dart';
part 'receive_history_entry.freezed.dart';
part 'receive_history_entry.g.dart';
part 'receive_history_entry.mapper.dart';
@freezed
class ReceiveHistoryEntry with _$ReceiveHistoryEntry {
const ReceiveHistoryEntry._(); // allow custom getters
@MappableClass()
class ReceiveHistoryEntry with ReceiveHistoryEntryMappable {
final String id;
final String fileName;
final FileType fileType;
final String? path;
final bool savedToGallery;
final int fileSize;
final String senderAlias;
final DateTime timestamp;
const factory ReceiveHistoryEntry({
required String id,
required String fileName,
required FileType fileType,
required String? path,
required bool savedToGallery,
required int fileSize,
required String senderAlias,
required DateTime timestamp,
}) = _ReceiveHistoryEntry;
factory ReceiveHistoryEntry.fromJson(Map<String, Object?> json) => _$ReceiveHistoryEntryFromJson(json);
const ReceiveHistoryEntry({
required this.id,
required this.fileName,
required this.fileType,
required this.path,
required this.savedToGallery,
required this.fileSize,
required this.senderAlias,
required this.timestamp,
});
/// Format string using the intl package.
/// Because the raw timestamp is saved in UTC, we need to transform it to local time zone first.
@@ -29,4 +33,6 @@ class ReceiveHistoryEntry with _$ReceiveHistoryEntry {
final localTimestamp = timestamp.toLocal();
return '${DateFormat.yMd(LocaleSettings.currentLocale.languageTag).format(localTimestamp)} ${DateFormat.jm(LocaleSettings.currentLocale.languageTag).format(localTimestamp)}';
}
static const fromJson = ReceiveHistoryEntryMapper.fromJson;
}
+11 -8
View File
@@ -1,12 +1,15 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:dart_mappable/dart_mappable.dart';
import 'package:localsend_app/model/device.dart';
part 'nearby_devices_state.freezed.dart';
part 'nearby_devices_state.mapper.dart';
@freezed
class NearbyDevicesState with _$NearbyDevicesState {
const factory NearbyDevicesState({
required Set<String> runningIps, // list of local ips
required Map<String, Device> devices, // ip -> device
}) = _NearbyDevicesState;
@MappableClass()
class NearbyDevicesState with NearbyDevicesStateMappable {
final Set<String> runningIps; // list of local ips
final Map<String, Device> devices; // ip -> device
const NearbyDevicesState({
required this.runningIps,
required this.devices,
});
}
+11 -8
View File
@@ -1,11 +1,14 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:dart_mappable/dart_mappable.dart';
part 'network_state.freezed.dart';
part 'network_state.mapper.dart';
@freezed
class NetworkState with _$NetworkState {
const factory NetworkState({
required List<String> localIps,
required bool initialized,
}) = _NetworkState;
@MappableClass()
class NetworkState with NetworkStateMappable {
final List<String> localIps;
final bool initialized;
const NetworkState({
required this.localIps,
required this.initialized,
});
}
@@ -1,23 +1,34 @@
import 'package:dart_mappable/dart_mappable.dart';
import 'package:dio/dio.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:localsend_app/model/device.dart';
import 'package:localsend_app/model/session_status.dart';
import 'package:localsend_app/model/state/send/sending_file.dart';
part 'send_session_state.freezed.dart';
part 'send_session_state.mapper.dart';
@freezed
class SendSessionState with _$SendSessionState {
const factory SendSessionState({
required String sessionId,
required String? remoteSessionId, // v2
required bool background,
required SessionStatus status,
required Device target,
required Map<String, SendingFile> files, // file id as key
required int? startTime,
required int? endTime,
required CancelToken? cancelToken,
required String? errorMessage,
}) = _SendSessionState;
@MappableClass()
class SendSessionState with SendSessionStateMappable {
final String sessionId;
final String? remoteSessionId; // v2
final bool background;
final SessionStatus status;
final Device target;
final Map<String, SendingFile> files; // file id as key
final int? startTime;
final int? endTime;
final CancelToken? cancelToken;
final String? errorMessage;
const SendSessionState({
required this.sessionId,
required this.remoteSessionId,
required this.background,
required this.status,
required this.target,
required this.files,
required this.startTime,
required this.endTime,
required this.cancelToken,
required this.errorMessage,
});
}
+21 -13
View File
@@ -1,19 +1,27 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:dart_mappable/dart_mappable.dart';
import 'package:localsend_app/model/dto/file_dto.dart';
import 'package:localsend_app/model/file_status.dart';
import 'package:wechat_assets_picker/wechat_assets_picker.dart';
part 'sending_file.freezed.dart';
part 'sending_file.mapper.dart';
@freezed
class SendingFile with _$SendingFile {
const factory SendingFile({
required FileDto file,
required FileStatus status,
required String? token,
required AssetEntity? asset, // for thumbnails
required String? path, // android, iOS, desktop
required List<int>? bytes, // web
required String? errorMessage, // when status == failed
}) = _SendingFile;
@MappableClass()
class SendingFile with SendingFileMappable {
final FileDto file;
final FileStatus status;
final String? token;
final AssetEntity? asset; // for thumbnails
final String? path; // android, iOS, desktop
final List<int>? bytes; // web
final String? errorMessage; // when status == failed
const SendingFile({
required this.file,
required this.status,
required this.token,
required this.asset,
required this.path,
required this.bytes,
required this.errorMessage,
});
}
+15 -10
View File
@@ -1,15 +1,20 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:dart_mappable/dart_mappable.dart';
import 'package:localsend_app/model/dto/file_dto.dart';
import 'package:wechat_assets_picker/wechat_assets_picker.dart';
part 'web_send_file.freezed.dart';
part 'web_send_file.mapper.dart';
@freezed
class WebSendFile with _$WebSendFile {
const factory WebSendFile({
required FileDto file,
required AssetEntity? asset, // for thumbnails
required String? path, // android, iOS, desktop
required List<int>? bytes, // web
}) = _WebSendFile;
@MappableClass()
class WebSendFile with WebSendFileMappable {
final FileDto file;
final AssetEntity? asset; // for thumbnails
final String? path; // android, iOS, desktop
final List<int>? bytes; // web
const WebSendFile({
required this.file,
required this.asset,
required this.path,
required this.bytes,
});
}
@@ -1,15 +1,20 @@
import 'dart:async';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:dart_mappable/dart_mappable.dart';
part 'web_send_session.freezed.dart';
part 'web_send_session.mapper.dart';
@freezed
class WebSendSession with _$WebSendSession {
const factory WebSendSession({
required String sessionId,
required StreamController<bool>? responseHandler, // used to accept or reject incoming requests
required String ip,
required String deviceInfo, // parsed from userAgent
}) = _WebSendSession;
@MappableClass()
class WebSendSession with WebSendSessionMappable {
final String sessionId;
final StreamController<bool>? responseHandler; // used to accept or reject incoming requests
final String ip;
final String deviceInfo; // parsed from userAgent
const WebSendSession({
required this.sessionId,
required this.responseHandler,
required this.ip,
required this.deviceInfo,
});
}
@@ -1,13 +1,16 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:dart_mappable/dart_mappable.dart';
import 'package:localsend_app/model/state/send/web/web_send_file.dart';
import 'package:localsend_app/model/state/send/web/web_send_session.dart';
part 'web_send_state.freezed.dart';
part 'web_send_state.mapper.dart';
@freezed
class WebSendState with _$WebSendState {
const factory WebSendState({
required Map<String, WebSendSession> sessions, // session id -> session data, also includes incoming requests
required Map<String, WebSendFile> files, // file id as key
}) = _WebSendState;
@MappableClass()
class WebSendState with WebSendStateMappable {
final Map<String, WebSendSession> sessions; // session id -> session data, also includes incoming requests
final Map<String, WebSendFile> files; // file id as key
const WebSendState({
required this.sessions,
required this.files,
});
}
@@ -1,31 +1,36 @@
import 'dart:async';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:dart_mappable/dart_mappable.dart';
import 'package:localsend_app/model/device.dart';
import 'package:localsend_app/model/file_type.dart';
import 'package:localsend_app/model/session_status.dart';
import 'package:localsend_app/model/state/server/receiving_file.dart';
part 'receive_session_state.freezed.dart';
part 'receive_session_state.mapper.dart';
@freezed
class ReceiveSessionState with _$ReceiveSessionState {
const ReceiveSessionState._(); // allow custom getters
@MappableClass()
class ReceiveSessionState with ReceiveSessionStateMappable {
final String sessionId;
final SessionStatus status;
final Device sender;
final Map<String, ReceivingFile> files; // file id as key
final int? startTime;
final int? endTime;
final String destinationDirectory;
final bool saveToGallery;
final StreamController<Map<String, String>?>? responseHandler;
const factory ReceiveSessionState({
required String sessionId,
required SessionStatus status,
required Device sender,
required Map<String, ReceivingFile> files,
required int? startTime,
required int? endTime,
required String destinationDirectory,
required bool saveToGallery,
// use this to accept / decline the request, empty map == decline
// FileId -> File Name
required StreamController<Map<String, String>?>? responseHandler,
}) = _ReceiveSessionState;
const ReceiveSessionState({
required this.sessionId,
required this.status,
required this.sender,
required this.files,
required this.startTime,
required this.endTime,
required this.destinationDirectory,
required this.saveToGallery,
required this.responseHandler,
});
/// Returns the message of this request if this is a "message request".
/// Message requests must contain a single text file with preview included.
+21 -13
View File
@@ -1,18 +1,26 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:dart_mappable/dart_mappable.dart';
import 'package:localsend_app/model/dto/file_dto.dart';
import 'package:localsend_app/model/file_status.dart';
part 'receiving_file.freezed.dart';
part 'receiving_file.mapper.dart';
@freezed
class ReceivingFile with _$ReceivingFile {
const factory ReceivingFile({
required FileDto file,
required FileStatus status,
required String? token,
required String? desiredName, // not null when accepted
required String? path, // when finished
required bool savedToGallery, // when finished
required String? errorMessage, // when status == failed
}) = _ReceivingFile;
@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
const ReceivingFile({
required this.file,
required this.status,
required this.token,
required this.desiredName,
required this.path,
required this.savedToGallery,
required this.errorMessage,
});
}
+19 -12
View File
@@ -1,19 +1,26 @@
import 'dart:io';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:dart_mappable/dart_mappable.dart';
import 'package:localsend_app/model/state/send/web/web_send_state.dart';
import 'package:localsend_app/model/state/server/receive_session_state.dart';
part 'server_state.freezed.dart';
part 'server_state.mapper.dart';
@freezed
class ServerState with _$ServerState {
const factory ServerState({
required HttpServer httpServer,
required String alias,
required int port,
required bool https,
required ReceiveSessionState? session,
required WebSendState? webSendState,
}) = _ServerState;
@MappableClass()
class ServerState with ServerStateMappable {
final HttpServer httpServer;
final String alias;
final int port;
final bool https;
final ReceiveSessionState? session;
final WebSendState? webSendState;
const ServerState({
required this.httpServer,
required this.alias,
required this.port,
required this.https,
required this.session,
required this.webSendState,
});
}
+47 -26
View File
@@ -1,34 +1,55 @@
import 'package:dart_mappable/dart_mappable.dart';
import 'package:flutter/material.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:localsend_app/gen/strings.g.dart';
import 'package:localsend_app/model/device.dart';
import 'package:localsend_app/model/persistence/color_mode.dart';
import 'package:localsend_app/model/send_mode.dart';
part 'settings_state.freezed.dart';
part 'settings_state.mapper.dart';
@freezed
class SettingsState with _$SettingsState {
const factory SettingsState({
required String showToken, // the token to show / maximize the window because only one instance is allowed
required String alias,
required ThemeMode theme,
required ColorMode colorMode,
required AppLocale? locale,
required int port,
required String multicastGroup,
required String? destination, // null = default
required bool saveToGallery, // only Android, iOS
required bool saveToHistory,
required bool quickSave, // automatically accept file requests
required bool minimizeToTray, // minimize to tray instead of exiting the app
required bool launchAtStartup, // Tracks if the option is enabled on Linux
required bool autoStartLaunchMinimized, // start hidden in tray (only available when launchAtStartup is true)
required bool https,
required SendMode sendMode,
required bool saveWindowPlacement,
required bool enableAnimations,
required DeviceType? deviceType,
required String? deviceModel,
}) = _SettingsState;
@MappableClass()
class SettingsState with SettingsStateMappable {
final String showToken; // the token to show / maximize the window because only one instance is allowed
final String alias;
final ThemeMode theme;
final ColorMode colorMode;
final AppLocale? locale;
final int port;
final String multicastGroup;
final String? destination; // null = default
final bool saveToGallery; // only Android, iOS
final bool saveToHistory;
final bool quickSave; // automatically accept file requests
final bool minimizeToTray; // minimize to tray instead of exiting the app
final bool launchAtStartup; // Tracks if the option is enabled on Linux
final bool autoStartLaunchMinimized; // start hidden in tray (only available when launchAtStartup is true)
final bool https;
final SendMode sendMode;
final bool saveWindowPlacement;
final bool enableAnimations;
final DeviceType? deviceType;
final String? deviceModel;
const SettingsState({
required this.showToken,
required this.alias,
required this.theme,
required this.colorMode,
required this.locale,
required this.port,
required this.multicastGroup,
required this.destination,
required this.saveToGallery,
required this.saveToHistory,
required this.quickSave,
required this.minimizeToTray,
required this.launchAtStartup,
required this.autoStartLaunchMinimized,
required this.https,
required this.sendMode,
required this.saveWindowPlacement,
required this.enableAnimations,
required this.deviceType,
required this.deviceModel,
});
}
+13 -9
View File
@@ -1,12 +1,16 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:dart_mappable/dart_mappable.dart';
part 'apk_provider_param.freezed.dart';
part 'apk_provider_param.mapper.dart';
@freezed
class ApkProviderParam with _$ApkProviderParam {
const factory ApkProviderParam({
required String query,
required bool includeSystemApps,
required bool onlyAppsWithLaunchIntent,
}) = _ApkProviderParam;
@MappableClass()
class ApkProviderParam with ApkProviderParamMappable {
final String query;
final bool includeSystemApps;
final bool onlyAppsWithLaunchIntent;
const ApkProviderParam({
required this.query,
required this.includeSystemApps,
required this.onlyAppsWithLaunchIntent,
});
}
@@ -1,11 +1,14 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:dart_mappable/dart_mappable.dart';
part 'cached_apk_provider_param.freezed.dart';
part 'cached_apk_provider_param.mapper.dart';
@freezed
class CachedApkProviderParam with _$CachedApkProviderParam {
const factory CachedApkProviderParam({
required bool includeSystemApps,
required bool onlyAppsWithLaunchIntent,
}) = _CachedApkProviderParam;
@MappableClass()
class CachedApkProviderParam with CachedApkProviderParamMappable {
final bool includeSystemApps;
final bool onlyAppsWithLaunchIntent;
const CachedApkProviderParam({
required this.includeSystemApps,
required this.onlyAppsWithLaunchIntent,
});
}
+35 -35
View File
@@ -17,6 +17,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "5.13.0"
ansicolor:
dependency: transitive
description:
name: ansicolor
sha256: "607f8fa9786f392043f169898923e6c59b4518242b68b8862eb8a8b7d9c30b4a"
url: "https://pub.dev"
source: hosted
version: "2.0.1"
archive:
dependency: transitive
description:
@@ -249,6 +257,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "5.0.2"
dart_mappable:
dependency: "direct main"
description:
name: dart_mappable
sha256: "708b01d81663f6dbfa14d6cd588ba7241ed24c9a346a40855148c3b82520c63c"
url: "https://pub.dev"
source: hosted
version: "3.3.0"
dart_mappable_builder:
dependency: "direct dev"
description:
name: dart_mappable_builder
sha256: ce10c4c19cb9071461703e6186bb50ff7ec806c99ef717cab9ed25099d09f8bd
url: "https://pub.dev"
source: hosted
version: "3.3.0"
dart_style:
dependency: transitive
description:
@@ -501,22 +525,6 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
freezed:
dependency: "direct dev"
description:
name: freezed
sha256: "2df89855fe181baae3b6d714dc3c4317acf4fccd495a6f36e5e00f24144c6c3b"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
freezed_annotation:
dependency: "direct main"
description:
name: freezed_annotation
sha256: c3fd9336eb55a38cc1bbd79ab17573113a8deccd0ecbbf926cca3c62803b5c2d
url: "https://pub.dev"
source: hosted
version: "2.4.1"
frontend_server_client:
dependency: transitive
description:
@@ -710,21 +718,13 @@ packages:
source: hosted
version: "3.0.1"
json_annotation:
dependency: "direct main"
dependency: transitive
description:
name: json_annotation
sha256: b10a7b2ff83d83c777edba3c6a0f97045ddadd56c944e1a23a3fdf43a1bf4467
url: "https://pub.dev"
source: hosted
version: "4.8.1"
json_serializable:
dependency: "direct dev"
description:
name: json_serializable
sha256: aa1f5a8912615733e0fdc7a02af03308933c93235bdc8d50d0b0c8a8ccb0b969
url: "https://pub.dev"
source: hosted
version: "6.7.1"
launch_at_startup:
dependency: "direct main"
description:
@@ -1075,10 +1075,10 @@ packages:
dependency: transitive
description:
name: refena
sha256: "50020d82c1b6f085b4af1ce8c81b20f02219e5efd67adc5567d63200290cfde8"
sha256: e7f3cd6440342201274e325e94f4dd4dd68f92395132a0df5a006c58a2b55bdc
url: "https://pub.dev"
source: hosted
version: "0.32.0"
version: "0.32.1"
refena_flutter:
dependency: "direct main"
description:
@@ -1308,14 +1308,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.4.0"
source_helper:
dependency: transitive
description:
name: source_helper
sha256: "6adebc0006c37dd63fe05bca0a929b99f06402fc95aa35bf36d67f5c06de01fd"
url: "https://pub.dev"
source: hosted
version: "1.3.4"
source_map_stack_trace:
dependency: transitive
description:
@@ -1445,6 +1437,14 @@ packages:
url: "https://github.com/Tienisto/tray_manager.git"
source: git
version: "0.2.0"
type_plus:
dependency: transitive
description:
name: type_plus
sha256: "52af1140887d0ce0ea89c768dfde1b244cd531221c7f48c8c29b1d24ae8aed9a"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
typed_data:
dependency: transitive
description:
+2 -4
View File
@@ -13,6 +13,7 @@ dependencies:
basic_utils: 5.6.1
collection: ^1.17.2 # allow newer versions, so it can compile with newer Flutter versions
connectivity_plus: 4.0.2
dart_mappable: 3.3.0
desktop_drop: 0.4.3
device_apps: 2.2.0
device_info_plus: 9.0.3
@@ -25,11 +26,9 @@ dependencies:
flutter_localizations:
sdk: flutter
flutter_markdown: 0.6.17+1
freezed_annotation: 2.4.1
gal: 1.9.1
image_picker: 1.0.4
intl: ^0.18.0 # allow newer versions, so it can compile with newer Flutter versions
json_annotation: 4.8.1
launch_at_startup: 0.2.2
logging: 1.2.0
mime: 1.0.4
@@ -68,10 +67,9 @@ dependencies:
dev_dependencies:
build_runner: 2.4.6
dart_mappable_builder: 3.3.0
flutter_gen_runner: 5.3.1
flutter_lints: 2.0.2
freezed: 2.4.1
json_serializable: 6.7.1
msix: 3.16.1
refena_inspector: 0.3.0
slang_build_runner: 3.23.0
@@ -1,14 +1,16 @@
import 'dart:convert';
import 'package:dart_mappable/dart_mappable.dart';
import 'package:localsend_app/model/device.dart';
import 'package:localsend_app/model/dto/file_dto.dart';
import 'package:localsend_app/model/dto/info_register_dto.dart';
import 'package:localsend_app/model/dto/multicast_dto.dart';
import 'package:localsend_app/model/dto/prepare_upload_request_dto.dart';
import 'package:localsend_app/model/dto/prepare_upload_response_dto.dart';
import 'package:localsend_app/model/file_type.dart';
import 'package:test/test.dart';
void main() {
MapperContainer.globals.use(const FileDtoMapper());
group('parse PrepareUploadRequestDto', () {
test('should parse valid enums', () {
final dto = {
@@ -33,6 +35,17 @@ void main() {
expect(parsed.files.values.first.fileType, FileType.image);
});
test('Should fallback deviceType (simple)', () {
final dto = {
'alias': 'Nice Banana',
'deviceModel': 'Samsung',
'deviceType': 'invalidType',
};
final parsed = InfoRegisterDto.fromJson(dto);
expect(parsed.deviceType, DeviceType.desktop);
});
test('should fallback deviceType', () {
final dto = {
'info': {
@@ -161,7 +174,7 @@ void main() {
),
},
);
final serialized = _deepSerialize(dto);
final serialized = dto.toJson();
expect(serialized['info']['deviceType'], 'mobile');
expect(serialized['files'].length, 2);
expect(serialized['files']['some id']['fileType'], 'image');
@@ -192,7 +205,7 @@ void main() {
),
},
);
final serialized = _deepSerialize(dto);
final serialized = dto.toJson();
expect(serialized['info']['deviceType'], 'mobile');
expect(serialized['files'].length, 2);
@@ -200,10 +213,19 @@ void main() {
expect(serialized['files']['some id 2']['fileType'], 'application/vnd.android.package-archive');
});
});
}
/// Deep serialize an object to a map.
/// The toJson method only serializes the first level.
Map<String, dynamic> _deepSerialize(Object object) {
return jsonDecode(jsonEncode(object));
test('PrepareUploadResponseDto', () {
final parsed = PrepareUploadResponseDto.fromJson({
'sessionId': 'some session id',
'files': {
'some id': 'some url',
'some id 2': 'some url 2',
},
});
expect(parsed.sessionId, 'some session id');
expect(parsed.files.length, 2);
expect(parsed.files['some id'], 'some url');
expect(parsed.files['some id 2'], 'some url 2');
});
}