chore(mobile): migrate remaining simple classes to Freezed (#30907)

This commit is contained in:
Adam Gastineau 2026-08-21 10:07:03 -07:00 committed by GitHub
parent 2f48a8aab4
commit 37e033a09d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 259 additions and 764 deletions

View file

@ -1,48 +1,15 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:immich_mobile/constants/enums.dart';
class CleanupConfig {
final bool keepFavorites;
final AssetKeepType keepMediaType;
final List<String> keepAlbumIds;
final int cutoffDaysAgo;
final bool defaultsInitialized;
part 'cleanup_config.freezed.dart';
const CleanupConfig({
this.keepFavorites = true,
this.keepMediaType = AssetKeepType.none,
this.keepAlbumIds = const [],
this.cutoffDaysAgo = -1,
this.defaultsInitialized = false,
});
CleanupConfig copyWith({
bool? keepFavorites,
AssetKeepType? keepMediaType,
List<String>? keepAlbumIds,
int? cutoffDaysAgo,
bool? defaultsInitialized,
}) => .new(
keepFavorites: keepFavorites ?? this.keepFavorites,
keepMediaType: keepMediaType ?? this.keepMediaType,
keepAlbumIds: keepAlbumIds ?? this.keepAlbumIds,
cutoffDaysAgo: cutoffDaysAgo ?? this.cutoffDaysAgo,
defaultsInitialized: defaultsInitialized ?? this.defaultsInitialized,
);
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is CleanupConfig &&
other.keepFavorites == keepFavorites &&
other.keepMediaType == keepMediaType &&
other.keepAlbumIds == keepAlbumIds &&
other.cutoffDaysAgo == cutoffDaysAgo &&
other.defaultsInitialized == defaultsInitialized);
@override
int get hashCode => Object.hash(keepFavorites, keepMediaType, keepAlbumIds, cutoffDaysAgo, defaultsInitialized);
@override
String toString() =>
'CleanupConfig(keepFavorites: $keepFavorites, keepMediaType: $keepMediaType, keepAlbumIds: $keepAlbumIds, cutoffDaysAgo: $cutoffDaysAgo, defaultsInitialized: $defaultsInitialized)';
@freezed
abstract class CleanupConfig with _$CleanupConfig {
const factory CleanupConfig({
@Default(true) bool keepFavorites,
@Default(AssetKeepType.none) AssetKeepType keepMediaType,
@Default([]) List<String> keepAlbumIds,
@Default(-1) int cutoffDaysAgo,
@Default(false) bool defaultsInitialized,
}) = _CleanupConfig;
}

View file

@ -1,25 +1,24 @@
import 'package:flutter/material.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:immich_mobile/utils/option.dart';
class MapConfig {
final int relativeDays;
final bool favoritesOnly;
final bool includeArchived;
final ThemeMode themeMode;
final bool withPartners;
final DateTime? customFrom;
final DateTime? customTo;
part 'map_config.freezed.dart';
const MapConfig({
this.relativeDays = 0,
this.favoritesOnly = false,
this.includeArchived = false,
this.themeMode = .system,
this.withPartners = false,
this.customFrom,
this.customTo,
});
@Freezed(copyWith: false)
abstract class MapConfig with _$MapConfig {
const MapConfig._();
const factory MapConfig({
@Default(0) int relativeDays,
@Default(false) bool favoritesOnly,
@Default(false) bool includeArchived,
@Default(ThemeMode.system) ThemeMode themeMode,
@Default(false) bool withPartners,
DateTime? customFrom,
DateTime? customTo,
}) = _MapConfig;
// We patch `customFrom` and `customTo`, which prevents us from using Freezed `copyWith`
MapConfig copyWith({
int? relativeDays,
bool? favoritesOnly,
@ -37,24 +36,4 @@ class MapConfig {
customFrom: customFrom.patch(this.customFrom),
customTo: customTo.patch(this.customTo),
);
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is MapConfig &&
other.relativeDays == relativeDays &&
other.favoritesOnly == favoritesOnly &&
other.includeArchived == includeArchived &&
other.themeMode == themeMode &&
other.withPartners == withPartners &&
other.customFrom == customFrom &&
other.customTo == customTo);
@override
int get hashCode =>
Object.hash(relativeDays, favoritesOnly, includeArchived, themeMode, withPartners, customFrom, customTo);
@override
String toString() =>
'MapConfig(relativeDays: $relativeDays, favoritesOnly: $favoritesOnly, includeArchived: $includeArchived, themeMode: $themeMode, withPartners: $withPartners, customFrom: $customFrom, customTo: $customTo)';
}

View file

@ -1,21 +1,21 @@
import 'package:flutter/foundation.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:immich_mobile/utils/option.dart';
class NetworkConfig {
final bool autoEndpointSwitching;
final String? preferredWifiName;
final String? localEndpoint;
final List<String> externalEndpointList;
final Map<String, String> customHeaders;
part 'network_config.freezed.dart';
const NetworkConfig({
this.autoEndpointSwitching = false,
this.preferredWifiName,
this.localEndpoint,
this.externalEndpointList = const [],
this.customHeaders = const {},
});
@Freezed(copyWith: false)
abstract class NetworkConfig with _$NetworkConfig {
const NetworkConfig._();
const factory NetworkConfig({
@Default(false) bool autoEndpointSwitching,
String? preferredWifiName,
String? localEndpoint,
@Default([]) List<String> externalEndpointList,
@Default({}) Map<String, String> customHeaders,
}) = _NetworkConfig;
// We patch `preferredWifiName` and `localEndpoint`, which prevents us from using Freezed `copyWith`
NetworkConfig copyWith({
bool? autoEndpointSwitching,
Option<String>? preferredWifiName,
@ -29,27 +29,4 @@ class NetworkConfig {
externalEndpointList: externalEndpointList ?? this.externalEndpointList,
customHeaders: customHeaders ?? this.customHeaders,
);
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is NetworkConfig &&
other.autoEndpointSwitching == autoEndpointSwitching &&
other.preferredWifiName == preferredWifiName &&
other.localEndpoint == localEndpoint &&
listEquals(other.externalEndpointList, externalEndpointList) &&
mapEquals(other.customHeaders, customHeaders));
@override
int get hashCode => Object.hash(
autoEndpointSwitching,
preferredWifiName,
localEndpoint,
Object.hashAll(externalEndpointList),
Object.hashAllUnordered(customHeaders.entries.map((e) => Object.hash(e.key, e.value))),
);
@override
String toString() =>
'NetworkConfig(autoEndpointSwitching: $autoEndpointSwitching, preferredWifiName: $preferredWifiName, localEndpoint: $localEndpoint, externalEndpointList: $externalEndpointList, customHeaders: $customHeaders)';
}

View file

@ -15,22 +15,8 @@ abstract class Stack with _$Stack {
}) = _Stack;
}
class StackResponse {
final String id;
final String primaryAssetId;
final List<String> assetIds;
const StackResponse({required this.id, required this.primaryAssetId, required this.assetIds});
@override
bool operator ==(covariant StackResponse other) {
if (identical(this, other)) {
return true;
}
return other.id == id && other.primaryAssetId == primaryAssetId && other.assetIds == assetIds;
}
@override
int get hashCode => id.hashCode ^ primaryAssetId.hashCode ^ assetIds.hashCode;
@freezed
abstract class StackResponse with _$StackResponse {
const factory StackResponse({required String id, required String primaryAssetId, required List<String> assetIds}) =
_StackResponse;
}

View file

@ -1,5 +1,8 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:immich_mobile/domain/models/user.model.dart';
part 'store.model.freezed.dart';
/// Key for each possible value in the `Store`.
/// Defines the data type for each value
enum StoreKey<T> {
@ -63,30 +66,7 @@ enum StoreKey<T> {
Type get type => T;
}
class StoreDto<T> {
final StoreKey<T> key;
final T? value;
const StoreDto(this.key, this.value);
@override
String toString() {
return '''
StoreDto: {
key: $key,
value: ${value ?? '<NA>'},
}''';
}
@override
bool operator ==(covariant StoreDto<T> other) {
if (identical(this, other)) {
return true;
}
return other.key == key && other.value == value;
}
@override
int get hashCode => key.hashCode ^ value.hashCode;
@freezed
abstract class StoreDto<T> with _$StoreDto<T> {
const factory StoreDto(StoreKey<T> key, T? value) = _StoreDto<T>;
}

View file

@ -1,14 +1,17 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:immich_mobile/utils/option.dart';
class TimeRange {
final DateTime? from;
final DateTime? to;
part 'time_range.model.freezed.dart';
const TimeRange({this.from, this.to});
@Freezed(copyWith: false)
abstract class TimeRange with _$TimeRange {
const TimeRange._();
TimeRange copyWith({Option<DateTime>? from, Option<DateTime>? to}) {
return TimeRange(from: from.patch(this.from), to: to.patch(this.to));
}
const factory TimeRange({DateTime? from, DateTime? to}) = _TimeRange;
// Patching is custom, which prevents using Freezed `copyWith`
TimeRange copyWith({Option<DateTime>? from, Option<DateTime>? to}) =>
TimeRange(from: from.patch(this.from), to: to.patch(this.to));
TimeRange clearFrom() => TimeRange(to: to);
TimeRange clearTo() => TimeRange(from: from);

View file

@ -35,96 +35,31 @@ enum AvatarColor {
}
// TODO: Rename to User once Isar is removed
class UserDto {
final String id;
final String email;
final String name;
final bool isAdmin;
final DateTime? updatedAt;
@Freezed(equal: false)
abstract class UserDto with _$UserDto {
const UserDto._();
final AvatarColor avatarColor;
final bool memoryEnabled;
final bool inTimeline;
final bool isPartnerSharedBy;
final bool isPartnerSharedWith;
final int quotaUsageInBytes;
final int quotaSizeInBytes;
const factory UserDto({
required String id,
required String email,
required String name,
@Default(false) bool isAdmin,
DateTime? updatedAt,
required DateTime profileChangedAt,
@Default(AvatarColor.primary) AvatarColor avatarColor,
@Default(true) bool memoryEnabled,
@Default(false) bool inTimeline,
@Default(false) bool isPartnerSharedBy,
@Default(false) bool isPartnerSharedWith,
@Default(false) bool hasProfileImage,
@Default(0) int quotaUsageInBytes,
@Default(0) int quotaSizeInBytes,
}) = _UserDto;
bool get hasQuota => quotaSizeInBytes > 0;
final bool hasProfileImage;
final DateTime profileChangedAt;
const UserDto({
required this.id,
required this.email,
required this.name,
this.isAdmin = false,
this.updatedAt,
required this.profileChangedAt,
this.avatarColor = AvatarColor.primary,
this.memoryEnabled = true,
this.inTimeline = false,
this.isPartnerSharedBy = false,
this.isPartnerSharedWith = false,
this.hasProfileImage = false,
this.quotaUsageInBytes = 0,
this.quotaSizeInBytes = 0,
});
@override
String toString() {
return '''User: {
id: $id,
email: $email,
name: $name,
isAdmin: $isAdmin,
updatedAt: $updatedAt,
avatarColor: $avatarColor,
memoryEnabled: $memoryEnabled,
inTimeline: $inTimeline,
isPartnerSharedBy: $isPartnerSharedBy,
isPartnerSharedWith: $isPartnerSharedWith,
hasProfileImage: $hasProfileImage
profileChangedAt: $profileChangedAt
}''';
}
UserDto copyWith({
String? id,
String? email,
String? name,
bool? isAdmin,
DateTime? updatedAt,
AvatarColor? avatarColor,
bool? memoryEnabled,
bool? inTimeline,
bool? isPartnerSharedBy,
bool? isPartnerSharedWith,
bool? hasProfileImage,
DateTime? profileChangedAt,
int? quotaSizeInBytes,
int? quotaUsageInBytes,
}) => UserDto(
id: id ?? this.id,
email: email ?? this.email,
name: name ?? this.name,
isAdmin: isAdmin ?? this.isAdmin,
updatedAt: updatedAt ?? this.updatedAt,
avatarColor: avatarColor ?? this.avatarColor,
memoryEnabled: memoryEnabled ?? this.memoryEnabled,
inTimeline: inTimeline ?? this.inTimeline,
isPartnerSharedBy: isPartnerSharedBy ?? this.isPartnerSharedBy,
isPartnerSharedWith: isPartnerSharedWith ?? this.isPartnerSharedWith,
hasProfileImage: hasProfileImage ?? this.hasProfileImage,
profileChangedAt: profileChangedAt ?? this.profileChangedAt,
quotaSizeInBytes: quotaSizeInBytes ?? this.quotaSizeInBytes,
quotaUsageInBytes: quotaUsageInBytes ?? this.quotaUsageInBytes,
);
// We use [DateTime.isAtSameMomentAs] for comparing across timezones. As Freezed doesn't support custom equality, we need to have our own `==` for now
// TODO(agg23): Switch to newtypes to fix equality
@override
bool operator ==(covariant UserDto other) {
if (identical(this, other)) {

View file

@ -1,17 +1,16 @@
import 'dart:convert';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'livephotos_medatada.model.freezed.dart';
enum LivePhotosPart { video, image }
class LivePhotosMetadata {
// enum
LivePhotosPart part;
@Freezed(fromJson: false, toJson: false)
abstract class LivePhotosMetadata with _$LivePhotosMetadata {
const LivePhotosMetadata._();
String id;
LivePhotosMetadata({required this.part, required this.id});
LivePhotosMetadata copyWith({LivePhotosPart? part, String? id}) {
return LivePhotosMetadata(part: part ?? this.part, id: id ?? this.id);
}
const factory LivePhotosMetadata({required LivePhotosPart part, required String id}) = _LivePhotosMetadata;
Map<String, dynamic> toMap() {
return <String, dynamic>{'part': part.index, 'id': id};
@ -25,19 +24,4 @@ class LivePhotosMetadata {
factory LivePhotosMetadata.fromJson(String source) =>
LivePhotosMetadata.fromMap(json.decode(source) as Map<String, dynamic>);
@override
String toString() => 'LivePhotosMetadata(part: $part, id: $id)';
@override
bool operator ==(covariant LivePhotosMetadata other) {
if (identical(this, other)) {
return true;
}
return other.part == part && other.id == id;
}
@override
int get hashCode => part.hashCode ^ id.hashCode;
}

View file

@ -7,15 +7,11 @@ import 'package:immich_mobile/utils/option.dart';
part 'search_filter.model.freezed.dart';
class SearchLocationFilter {
String? country;
String? state;
String? city;
SearchLocationFilter({this.country, this.state, this.city});
@Freezed(fromJson: false, toJson: false)
abstract class SearchLocationFilter with _$SearchLocationFilter {
const SearchLocationFilter._();
SearchLocationFilter copyWith({String? country, String? state, String? city}) {
return SearchLocationFilter(country: country ?? this.country, state: state ?? this.state, city: city ?? this.city);
}
const factory SearchLocationFilter({String? country, String? state, String? city}) = _SearchLocationFilter;
Map<String, dynamic> toMap() {
return <String, dynamic>{'country': country, 'state': state, 'city': city};
@ -33,31 +29,13 @@ class SearchLocationFilter {
factory SearchLocationFilter.fromJson(String source) =>
SearchLocationFilter.fromMap(json.decode(source) as Map<String, dynamic>);
@override
String toString() => 'SearchLocationFilter(country: $country, state: $state, city: $city)';
@override
bool operator ==(covariant SearchLocationFilter other) {
if (identical(this, other)) {
return true;
}
return other.country == country && other.state == state && other.city == city;
}
@override
int get hashCode => country.hashCode ^ state.hashCode ^ city.hashCode;
}
class SearchCameraFilter {
String? make;
String? model;
SearchCameraFilter({this.make, this.model});
@Freezed(fromJson: false, toJson: false)
abstract class SearchCameraFilter with _$SearchCameraFilter {
const SearchCameraFilter._();
SearchCameraFilter copyWith({String? make, String? model}) {
return SearchCameraFilter(make: make ?? this.make, model: model ?? this.model);
}
const factory SearchCameraFilter({String? make, String? model}) = _SearchCameraFilter;
Map<String, dynamic> toMap() {
return <String, dynamic>{'make': make, 'model': model};
@ -74,31 +52,13 @@ class SearchCameraFilter {
factory SearchCameraFilter.fromJson(String source) =>
SearchCameraFilter.fromMap(json.decode(source) as Map<String, dynamic>);
@override
String toString() => 'SearchCameraFilter(make: $make, model: $model)';
@override
bool operator ==(covariant SearchCameraFilter other) {
if (identical(this, other)) {
return true;
}
return other.make == make && other.model == model;
}
@override
int get hashCode => make.hashCode ^ model.hashCode;
}
class SearchDateFilter {
DateTime? takenBefore;
DateTime? takenAfter;
SearchDateFilter({this.takenBefore, this.takenAfter});
@Freezed(fromJson: false, toJson: false)
abstract class SearchDateFilter with _$SearchDateFilter {
const SearchDateFilter._();
SearchDateFilter copyWith({DateTime? takenBefore, DateTime? takenAfter}) {
return SearchDateFilter(takenBefore: takenBefore ?? this.takenBefore, takenAfter: takenAfter ?? this.takenAfter);
}
const factory SearchDateFilter({DateTime? takenBefore, DateTime? takenAfter}) = _SearchDateFilter;
Map<String, dynamic> toMap() {
return <String, dynamic>{
@ -118,31 +78,15 @@ class SearchDateFilter {
factory SearchDateFilter.fromJson(String source) =>
SearchDateFilter.fromMap(json.decode(source) as Map<String, dynamic>);
@override
String toString() => 'SearchDateFilter(takenBefore: $takenBefore, takenAfter: $takenAfter)';
@override
bool operator ==(covariant SearchDateFilter other) {
if (identical(this, other)) {
return true;
}
return other.takenBefore == takenBefore && other.takenAfter == takenAfter;
}
@override
int get hashCode => takenBefore.hashCode ^ takenAfter.hashCode;
}
class SearchRatingFilter {
/// none = no filter; some(null) = filter for unrated; some(1-5) = filter for that rating
Option<int?> rating;
SearchRatingFilter({this.rating = const Option.none()});
@Freezed(fromJson: false, toJson: false)
abstract class SearchRatingFilter with _$SearchRatingFilter {
const SearchRatingFilter._();
SearchRatingFilter copyWith({Option<int?>? rating}) {
return SearchRatingFilter(rating: rating ?? this.rating);
}
/// [rating]: none = no filter; some(null) = filter for unrated; some(1-5) = filter for that rating
// TODO(agg23): Switch to enum
const factory SearchRatingFilter({@Default(Option.none()) Option<int?> rating}) = _SearchRatingFilter;
Map<String, dynamic> toMap() {
if (rating.isNone) {
@ -153,7 +97,7 @@ class SearchRatingFilter {
factory SearchRatingFilter.fromMap(Map<String, dynamic> map) {
if (!(map['active'] as bool? ?? false)) {
return SearchRatingFilter();
return const SearchRatingFilter();
}
return SearchRatingFilter(rating: Option.some(map['value'] as int?));
}
@ -162,21 +106,6 @@ class SearchRatingFilter {
factory SearchRatingFilter.fromJson(String source) =>
SearchRatingFilter.fromMap(json.decode(source) as Map<String, dynamic>);
@override
String toString() => 'SearchRatingFilter(rating: $rating)';
@override
bool operator ==(covariant SearchRatingFilter other) {
if (identical(this, other)) {
return true;
}
return other.rating == rating;
}
@override
int get hashCode => rating.hashCode;
}
@freezed
@ -185,40 +114,26 @@ abstract class SearchDisplayFilters with _$SearchDisplayFilters {
_SearchDisplayFilters;
}
class SearchFilter {
String? context;
String? filename;
String? description;
String? ocr;
String? language;
String? assetId;
List<String>? tagIds;
Set<Person> people;
SearchLocationFilter location;
SearchCameraFilter camera;
SearchDateFilter date;
SearchRatingFilter rating;
SearchDisplayFilters display;
@freezed
abstract class SearchFilter with _$SearchFilter {
const SearchFilter._();
// Enum
AssetType mediaType;
SearchFilter({
this.context,
this.filename,
this.description,
this.ocr,
this.language,
this.assetId,
this.tagIds,
required this.people,
required this.location,
required this.camera,
required this.date,
required this.display,
required this.rating,
required this.mediaType,
});
const factory SearchFilter({
String? context,
String? filename,
String? description,
String? ocr,
String? language,
String? assetId,
List<String>? tagIds,
required Set<Person> people,
required SearchLocationFilter location,
required SearchCameraFilter camera,
required SearchDateFilter date,
required SearchRatingFilter rating,
required SearchDisplayFilters display,
required AssetType mediaType,
}) = _SearchFilter;
bool get isEmpty {
return (context == null || (context != null && context!.isEmpty)) &&
@ -241,83 +156,4 @@ class SearchFilter {
rating.rating.isNone &&
mediaType == AssetType.other;
}
SearchFilter copyWith({
String? context,
String? filename,
String? description,
String? language,
String? ocr,
String? assetId,
Set<Person>? people,
List<String>? tagIds,
SearchLocationFilter? location,
SearchCameraFilter? camera,
SearchDateFilter? date,
SearchDisplayFilters? display,
SearchRatingFilter? rating,
AssetType? mediaType,
}) {
return SearchFilter(
context: context ?? this.context,
filename: filename ?? this.filename,
description: description ?? this.description,
language: language ?? this.language,
ocr: ocr ?? this.ocr,
assetId: assetId ?? this.assetId,
people: people ?? this.people,
location: location ?? this.location,
camera: camera ?? this.camera,
date: date ?? this.date,
display: display ?? this.display,
rating: rating ?? this.rating,
mediaType: mediaType ?? this.mediaType,
tagIds: tagIds ?? this.tagIds,
);
}
@override
String toString() {
return 'SearchFilter(context: $context, filename: $filename, description: $description, language: $language, ocr: $ocr, people: $people, location: $location, tagIds: $tagIds, camera: $camera, date: $date, display: $display, rating: $rating, mediaType: $mediaType, assetId: $assetId)';
}
@override
bool operator ==(covariant SearchFilter other) {
if (identical(this, other)) {
return true;
}
return other.context == context &&
other.filename == filename &&
other.description == description &&
other.language == language &&
other.ocr == ocr &&
other.assetId == assetId &&
other.people == people &&
other.tagIds == tagIds &&
other.location == location &&
other.camera == camera &&
other.date == date &&
other.display == display &&
other.rating == rating &&
other.mediaType == mediaType;
}
@override
int get hashCode {
return context.hashCode ^
filename.hashCode ^
description.hashCode ^
language.hashCode ^
ocr.hashCode ^
assetId.hashCode ^
people.hashCode ^
tagIds.hashCode ^
location.hashCode ^
camera.hashCode ^
date.hashCode ^
display.hashCode ^
rating.hashCode ^
mediaType.hashCode;
}
}

View file

@ -1,52 +1,23 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:openapi/api.dart';
class ServerConfig {
final int trashDays;
final String oauthButtonText;
final String externalDomain;
final String mapDarkStyleUrl;
final String mapLightStyleUrl;
part 'server_config.model.freezed.dart';
const ServerConfig({
required this.trashDays,
required this.oauthButtonText,
required this.externalDomain,
required this.mapDarkStyleUrl,
required this.mapLightStyleUrl,
});
@freezed
abstract class ServerConfig with _$ServerConfig {
const factory ServerConfig({
required int trashDays,
required String oauthButtonText,
required String externalDomain,
required String mapDarkStyleUrl,
required String mapLightStyleUrl,
}) = _ServerConfig;
ServerConfig copyWith({int? trashDays, String? oauthButtonText, String? externalDomain}) {
return ServerConfig(
trashDays: trashDays ?? this.trashDays,
oauthButtonText: oauthButtonText ?? this.oauthButtonText,
externalDomain: externalDomain ?? this.externalDomain,
mapDarkStyleUrl: mapDarkStyleUrl,
mapLightStyleUrl: mapLightStyleUrl,
);
}
@override
String toString() =>
'ServerConfig(trashDays: $trashDays, oauthButtonText: $oauthButtonText, externalDomain: $externalDomain)';
ServerConfig.fromDto(ServerConfigDto dto)
: trashDays = dto.trashDays,
oauthButtonText = dto.oauthButtonText,
externalDomain = dto.externalDomain,
mapDarkStyleUrl = dto.mapDarkStyleUrl,
mapLightStyleUrl = dto.mapLightStyleUrl;
@override
bool operator ==(covariant ServerConfig other) {
if (identical(this, other)) {
return true;
}
return other.trashDays == trashDays &&
other.oauthButtonText == oauthButtonText &&
other.externalDomain == externalDomain;
}
@override
int get hashCode => trashDays.hashCode ^ oauthButtonText.hashCode ^ externalDomain.hashCode;
factory ServerConfig.fromDto(ServerConfigDto dto) => ServerConfig(
trashDays: dto.trashDays,
oauthButtonText: dto.oauthButtonText,
externalDomain: dto.externalDomain,
mapDarkStyleUrl: dto.mapDarkStyleUrl,
mapLightStyleUrl: dto.mapLightStyleUrl,
);
}

View file

@ -1,33 +1,27 @@
import 'dart:convert';
import 'dart:io';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:immich_mobile/utils/bytes_units.dart';
import 'package:path/path.dart';
part 'share_intent_attachment.model.freezed.dart';
enum ShareIntentAttachmentType { image, video }
enum UploadStatus { enqueued, running, complete, failed }
class ShareIntentAttachment {
final String path;
@Freezed(fromJson: false, toJson: false, equal: false)
abstract class ShareIntentAttachment with _$ShareIntentAttachment {
const ShareIntentAttachment._();
// enum
final ShareIntentAttachmentType type;
// enum
final UploadStatus status;
final double uploadProgress;
final int fileLength;
ShareIntentAttachment({
required this.path,
required this.type,
required this.status,
this.uploadProgress = 0,
this.fileLength = 0,
});
const factory ShareIntentAttachment({
required String path,
required ShareIntentAttachmentType type,
required UploadStatus status,
@Default(0.0) double uploadProgress,
@Default(0) int fileLength,
}) = _ShareIntentAttachment;
int get id => hash(path);
@ -39,23 +33,7 @@ class ShareIntentAttachment {
bool get isVideo => type == ShareIntentAttachmentType.video;
String? _fileSize;
String get fileSize => _fileSize ??= formatHumanReadableBytes(fileLength, 2);
ShareIntentAttachment copyWith({
String? path,
ShareIntentAttachmentType? type,
UploadStatus? status,
double? uploadProgress,
}) {
return ShareIntentAttachment(
path: path ?? this.path,
type: type ?? this.type,
status: status ?? this.status,
uploadProgress: uploadProgress ?? this.uploadProgress,
);
}
String get fileSize => formatHumanReadableBytes(fileLength, 2);
Map<String, dynamic> toMap() {
return <String, dynamic>{
@ -80,18 +58,14 @@ class ShareIntentAttachment {
factory ShareIntentAttachment.fromJson(String source) =>
ShareIntentAttachment.fromMap(json.decode(source) as Map<String, dynamic>);
// Identity is sourced from the backing file, not from upload progress
@override
String toString() {
return 'ShareIntentAttachment(path: $path, type: $type, status: $status, uploadProgress: $uploadProgress)';
}
@override
bool operator ==(covariant ShareIntentAttachment other) {
bool operator ==(Object other) {
if (identical(this, other)) {
return true;
}
return other.path == path && other.type == type;
return other is ShareIntentAttachment && other.path == path && other.type == type;
}
@override

View file

@ -28,11 +28,11 @@ class SimilarPhotosAction extends ActionBuilder {
.new(
assetId: assetId,
people: {},
location: .new(),
camera: .new(),
date: .new(),
location: const .new(),
camera: const .new(),
date: const .new(),
display: const .new(isNotInAlbum: false, isArchive: false, isFavorite: false),
rating: .new(),
rating: const .new(),
mediaType: .other,
),
);

View file

@ -53,11 +53,11 @@ class DriftSearchPage extends HookConsumerWidget {
final filter = useState<SearchFilter>(
SearchFilter(
people: {},
location: SearchLocationFilter(),
camera: SearchCameraFilter(),
date: SearchDateFilter(),
location: const SearchLocationFilter(),
camera: const SearchCameraFilter(),
date: const SearchDateFilter(),
display: const SearchDisplayFilters(isNotInAlbum: false, isArchive: false, isFavorite: false),
rating: SearchRatingFilter(),
rating: const SearchRatingFilter(),
mediaType: AssetType.other,
language: "${context.locale.languageCode}-${context.locale.countryCode}",
tagIds: [],
@ -208,7 +208,7 @@ class DriftSearchPage extends HookConsumerWidget {
void handleClear() {
locationCurrentFilterWidget.value = null;
search(filter.value.copyWith(location: SearchLocationFilter()));
search(filter.value.copyWith(location: const SearchLocationFilter()));
}
void handleApply() {
@ -256,7 +256,7 @@ class DriftSearchPage extends HookConsumerWidget {
void handleClear() {
cameraCurrentFilterWidget.value = null;
search(filter.value.copyWith(camera: SearchCameraFilter()));
search(filter.value.copyWith(camera: const SearchCameraFilter()));
}
void handleApply() {
@ -290,7 +290,7 @@ class DriftSearchPage extends HookConsumerWidget {
dateInputFilter.value = selectedDate;
if (selectedDate == null) {
dateRangeCurrentFilterWidget.value = null;
search(filter.value.copyWith(date: SearchDateFilter()));
search(filter.value.copyWith(date: const SearchDateFilter()));
return;
}
@ -419,7 +419,7 @@ class DriftSearchPage extends HookConsumerWidget {
void handleClear() {
ratingCurrentFilterWidget.value = null;
search(filter.value.copyWith(rating: SearchRatingFilter()));
search(filter.value.copyWith(rating: const SearchRatingFilter()));
}
void handleApply() {

View file

@ -47,8 +47,8 @@ class _AlbumSelectorState extends ConsumerState<AlbumSelector> {
List<RemoteAlbum> sortedAlbums = [];
List<RemoteAlbum> shownAlbums = [];
AlbumFilter filter = AlbumFilter(query: "", mode: QuickFilterMode.all);
AlbumSort sort = AlbumSort(mode: AlbumSortMode.lastModified, isReverse: true);
AlbumFilter filter = const AlbumFilter(query: "", mode: QuickFilterMode.all);
AlbumSort sort = const AlbumSort(mode: AlbumSortMode.lastModified, isReverse: true);
@override
void initState() {

View file

@ -1,6 +1,7 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/events.model.dart';
import 'package:immich_mobile/domain/models/time_range.model.dart';
@ -12,25 +13,23 @@ import 'package:immich_mobile/providers/map/map_state.provider.dart';
import 'package:immich_mobile/utils/option.dart';
import 'package:maplibre_gl/maplibre_gl.dart';
class MapState {
final ThemeMode themeMode;
final LatLngBounds bounds;
final bool onlyFavorites;
final bool includeArchived;
final bool withPartners;
final int relativeDays;
final TimeRange timeRange;
part 'map.state.freezed.dart';
const MapState({
this.themeMode = ThemeMode.system,
required this.bounds,
this.onlyFavorites = false,
this.includeArchived = false,
this.withPartners = false,
this.relativeDays = 0,
this.timeRange = const TimeRange(),
});
@Freezed(equal: false)
abstract class MapState with _$MapState {
const MapState._();
const factory MapState({
@Default(ThemeMode.system) ThemeMode themeMode,
required LatLngBounds bounds,
@Default(false) bool onlyFavorites,
@Default(false) bool includeArchived,
@Default(false) bool withPartners,
@Default(0) int relativeDays,
@Default(TimeRange()) TimeRange timeRange,
}) = _MapState;
// We only care about bounds changes, overriding Freezed
@override
bool operator ==(covariant MapState other) {
return bounds == other.bounds;
@ -39,26 +38,6 @@ class MapState {
@override
int get hashCode => bounds.hashCode;
MapState copyWith({
LatLngBounds? bounds,
ThemeMode? themeMode,
bool? onlyFavorites,
bool? includeArchived,
bool? withPartners,
int? relativeDays,
TimeRange? timeRange,
}) {
return MapState(
bounds: bounds ?? this.bounds,
themeMode: themeMode ?? this.themeMode,
onlyFavorites: onlyFavorites ?? this.onlyFavorites,
includeArchived: includeArchived ?? this.includeArchived,
withPartners: withPartners ?? this.withPartners,
relativeDays: relativeDays ?? this.relativeDays,
timeRange: timeRange ?? this.timeRange,
);
}
TimelineMapOptions toOptions() => TimelineMapOptions(
bounds: bounds,
onlyFavorites: onlyFavorites,

View file

@ -2,6 +2,7 @@ import 'dart:async';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/constants.dart';
import 'package:immich_mobile/domain/models/timeline.model.dart';
@ -14,6 +15,8 @@ import 'package:immich_mobile/providers/haptic_feedback.provider.dart';
import 'package:immich_mobile/utils/debounce.dart';
import 'package:intl/intl.dart' hide TextDirection;
part 'scrubber.widget.freezed.dart';
/// A widget that will display a BoxScrollView with a ScrollThumb that can be dragged
/// for quick navigation of the BoxScrollView.
class Scrubber extends ConsumerStatefulWidget {
@ -596,25 +599,12 @@ class _SlideFadeTransition extends StatelessWidget {
}
}
class _Segment {
final DateTime date;
final double startOffset;
final String scrollLabel;
final bool showSegment;
const _Segment({required this.date, required this.startOffset, required this.scrollLabel, this.showSegment = false});
_Segment copyWith({DateTime? date, double? startOffset, String? scrollLabel, bool? showSegment}) {
return _Segment(
date: date ?? this.date,
startOffset: startOffset ?? this.startOffset,
scrollLabel: scrollLabel ?? this.scrollLabel,
showSegment: showSegment ?? this.showSegment,
);
}
@override
String toString() {
return 'Segment(scrollLabel: $scrollLabel, date: $date)';
}
@freezed
abstract class _Segment with _$Segment {
const factory _Segment({
required DateTime date,
required double startOffset,
required String scrollLabel,
@Default(false) bool showSegment,
}) = __Segment;
}

View file

@ -1,15 +1,16 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
class PendingAlbumUpload {
final LocalAsset asset;
final double progress;
final bool failed;
part 'pending_album_uploads.provider.freezed.dart';
const PendingAlbumUpload({required this.asset, this.progress = 0.0, this.failed = false});
PendingAlbumUpload copyWith({double? progress, bool? failed}) =>
PendingAlbumUpload(asset: asset, progress: progress ?? this.progress, failed: failed ?? this.failed);
@freezed
abstract class PendingAlbumUpload with _$PendingAlbumUpload {
const factory PendingAlbumUpload({
required LocalAsset asset,
@Default(0.0) double progress,
@Default(false) bool failed,
}) = _PendingAlbumUpload;
}
class AlbumPendingUploadsNotifier extends AutoDisposeFamilyNotifier<List<PendingAlbumUpload>, String> {

View file

@ -1,26 +1,22 @@
import 'dart:async';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:logging/logging.dart';
import 'package:native_video_player/native_video_player.dart';
import 'package:wakelock_plus/wakelock_plus.dart';
part 'video_player_provider.freezed.dart';
enum VideoPlaybackStatus { paused, playing, buffering, completed }
class VideoPlayerState {
final Duration position;
final Duration duration;
final VideoPlaybackStatus status;
const VideoPlayerState({required this.position, required this.duration, required this.status});
VideoPlayerState copyWith({Duration? position, Duration? duration, VideoPlaybackStatus? status}) {
return VideoPlayerState(
position: position ?? this.position,
duration: duration ?? this.duration,
status: status ?? this.status,
);
}
@freezed
abstract class VideoPlayerState with _$VideoPlayerState {
const factory VideoPlayerState({
required Duration position,
required Duration duration,
required VideoPlaybackStatus status,
}) = _VideoPlayerState;
}
const _defaultState = VideoPlayerState(
@ -221,7 +217,7 @@ class VideoPlayerNotifier extends StateNotifier<VideoPlayerState> {
state = state.copyWith(
position: position,
status: state.status == VideoPlaybackStatus.buffering ? VideoPlaybackStatus.playing : null,
status: state.status == VideoPlaybackStatus.buffering ? VideoPlaybackStatus.playing : state.status,
);
}

View file

@ -15,18 +15,9 @@ import 'package:logging/logging.dart';
part 'drift_backup.provider.freezed.dart';
class EnqueueStatus {
final int enqueueCount;
final int totalCount;
const EnqueueStatus({required this.enqueueCount, required this.totalCount});
EnqueueStatus copyWith({int? enqueueCount, int? totalCount}) {
return EnqueueStatus(enqueueCount: enqueueCount ?? this.enqueueCount, totalCount: totalCount ?? this.totalCount);
}
@override
String toString() => 'EnqueueStatus(enqueueCount: $enqueueCount, totalCount: $totalCount)';
@freezed
abstract class EnqueueStatus with _$EnqueueStatus {
const factory EnqueueStatus({required int enqueueCount, required int totalCount}) = _EnqueueStatus;
}
@freezed

View file

@ -1,5 +1,6 @@
import 'dart:async';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
@ -8,48 +9,20 @@ import 'package:immich_mobile/providers/infrastructure/settings.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/services/cleanup.service.dart';
class CleanupState {
final DateTime? selectedDate;
final List<LocalAsset> assetsToDelete;
final int totalBytes;
final bool isScanning;
final bool isDeleting;
final AssetKeepType keepMediaType;
final bool keepFavorites;
final Set<String> keepAlbumIds;
part 'cleanup.provider.freezed.dart';
const CleanupState({
this.selectedDate,
this.assetsToDelete = const [],
this.totalBytes = 0,
this.isScanning = false,
this.isDeleting = false,
this.keepMediaType = AssetKeepType.none,
this.keepFavorites = true,
this.keepAlbumIds = const {},
});
CleanupState copyWith({
@freezed
abstract class CleanupState with _$CleanupState {
const factory CleanupState({
DateTime? selectedDate,
List<LocalAsset>? assetsToDelete,
int? totalBytes,
bool? isScanning,
bool? isDeleting,
AssetKeepType? keepMediaType,
bool? keepFavorites,
Set<String>? keepAlbumIds,
}) {
return CleanupState(
selectedDate: selectedDate ?? this.selectedDate,
assetsToDelete: assetsToDelete ?? this.assetsToDelete,
totalBytes: totalBytes ?? this.totalBytes,
isScanning: isScanning ?? this.isScanning,
isDeleting: isDeleting ?? this.isDeleting,
keepMediaType: keepMediaType ?? this.keepMediaType,
keepFavorites: keepFavorites ?? this.keepFavorites,
keepAlbumIds: keepAlbumIds ?? this.keepAlbumIds,
);
}
@Default([]) List<LocalAsset> assetsToDelete,
@Default(0) int totalBytes,
@Default(false) bool isScanning,
@Default(false) bool isDeleting,
@Default(AssetKeepType.none) AssetKeepType keepMediaType,
@Default(true) bool keepFavorites,
@Default({}) Set<String> keepAlbumIds,
}) = _CleanupState;
}
final cleanupProvider = StateNotifierProvider<CleanupNotifier, CleanupState>((ref) {

View file

@ -1,25 +1,15 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:immich_mobile/models/albums/album_search.model.dart';
import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart';
class AlbumFilter {
String? userId;
String? query;
QuickFilterMode mode;
part 'album_filter.utils.freezed.dart';
AlbumFilter({required this.mode, this.userId, this.query});
AlbumFilter copyWith({String? userId, String? query, QuickFilterMode? mode}) {
return AlbumFilter(userId: userId ?? this.userId, query: query ?? this.query, mode: mode ?? this.mode);
}
@freezed
abstract class AlbumFilter with _$AlbumFilter {
const factory AlbumFilter({required QuickFilterMode mode, String? userId, String? query}) = _AlbumFilter;
}
class AlbumSort {
AlbumSortMode mode;
bool isReverse;
AlbumSort({required this.mode, this.isReverse = false});
AlbumSort copyWith({AlbumSortMode? mode, bool? isReverse}) {
return AlbumSort(mode: mode ?? this.mode, isReverse: isReverse ?? this.isReverse);
}
@freezed
abstract class AlbumSort with _$AlbumSort {
const factory AlbumSort({required AlbumSortMode mode, @Default(false) bool isReverse}) = _AlbumSort;
}

View file

@ -1,6 +1,7 @@
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/duration_extensions.dart';
import 'package:immich_mobile/generated/translations.g.dart';
@ -9,6 +10,8 @@ import 'package:intl/intl.dart';
import 'package:timezone/timezone.dart' as tz;
import 'package:timezone/timezone.dart';
part 'date_time_picker.freezed.dart';
Future<String?> showDateTimePicker({
required BuildContext context,
DateTime? initialDateTime,
@ -165,39 +168,19 @@ class _DateTimePicker extends HookWidget {
}
}
class _TimeZoneOffset implements Comparable<_TimeZoneOffset> {
final String display;
final Location location;
@freezed
abstract class _TimeZoneOffset with _$TimeZoneOffset implements Comparable<_TimeZoneOffset> {
const _TimeZoneOffset._();
const _TimeZoneOffset({required this.display, required this.location});
const factory _TimeZoneOffset({required String display, required Location location}) = __TimeZoneOffset;
_TimeZoneOffset copyWith({String? display, Location? location}) {
return _TimeZoneOffset(display: display ?? this.display, location: location ?? this.location);
}
factory _TimeZoneOffset.fromLocation(tz.Location l) =>
_TimeZoneOffset(display: _getFormattedOffset(l.currentTimeZone.offset, l), location: l);
int get offsetInMilliseconds => location.currentTimeZone.offset;
_TimeZoneOffset.fromLocation(tz.Location l)
: display = _getFormattedOffset(l.currentTimeZone.offset, l),
location = l;
@override
int compareTo(_TimeZoneOffset other) {
return offsetInMilliseconds.compareTo(other.offsetInMilliseconds);
}
@override
String toString() => '_TimeZoneOffset(display: $display, location: $location)';
@override
bool operator ==(Object other) {
if (identical(this, other)) {
return true;
}
return other is _TimeZoneOffset && other.display == display && other.offsetInMilliseconds == offsetInMilliseconds;
}
@override
int get hashCode => display.hashCode ^ offsetInMilliseconds.hashCode ^ location.hashCode;
}

View file

@ -69,12 +69,12 @@ void main() {
fakeAsync((async) {
final image = _task(
'live-image',
metaData: LivePhotosMetadata(part: LivePhotosPart.image, id: 'live-1').toJson(),
metaData: const LivePhotosMetadata(part: LivePhotosPart.image, id: 'live-1').toJson(),
);
final video = _task(
'live-video',
filename: 'photo.MOV',
metaData: LivePhotosMetadata(part: LivePhotosPart.video, id: 'live-1').toJson(),
metaData: const LivePhotosMetadata(part: LivePhotosPart.video, id: 'live-1').toJson(),
);
onProgress(TaskProgressUpdate(image, 0.9));
onProgress(TaskProgressUpdate(video, 0.9));