Source Code added
This commit is contained in:
parent
800376eafd
commit
9efa9bc6dd
3912 changed files with 754770 additions and 2 deletions
17
mobile/lib/extensions/asset_extensions.dart
Normal file
17
mobile/lib/extensions/asset_extensions.dart
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/utils/timezone.dart';
|
||||
|
||||
extension TZExtension on Asset {
|
||||
/// Returns the created time of the asset from the exif info (if available) or from
|
||||
/// the fileCreatedAt field, adjusted to the timezone value from the exif info along with
|
||||
/// the timezone offset in [Duration]
|
||||
(DateTime, Duration) getTZAdjustedTimeAndOffset() {
|
||||
DateTime dt = fileCreatedAt.toLocal();
|
||||
|
||||
if (exifInfo?.dateTimeOriginal != null) {
|
||||
return applyTimezoneOffset(dateTime: exifInfo!.dateTimeOriginal!, timeZone: exifInfo?.timeZone);
|
||||
}
|
||||
|
||||
return (dt, dt.timeZoneOffset);
|
||||
}
|
||||
}
|
||||
36
mobile/lib/extensions/asyncvalue_extensions.dart
Normal file
36
mobile/lib/extensions/asyncvalue_extensions.dart
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_loading_indicator.dart';
|
||||
import 'package:immich_mobile/widgets/common/scaffold_error_body.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
extension LogOnError<T> on AsyncValue<T> {
|
||||
static final Logger _asyncErrorLogger = Logger("AsyncValue");
|
||||
|
||||
/// Used to return the [ImmichLoadingIndicator] and [ScaffoldErrorBody] widgets by default on loading
|
||||
/// and error cases respectively
|
||||
Widget widgetWhen({
|
||||
bool skipLoadingOnRefresh = true,
|
||||
Widget Function()? onLoading,
|
||||
Widget Function(Object? error, StackTrace? stack)? onError,
|
||||
required Widget Function(T data) onData,
|
||||
}) {
|
||||
if (isLoading) {
|
||||
bool skip = false;
|
||||
if (isRefreshing) {
|
||||
skip = skipLoadingOnRefresh;
|
||||
}
|
||||
|
||||
if (!skip) {
|
||||
return onLoading?.call() ?? const Center(child: ImmichLoadingIndicator());
|
||||
}
|
||||
}
|
||||
|
||||
if (hasError && !hasValue) {
|
||||
_asyncErrorLogger.severe('Could not load value', error, stackTrace);
|
||||
return onError?.call(error, stackTrace) ?? ScaffoldErrorBody(errorMsg: error?.toString());
|
||||
}
|
||||
|
||||
return onData(requireValue);
|
||||
}
|
||||
}
|
||||
67
mobile/lib/extensions/build_context_extensions.dart
Normal file
67
mobile/lib/extensions/build_context_extensions.dart
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
extension ContextHelper on BuildContext {
|
||||
// Returns the current padding from MediaQuery
|
||||
EdgeInsets get padding => MediaQuery.paddingOf(this);
|
||||
|
||||
// Returns the current view insets from MediaQuery
|
||||
EdgeInsets get viewInsets => MediaQuery.viewInsetsOf(this);
|
||||
|
||||
// Returns the current width from MediaQuery
|
||||
double get width => MediaQuery.sizeOf(this).width;
|
||||
|
||||
// Returns the current height from MediaQuery
|
||||
double get height => MediaQuery.sizeOf(this).height;
|
||||
|
||||
// Returns the current size from MediaQuery
|
||||
Size get sizeData => MediaQuery.sizeOf(this);
|
||||
|
||||
// Returns true if the app is running on a mobile device (!tablets)
|
||||
bool get isMobile => width < 550;
|
||||
|
||||
// Returns the current device pixel ratio from MediaQuery
|
||||
double get devicePixelRatio => MediaQuery.devicePixelRatioOf(this);
|
||||
|
||||
// Returns the current orientation from MediaQuery
|
||||
Orientation get orientation => MediaQuery.orientationOf(this);
|
||||
|
||||
// Returns the current platform brightness from MediaQuery
|
||||
Brightness get platformBrightness => MediaQuery.platformBrightnessOf(this);
|
||||
|
||||
// Returns the current ThemeData
|
||||
ThemeData get themeData => Theme.of(this);
|
||||
|
||||
// Returns true if the app is using a dark theme
|
||||
bool get isDarkTheme => themeData.brightness == Brightness.dark;
|
||||
|
||||
// Returns the current Primary color of the Theme
|
||||
Color get primaryColor => themeData.colorScheme.primary;
|
||||
Color get logoYellow => const Color.fromARGB(255, 255, 184, 0);
|
||||
Color get logoRed => const Color.fromARGB(255, 230, 65, 30);
|
||||
Color get logoPink => const Color.fromARGB(255, 222, 127, 179);
|
||||
Color get logoGreen => const Color.fromARGB(255, 49, 164, 82);
|
||||
|
||||
// Returns the Scaffold background color of the Theme
|
||||
Color get scaffoldBackgroundColor => colorScheme.surface;
|
||||
|
||||
// Returns the current TextTheme
|
||||
TextTheme get textTheme => themeData.textTheme;
|
||||
|
||||
// Current ColorScheme used
|
||||
ColorScheme get colorScheme => themeData.colorScheme;
|
||||
|
||||
// Navigate by pushing or popping routes from the current context
|
||||
NavigatorState get navigator => Navigator.of(this);
|
||||
|
||||
// Showing material banners from the current context
|
||||
ScaffoldMessengerState get scaffoldMessenger => ScaffoldMessenger.of(this);
|
||||
|
||||
// Pop-out from the current context with optional result
|
||||
void pop<T>([T? result]) => Navigator.of(this).pop(result);
|
||||
|
||||
// Managing focus within the widget tree from the current context
|
||||
FocusScopeNode get focusScope => FocusScope.of(this);
|
||||
|
||||
// Show SnackBars from the current context
|
||||
void showSnackBar(SnackBar snackBar) => ScaffoldMessenger.of(this).showSnackBar(snackBar);
|
||||
}
|
||||
10
mobile/lib/extensions/codec_extensions.dart
Normal file
10
mobile/lib/extensions/codec_extensions.dart
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/painting.dart';
|
||||
|
||||
extension CodecImageInfoExtension on Codec {
|
||||
Future<ImageInfo> getImageInfo({double scale = 1.0}) async {
|
||||
final frame = await getNextFrame();
|
||||
return ImageInfo(image: frame.image, scale: scale);
|
||||
}
|
||||
}
|
||||
72
mobile/lib/extensions/collection_extensions.dart
Normal file
72
mobile/lib/extensions/collection_extensions.dart
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
import 'dart:typed_data';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/utils/hash.dart';
|
||||
|
||||
extension ListExtension<E> on List<E> {
|
||||
List<E> uniqueConsecutive({int Function(E a, E b)? compare, void Function(E a, E b)? onDuplicate}) {
|
||||
compare ??= (E a, E b) => a == b ? 0 : 1;
|
||||
int i = 1, j = 1;
|
||||
for (; i < length; i++) {
|
||||
if (compare(this[i - 1], this[i]) != 0) {
|
||||
if (i != j) {
|
||||
this[j] = this[i];
|
||||
}
|
||||
j++;
|
||||
} else if (onDuplicate != null) {
|
||||
onDuplicate(this[i - 1], this[i]);
|
||||
}
|
||||
}
|
||||
length = length == 0 ? 0 : j;
|
||||
return this;
|
||||
}
|
||||
|
||||
ListSlice<E> nestedSlice(int start, int end) {
|
||||
if (this is ListSlice) {
|
||||
final ListSlice<E> self = this as ListSlice<E>;
|
||||
return ListSlice<E>(self.source, self.start + start, self.start + end);
|
||||
}
|
||||
return ListSlice<E>(this, start, end);
|
||||
}
|
||||
}
|
||||
|
||||
extension IntListExtension on Iterable<int> {
|
||||
Int64List toInt64List() {
|
||||
final list = Int64List(length);
|
||||
list.setAll(0, this);
|
||||
return list;
|
||||
}
|
||||
}
|
||||
|
||||
extension AssetListExtension on Iterable<Asset> {
|
||||
/// Returns the assets that are already available in the Immich server
|
||||
Iterable<Asset> remoteOnly({void Function()? errorCallback}) {
|
||||
final bool onlyRemote = every((e) => e.isRemote);
|
||||
if (!onlyRemote) {
|
||||
if (errorCallback != null) errorCallback();
|
||||
return where((a) => a.isRemote);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/// Returns the assets that are owned by the user passed to the [owner] param
|
||||
/// If [owner] is null, an empty list is returned
|
||||
Iterable<Asset> ownedOnly(UserDto? owner, {void Function()? errorCallback}) {
|
||||
if (owner == null) return [];
|
||||
final isarUserId = fastHash(owner.id);
|
||||
final bool onlyOwned = every((e) => e.ownerId == isarUserId);
|
||||
if (!onlyOwned) {
|
||||
if (errorCallback != null) errorCallback();
|
||||
return where((a) => a.ownerId == isarUserId);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
extension SortedByProperty<T> on Iterable<T> {
|
||||
Iterable<T> sortedByField(Comparable Function(T e) key) {
|
||||
return sorted((a, b) => key(a).compareTo(key(b)));
|
||||
}
|
||||
}
|
||||
87
mobile/lib/extensions/datetime_extensions.dart
Normal file
87
mobile/lib/extensions/datetime_extensions.dart
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
import 'dart:ui';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
|
||||
extension TimeAgoExtension on DateTime {
|
||||
/// Displays the time difference of this [DateTime] object to the current time as a [String]
|
||||
String timeAgo({bool numericDates = true}) {
|
||||
DateTime date = toLocal();
|
||||
final now = DateTime.now().toLocal();
|
||||
final difference = now.difference(date);
|
||||
|
||||
if (difference.inSeconds < 5) {
|
||||
return 'Just now';
|
||||
} else if (difference.inSeconds < 60) {
|
||||
return '${difference.inSeconds} seconds ago';
|
||||
} else if (difference.inMinutes <= 1) {
|
||||
return (numericDates) ? '1 minute ago' : 'A minute ago';
|
||||
} else if (difference.inMinutes < 60) {
|
||||
return '${difference.inMinutes} minutes ago';
|
||||
} else if (difference.inHours <= 1) {
|
||||
return (numericDates) ? '1 hour ago' : 'An hour ago';
|
||||
} else if (difference.inHours < 60) {
|
||||
return '${difference.inHours} hours ago';
|
||||
} else if (difference.inDays <= 1) {
|
||||
return (numericDates) ? '1 day ago' : 'Yesterday';
|
||||
} else if (difference.inDays < 6) {
|
||||
return '${difference.inDays} days ago';
|
||||
} else if ((difference.inDays / 7).ceil() <= 1) {
|
||||
return (numericDates) ? '1 week ago' : 'Last week';
|
||||
} else if ((difference.inDays / 7).ceil() < 4) {
|
||||
return '${(difference.inDays / 7).ceil()} weeks ago';
|
||||
} else if ((difference.inDays / 30).ceil() <= 1) {
|
||||
return (numericDates) ? '1 month ago' : 'Last month';
|
||||
} else if ((difference.inDays / 30).ceil() < 30) {
|
||||
return '${(difference.inDays / 30).ceil()} months ago';
|
||||
} else if ((difference.inDays / 365).ceil() <= 1) {
|
||||
return (numericDates) ? '1 year ago' : 'Last year';
|
||||
}
|
||||
return '${(difference.inDays / 365).floor()} years ago';
|
||||
}
|
||||
}
|
||||
|
||||
/// Extension to format date ranges according to UI requirements
|
||||
extension DateRangeFormatting on DateTime {
|
||||
/// Formats a date range according to specific rules:
|
||||
/// - Single date of this year: "Aug 28"
|
||||
/// - Single date of other year: "Aug 28, 2023"
|
||||
/// - Date range of this year: "Mar 23-May 31"
|
||||
/// - Date range of other year: "Aug 28 - Sep 30, 2023"
|
||||
/// - Date range over multiple years: "Apr 17, 2021 - Apr 9, 2022"
|
||||
static String formatDateRange(DateTime startDate, DateTime endDate, Locale? locale) {
|
||||
final now = DateTime.now();
|
||||
final currentYear = now.year;
|
||||
final localeString = locale?.toString() ?? 'en_US';
|
||||
|
||||
// Check if it's a single date (same day)
|
||||
if (startDate.year == endDate.year && startDate.month == endDate.month && startDate.day == endDate.day) {
|
||||
if (startDate.year == currentYear) {
|
||||
// Single date of this year: "Aug 28"
|
||||
return DateFormat.MMMd(localeString).format(startDate);
|
||||
} else {
|
||||
// Single date of other year: "Aug 28, 2023"
|
||||
return DateFormat.yMMMd(localeString).format(startDate);
|
||||
}
|
||||
}
|
||||
|
||||
// It's a date range
|
||||
if (startDate.year == endDate.year) {
|
||||
// Same year
|
||||
if (startDate.year == currentYear) {
|
||||
// Date range of this year: "Mar 23-May 31"
|
||||
final startFormatted = DateFormat.MMMd(localeString).format(startDate);
|
||||
final endFormatted = DateFormat.MMMd(localeString).format(endDate);
|
||||
return '$startFormatted - $endFormatted';
|
||||
} else {
|
||||
// Date range of other year: "Aug 28 - Sep 30, 2023"
|
||||
final startFormatted = DateFormat.MMMd(localeString).format(startDate);
|
||||
final endFormatted = DateFormat.MMMd(localeString).format(endDate);
|
||||
return '$startFormatted - $endFormatted, ${startDate.year}';
|
||||
}
|
||||
} else {
|
||||
// Date range over multiple years: "Apr 17, 2021 - Apr 9, 2022"
|
||||
final startFormatted = DateFormat.yMMMd(localeString).format(startDate);
|
||||
final endFormatted = DateFormat.yMMMd(localeString).format(endDate);
|
||||
return '$startFormatted - $endFormatted';
|
||||
}
|
||||
}
|
||||
}
|
||||
17
mobile/lib/extensions/duration_extensions.dart
Normal file
17
mobile/lib/extensions/duration_extensions.dart
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
extension TZOffsetExtension on Duration {
|
||||
/// Formats the duration in the format of ±HH:MM
|
||||
String formatAsOffset() =>
|
||||
"${isNegative ? '-' : '+'}${inHours.abs().toString().padLeft(2, '0')}:${inMinutes.abs().remainder(60).toString().padLeft(2, '0')}";
|
||||
}
|
||||
|
||||
extension DurationFormatExtension on Duration {
|
||||
String format() {
|
||||
final seconds = inSeconds.remainder(60).toString().padLeft(2, '0');
|
||||
final minutes = inMinutes.remainder(60).toString().padLeft(2, '0');
|
||||
if (inHours == 0) {
|
||||
return "$minutes:$seconds";
|
||||
}
|
||||
final hours = inHours.toString().padLeft(2, '0');
|
||||
return "$hours:$minutes:$seconds";
|
||||
}
|
||||
}
|
||||
20
mobile/lib/extensions/latlngbounds_extension.dart
Normal file
20
mobile/lib/extensions/latlngbounds_extension.dart
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import 'package:maplibre_gl/maplibre_gl.dart';
|
||||
|
||||
extension WithinBounds on LatLngBounds {
|
||||
/// Checks whether [point] is inside bounds
|
||||
bool contains(LatLng point) {
|
||||
final sw = point;
|
||||
final ne = point;
|
||||
return containsBounds(LatLngBounds(southwest: sw, northeast: ne));
|
||||
}
|
||||
|
||||
/// Checks whether [bounds] is contained inside bounds
|
||||
bool containsBounds(LatLngBounds bounds) {
|
||||
final sw = bounds.southwest;
|
||||
final ne = bounds.northeast;
|
||||
return (sw.latitude >= southwest.latitude) &&
|
||||
(ne.latitude <= northeast.latitude) &&
|
||||
(sw.longitude >= southwest.longitude) &&
|
||||
(ne.longitude <= northeast.longitude);
|
||||
}
|
||||
}
|
||||
89
mobile/lib/extensions/maplibrecontroller_extensions.dart
Normal file
89
mobile/lib/extensions/maplibrecontroller_extensions.dart
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:immich_mobile/models/map/map_marker.model.dart';
|
||||
import 'package:immich_mobile/utils/map_utils.dart';
|
||||
import 'package:maplibre_gl/maplibre_gl.dart';
|
||||
|
||||
extension MapMarkers on MapLibreMapController {
|
||||
static var _completer = Completer()..complete();
|
||||
|
||||
Future<void> addGeoJSONSourceForMarkers(List<MapMarker> markers) async {
|
||||
return addSource(
|
||||
MapUtils.defaultSourceId,
|
||||
GeojsonSourceProperties(data: MapUtils.generateGeoJsonForMarkers(markers.toList())),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> reloadAllLayersForMarkers(List<MapMarker> markers) async {
|
||||
// Wait for previous reload to complete
|
||||
if (!_completer.isCompleted) {
|
||||
return _completer.future;
|
||||
}
|
||||
_completer = Completer();
|
||||
|
||||
// !! Make sure to remove layers before sources else the native
|
||||
// maplibre library would crash when removing the source saying that
|
||||
// the source is still in use
|
||||
final existingLayers = await getLayerIds();
|
||||
if (existingLayers.contains(MapUtils.defaultHeatMapLayerId)) {
|
||||
await removeLayer(MapUtils.defaultHeatMapLayerId);
|
||||
}
|
||||
|
||||
final existingSources = await getSourceIds();
|
||||
if (existingSources.contains(MapUtils.defaultSourceId)) {
|
||||
await removeSource(MapUtils.defaultSourceId);
|
||||
}
|
||||
|
||||
await addGeoJSONSourceForMarkers(markers);
|
||||
|
||||
if (Platform.isAndroid) {
|
||||
await addCircleLayer(
|
||||
MapUtils.defaultSourceId,
|
||||
MapUtils.defaultHeatMapLayerId,
|
||||
const CircleLayerProperties(
|
||||
circleRadius: 10,
|
||||
circleColor: "rgba(150,86,34,0.7)",
|
||||
circleBlur: 1.0,
|
||||
circleOpacity: 0.7,
|
||||
circleStrokeWidth: 0.1,
|
||||
circleStrokeColor: "rgba(203,46,19,0.5)",
|
||||
circleStrokeOpacity: 0.7,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (Platform.isIOS) {
|
||||
await addHeatmapLayer(
|
||||
MapUtils.defaultSourceId,
|
||||
MapUtils.defaultHeatMapLayerId,
|
||||
MapUtils.defaultHeatMapLayerProperties,
|
||||
);
|
||||
}
|
||||
|
||||
_completer.complete();
|
||||
}
|
||||
|
||||
Future<Symbol?> addMarkerAtLatLng(LatLng centre) async {
|
||||
// no marker is displayed if asset-path is incorrect
|
||||
try {
|
||||
final ByteData bytes = await rootBundle.load("assets/location-pin.png");
|
||||
await addImage("mapMarker", bytes.buffer.asUint8List());
|
||||
return addSymbol(SymbolOptions(geometry: centre, iconImage: "mapMarker", iconSize: 0.15, iconAnchor: "bottom"));
|
||||
} finally {
|
||||
// no-op
|
||||
}
|
||||
}
|
||||
|
||||
Future<LatLngBounds> getBoundsFromPoint(Point<double> point, double distance) async {
|
||||
final southWestPx = Point(point.x - distance, point.y + distance);
|
||||
final northEastPx = Point(point.x + distance, point.y - distance);
|
||||
|
||||
final southWest = await toLatLng(southWestPx);
|
||||
final northEast = await toLatLng(northEastPx);
|
||||
|
||||
return LatLngBounds(southwest: southWest, northeast: northEast);
|
||||
}
|
||||
}
|
||||
8
mobile/lib/extensions/network_capability_extensions.dart
Normal file
8
mobile/lib/extensions/network_capability_extensions.dart
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import 'package:immich_mobile/platform/connectivity_api.g.dart';
|
||||
|
||||
extension NetworkCapabilitiesGetters on List<NetworkCapability> {
|
||||
bool get hasCellular => contains(NetworkCapability.cellular);
|
||||
bool get hasWifi => contains(NetworkCapability.wifi);
|
||||
bool get hasVpn => contains(NetworkCapability.vpn);
|
||||
bool get isUnmetered => contains(NetworkCapability.unmetered);
|
||||
}
|
||||
9
mobile/lib/extensions/platform_extensions.dart
Normal file
9
mobile/lib/extensions/platform_extensions.dart
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import 'package:flutter/foundation.dart';
|
||||
|
||||
extension CurrentPlatform on TargetPlatform {
|
||||
@pragma('vm:prefer-inline')
|
||||
static bool get isIOS => defaultTargetPlatform == TargetPlatform.iOS;
|
||||
|
||||
@pragma('vm:prefer-inline')
|
||||
static bool get isAndroid => defaultTargetPlatform == TargetPlatform.android;
|
||||
}
|
||||
5
mobile/lib/extensions/response_extensions.dart
Normal file
5
mobile/lib/extensions/response_extensions.dart
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import 'package:http/http.dart';
|
||||
|
||||
extension LoggerExtension on Response {
|
||||
String toLoggerString() => "Status: $statusCode $reasonPhrase\n\n$body";
|
||||
}
|
||||
34
mobile/lib/extensions/scroll_extensions.dart
Normal file
34
mobile/lib/extensions/scroll_extensions.dart
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import 'package:flutter/cupertino.dart';
|
||||
|
||||
// https://stackoverflow.com/a/74453792
|
||||
class FastScrollPhysics extends ScrollPhysics {
|
||||
const FastScrollPhysics({super.parent});
|
||||
|
||||
@override
|
||||
FastScrollPhysics applyTo(ScrollPhysics? ancestor) {
|
||||
return FastScrollPhysics(parent: buildParent(ancestor));
|
||||
}
|
||||
|
||||
@override
|
||||
SpringDescription get spring => const SpringDescription(mass: 1, stiffness: 402.49984375, damping: 40);
|
||||
}
|
||||
|
||||
class FastClampingScrollPhysics extends ClampingScrollPhysics {
|
||||
const FastClampingScrollPhysics({super.parent});
|
||||
|
||||
@override
|
||||
FastClampingScrollPhysics applyTo(ScrollPhysics? ancestor) {
|
||||
return FastClampingScrollPhysics(parent: buildParent(ancestor));
|
||||
}
|
||||
|
||||
@override
|
||||
SpringDescription get spring => const SpringDescription(
|
||||
// When swiping between videos on Android, the placeholder of the first opened video
|
||||
// can briefly be seen and cause a flicker effect if the video begins to initialize
|
||||
// before the animation finishes - probably a bug in PhotoViewGallery's animation handling
|
||||
// Making the animation faster is not just stylistic, but also helps to avoid this flicker
|
||||
mass: 1,
|
||||
stiffness: 1601.2499609375,
|
||||
damping: 80,
|
||||
);
|
||||
}
|
||||
35
mobile/lib/extensions/string_extensions.dart
Normal file
35
mobile/lib/extensions/string_extensions.dart
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import 'dart:convert';
|
||||
|
||||
extension StringExtension on String {
|
||||
String capitalize() {
|
||||
return split(" ").map((str) => str.isEmpty ? str : str[0].toUpperCase() + str.substring(1)).join(" ");
|
||||
}
|
||||
}
|
||||
|
||||
extension DurationExtension on String {
|
||||
/// Parses and returns the string of format HH:MM:SS as a duration object else null
|
||||
Duration? toDuration() {
|
||||
try {
|
||||
final parts = split(':').map((e) => double.parse(e).toInt()).toList(growable: false);
|
||||
return Duration(hours: parts[0], minutes: parts[1], seconds: parts[2]);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
double toDouble() {
|
||||
return double.parse(this);
|
||||
}
|
||||
|
||||
int toInt() {
|
||||
return int.parse(this);
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, dynamic>? tryJsonDecode(dynamic json) {
|
||||
try {
|
||||
return jsonDecode(json) as Map<String, dynamic>;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
16
mobile/lib/extensions/theme_extensions.dart
Normal file
16
mobile/lib/extensions/theme_extensions.dart
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
extension ImmichColorSchemeExtensions on ColorScheme {
|
||||
bool get _isDarkMode => brightness == Brightness.dark;
|
||||
Color get onSurfaceSecondary => _isDarkMode ? onSurface.darken(amount: .3) : onSurface.lighten(amount: .3);
|
||||
}
|
||||
|
||||
extension ColorExtensions on Color {
|
||||
Color lighten({double amount = 0.1}) {
|
||||
return Color.alphaBlend(Colors.white.withValues(alpha: amount), this);
|
||||
}
|
||||
|
||||
Color darken({double amount = 0.1}) {
|
||||
return Color.alphaBlend(Colors.black.withValues(alpha: amount), this);
|
||||
}
|
||||
}
|
||||
46
mobile/lib/extensions/translate_extensions.dart
Normal file
46
mobile/lib/extensions/translate_extensions.dart
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:intl/message_format.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_mobile/utils/debug_print.dart';
|
||||
|
||||
extension StringTranslateExtension on String {
|
||||
String t({BuildContext? context, Map<String, Object>? args}) {
|
||||
return _translateHelper(context, this, args);
|
||||
}
|
||||
}
|
||||
|
||||
extension TextTranslateExtension on Text {
|
||||
Text t({BuildContext? context, Map<String, Object>? args}) {
|
||||
return Text(
|
||||
_translateHelper(context, data ?? '', args),
|
||||
key: key,
|
||||
style: style,
|
||||
strutStyle: strutStyle,
|
||||
textAlign: textAlign,
|
||||
textDirection: textDirection,
|
||||
locale: locale,
|
||||
softWrap: softWrap,
|
||||
overflow: overflow,
|
||||
textScaler: textScaler,
|
||||
maxLines: maxLines,
|
||||
semanticsLabel: semanticsLabel,
|
||||
textWidthBasis: textWidthBasis,
|
||||
textHeightBehavior: textHeightBehavior,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _translateHelper(BuildContext? context, String key, [Map<String, Object>? args]) {
|
||||
if (key.isEmpty) {
|
||||
return '';
|
||||
}
|
||||
try {
|
||||
final translatedMessage = key.tr(context: context);
|
||||
return args != null
|
||||
? MessageFormat(translatedMessage, locale: Intl.defaultLocale ?? 'en').format(args)
|
||||
: translatedMessage;
|
||||
} catch (e) {
|
||||
dPrint(() => 'Translation failed for key "$key". Error: $e');
|
||||
return key;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue