mirror of
https://github.com/immich-app/immich.git
synced 2025-07-09 03:04:16 -04:00
feat: appbar
This commit is contained in:
parent
5385d43c8c
commit
8450c8cc4f
@ -21,6 +21,7 @@ dart_code_metrics:
|
|||||||
- arguments-ordering:
|
- arguments-ordering:
|
||||||
last:
|
last:
|
||||||
- child
|
- child
|
||||||
|
- children
|
||||||
- avoid-accessing-other-classes-private-members
|
- avoid-accessing-other-classes-private-members
|
||||||
- avoid-assigning-to-static-field
|
- avoid-assigning-to-static-field
|
||||||
- avoid-assignments-as-conditions
|
- avoid-assignments-as-conditions
|
||||||
|
@ -38,7 +38,7 @@
|
|||||||
},
|
},
|
||||||
"logs": {
|
"logs": {
|
||||||
"title": "Logs",
|
"title": "Logs",
|
||||||
"no_logs": "No logs available",
|
"no_logs_message": "No logs available",
|
||||||
"detail": {
|
"detail": {
|
||||||
"title": "Log Detail",
|
"title": "Log Detail",
|
||||||
"message_heading": "MESSAGE",
|
"message_heading": "MESSAGE",
|
||||||
@ -49,6 +49,27 @@
|
|||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
"loading": "Loading",
|
"loading": "Loading",
|
||||||
"copied_long": "Copied to clipboard"
|
"copied_long": "Copied to clipboard",
|
||||||
|
"components": {
|
||||||
|
"grid_empty_message": "Capture memories to start populating the timeline",
|
||||||
|
"appbar": {
|
||||||
|
"server_storage": "Server Storage",
|
||||||
|
"storage_used": "{used: String} of {total: String} used",
|
||||||
|
"action_logs": "Logs",
|
||||||
|
"action_settings": "Settings",
|
||||||
|
"action_signout": "Sign Out",
|
||||||
|
"footer_documentation": "Documentation",
|
||||||
|
"footer_github": "Github",
|
||||||
|
"server_version_label": "Server Version",
|
||||||
|
"server_url_label": "Server URL",
|
||||||
|
"server_version_common_error": "Please check your network connection, make sure the server is reachable and app/server versions are compatible.",
|
||||||
|
"server_version_major_error": "Server is out of date. Please update to the latest major version.",
|
||||||
|
"server_version_minor_error": "Server is out of date. Please update to the latest minor version.",
|
||||||
|
"app_version_major_error": "Mobile App is out of date. Please update to the latest major version.",
|
||||||
|
"app_version_minor_error": "Mobile App is out of date. Please update to the latest minor version.",
|
||||||
|
"app_version_ok": "Client and Server are up-to-date",
|
||||||
|
"app_version_label": "App Version"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,5 +1,7 @@
|
|||||||
import 'package:immich_mobile/domain/models/server-info/server_config.model.dart';
|
import 'package:immich_mobile/domain/models/server-info/server_config.model.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/server-info/server_disk_info.model.dart';
|
||||||
import 'package:immich_mobile/domain/models/server-info/server_features.model.dart';
|
import 'package:immich_mobile/domain/models/server-info/server_features.model.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/server-info/server_version.model.dart';
|
||||||
|
|
||||||
abstract interface class IServerApiRepository {
|
abstract interface class IServerApiRepository {
|
||||||
/// Pings and check if server is reachable
|
/// Pings and check if server is reachable
|
||||||
@ -10,4 +12,10 @@ abstract interface class IServerApiRepository {
|
|||||||
|
|
||||||
/// Fetches the server configuration and settings
|
/// Fetches the server configuration and settings
|
||||||
Future<ServerConfig?> getServerConfig();
|
Future<ServerConfig?> getServerConfig();
|
||||||
|
|
||||||
|
/// Fetches the server disk info
|
||||||
|
Future<ServerDiskInfo?> getServerDiskInfo();
|
||||||
|
|
||||||
|
/// Fetches the server version
|
||||||
|
Future<ServerVersion?> getServerVersion();
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,56 @@
|
|||||||
|
class ServerDiskInfo {
|
||||||
|
final int diskAvailableInBytes;
|
||||||
|
final int diskSizeInBytes;
|
||||||
|
final int diskUseInBytes;
|
||||||
|
final double diskUsagePercentage;
|
||||||
|
|
||||||
|
const ServerDiskInfo({
|
||||||
|
required this.diskAvailableInBytes,
|
||||||
|
required this.diskSizeInBytes,
|
||||||
|
required this.diskUseInBytes,
|
||||||
|
required this.diskUsagePercentage,
|
||||||
|
});
|
||||||
|
|
||||||
|
const ServerDiskInfo.initial()
|
||||||
|
: diskSizeInBytes = -1,
|
||||||
|
diskUseInBytes = -1,
|
||||||
|
diskAvailableInBytes = -1,
|
||||||
|
diskUsagePercentage = -1;
|
||||||
|
|
||||||
|
ServerDiskInfo copyWith({
|
||||||
|
int? diskAvailableInBytes,
|
||||||
|
int? diskSizeInBytes,
|
||||||
|
int? diskUseInBytes,
|
||||||
|
double? diskUsagePercentage,
|
||||||
|
}) {
|
||||||
|
return ServerDiskInfo(
|
||||||
|
diskAvailableInBytes: diskAvailableInBytes ?? this.diskAvailableInBytes,
|
||||||
|
diskSizeInBytes: diskSizeInBytes ?? this.diskSizeInBytes,
|
||||||
|
diskUseInBytes: diskUseInBytes ?? this.diskUseInBytes,
|
||||||
|
diskUsagePercentage: diskUsagePercentage ?? this.diskUsagePercentage,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'ServerDiskInfo(diskAvailableInBytes: $diskAvailableInBytes, diskSizeInBytes: $diskSizeInBytes, diskUseInBytes: $diskUseInBytes, diskUsagePercentage: $diskUsagePercentage)';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(covariant ServerDiskInfo other) {
|
||||||
|
if (identical(this, other)) return true;
|
||||||
|
|
||||||
|
return other.diskAvailableInBytes == diskAvailableInBytes &&
|
||||||
|
other.diskSizeInBytes == diskSizeInBytes &&
|
||||||
|
other.diskUseInBytes == diskUseInBytes &&
|
||||||
|
other.diskUsagePercentage == diskUsagePercentage;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode {
|
||||||
|
return diskAvailableInBytes.hashCode ^
|
||||||
|
diskSizeInBytes.hashCode ^
|
||||||
|
diskUseInBytes.hashCode ^
|
||||||
|
diskUsagePercentage.hashCode;
|
||||||
|
}
|
||||||
|
}
|
@ -1,37 +0,0 @@
|
|||||||
import 'package:immich_mobile/domain/models/server-info/server_config.model.dart';
|
|
||||||
import 'package:immich_mobile/domain/models/server-info/server_features.model.dart';
|
|
||||||
|
|
||||||
class ServerFeatureConfig {
|
|
||||||
final ServerFeatures features;
|
|
||||||
final ServerConfig config;
|
|
||||||
|
|
||||||
const ServerFeatureConfig({required this.features, required this.config});
|
|
||||||
|
|
||||||
ServerFeatureConfig copyWith({
|
|
||||||
ServerFeatures? features,
|
|
||||||
ServerConfig? config,
|
|
||||||
}) {
|
|
||||||
return ServerFeatureConfig(
|
|
||||||
features: features ?? this.features,
|
|
||||||
config: config ?? this.config,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const ServerFeatureConfig.initial()
|
|
||||||
: features = const ServerFeatures.initial(),
|
|
||||||
config = const ServerConfig.initial();
|
|
||||||
|
|
||||||
@override
|
|
||||||
String toString() =>
|
|
||||||
'ServerFeatureConfig(features: $features, config: $config)';
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(covariant ServerFeatureConfig other) {
|
|
||||||
if (identical(this, other)) return true;
|
|
||||||
|
|
||||||
return other.features == features && other.config == config;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode => features.hashCode ^ config.hashCode;
|
|
||||||
}
|
|
@ -0,0 +1,56 @@
|
|||||||
|
import 'package:immich_mobile/domain/models/server-info/server_config.model.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/server-info/server_disk_info.model.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/server-info/server_features.model.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/server-info/server_version.model.dart';
|
||||||
|
|
||||||
|
class ServerInfo {
|
||||||
|
final ServerFeatures features;
|
||||||
|
final ServerConfig config;
|
||||||
|
final ServerDiskInfo disk;
|
||||||
|
final ServerVersion version;
|
||||||
|
|
||||||
|
const ServerInfo({
|
||||||
|
required this.features,
|
||||||
|
required this.config,
|
||||||
|
required this.disk,
|
||||||
|
required this.version,
|
||||||
|
});
|
||||||
|
|
||||||
|
ServerInfo copyWith({
|
||||||
|
ServerFeatures? features,
|
||||||
|
ServerConfig? config,
|
||||||
|
ServerDiskInfo? disk,
|
||||||
|
ServerVersion? version,
|
||||||
|
}) {
|
||||||
|
return ServerInfo(
|
||||||
|
features: features ?? this.features,
|
||||||
|
config: config ?? this.config,
|
||||||
|
disk: disk ?? this.disk,
|
||||||
|
version: version ?? this.version,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ServerInfo.initial()
|
||||||
|
: features = const ServerFeatures.initial(),
|
||||||
|
config = const ServerConfig.initial(),
|
||||||
|
disk = const ServerDiskInfo.initial(),
|
||||||
|
version = const ServerVersion.initial();
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() =>
|
||||||
|
'ServerInfo(features: $features, config: $config, disk: $disk, version: $version)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(covariant ServerInfo other) {
|
||||||
|
if (identical(this, other)) return true;
|
||||||
|
|
||||||
|
return other.features == features &&
|
||||||
|
other.config == config &&
|
||||||
|
other.disk == disk &&
|
||||||
|
other.version == version;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode =>
|
||||||
|
features.hashCode ^ config.hashCode ^ disk.hashCode ^ version.hashCode;
|
||||||
|
}
|
@ -0,0 +1,38 @@
|
|||||||
|
class ServerVersion {
|
||||||
|
final int major;
|
||||||
|
final int minor;
|
||||||
|
final int patch;
|
||||||
|
|
||||||
|
const ServerVersion({
|
||||||
|
required this.major,
|
||||||
|
required this.minor,
|
||||||
|
required this.patch,
|
||||||
|
});
|
||||||
|
|
||||||
|
ServerVersion copyWith({int? major, int? minor, int? patch}) {
|
||||||
|
return ServerVersion(
|
||||||
|
major: major ?? this.major,
|
||||||
|
minor: minor ?? this.minor,
|
||||||
|
patch: patch ?? this.patch,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ServerVersion.initial()
|
||||||
|
: major = 1,
|
||||||
|
minor = 1,
|
||||||
|
patch = 1;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() =>
|
||||||
|
'ServerVersion(major: $major, minor: $minor, patch: $patch)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(covariant ServerVersion other) {
|
||||||
|
if (identical(this, other)) return true;
|
||||||
|
|
||||||
|
return other.major == major && other.minor == minor && other.patch == patch;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => major.hashCode ^ minor.hashCode ^ patch.hashCode;
|
||||||
|
}
|
@ -1,6 +1,8 @@
|
|||||||
import 'package:immich_mobile/domain/interfaces/api/server_api.interface.dart';
|
import 'package:immich_mobile/domain/interfaces/api/server_api.interface.dart';
|
||||||
import 'package:immich_mobile/domain/models/server-info/server_config.model.dart';
|
import 'package:immich_mobile/domain/models/server-info/server_config.model.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/server-info/server_disk_info.model.dart';
|
||||||
import 'package:immich_mobile/domain/models/server-info/server_features.model.dart';
|
import 'package:immich_mobile/domain/models/server-info/server_features.model.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/server-info/server_version.model.dart';
|
||||||
import 'package:immich_mobile/utils/mixins/log.mixin.dart';
|
import 'package:immich_mobile/utils/mixins/log.mixin.dart';
|
||||||
import 'package:openapi/api.dart' as api;
|
import 'package:openapi/api.dart' as api;
|
||||||
|
|
||||||
@ -40,6 +42,32 @@ class ServerApiRepository with LogMixin implements IServerApiRepository {
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<ServerDiskInfo?> getServerDiskInfo() async {
|
||||||
|
try {
|
||||||
|
final storage = await _serverApi.getStorage();
|
||||||
|
if (storage != null) {
|
||||||
|
return _fromStorageDto(storage);
|
||||||
|
}
|
||||||
|
} catch (e, s) {
|
||||||
|
log.e("Exception occured while fetching server disk info", e, s);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<ServerVersion?> getServerVersion() async {
|
||||||
|
try {
|
||||||
|
final version = await _serverApi.getServerVersion();
|
||||||
|
if (version != null) {
|
||||||
|
return _fromVersionDto(version);
|
||||||
|
}
|
||||||
|
} catch (e, s) {
|
||||||
|
log.e("Exception occured while fetching server version", e, s);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ServerConfig _fromConfigDto(api.ServerConfigDto dto) => ServerConfig(
|
ServerConfig _fromConfigDto(api.ServerConfigDto dto) => ServerConfig(
|
||||||
@ -50,3 +78,14 @@ ServerFeatures _fromFeatureDto(api.ServerFeaturesDto dto) => ServerFeatures(
|
|||||||
hasPasswordLogin: dto.passwordLogin,
|
hasPasswordLogin: dto.passwordLogin,
|
||||||
hasOAuthLogin: dto.oauth,
|
hasOAuthLogin: dto.oauth,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
ServerDiskInfo _fromStorageDto(api.ServerStorageResponseDto dto) =>
|
||||||
|
ServerDiskInfo(
|
||||||
|
diskAvailableInBytes: dto.diskAvailableRaw,
|
||||||
|
diskSizeInBytes: dto.diskSizeRaw,
|
||||||
|
diskUseInBytes: dto.diskUseRaw,
|
||||||
|
diskUsagePercentage: dto.diskUsagePercentage,
|
||||||
|
);
|
||||||
|
|
||||||
|
ServerVersion _fromVersionDto(api.ServerVersionResponseDto dto) =>
|
||||||
|
ServerVersion(major: dto.major, minor: dto.minor, patch: dto.patch_);
|
||||||
|
@ -19,7 +19,7 @@ import 'package:immich_mobile/domain/models/user.model.dart';
|
|||||||
import 'package:immich_mobile/domain/services/album_sync.service.dart';
|
import 'package:immich_mobile/domain/services/album_sync.service.dart';
|
||||||
import 'package:immich_mobile/domain/services/asset_sync.service.dart';
|
import 'package:immich_mobile/domain/services/asset_sync.service.dart';
|
||||||
import 'package:immich_mobile/presentation/states/gallery_permission.state.dart';
|
import 'package:immich_mobile/presentation/states/gallery_permission.state.dart';
|
||||||
import 'package:immich_mobile/presentation/states/server_feature_config.state.dart';
|
import 'package:immich_mobile/presentation/states/server_info.state.dart';
|
||||||
import 'package:immich_mobile/service_locator.dart';
|
import 'package:immich_mobile/service_locator.dart';
|
||||||
import 'package:immich_mobile/utils/immich_api_client.dart';
|
import 'package:immich_mobile/utils/immich_api_client.dart';
|
||||||
import 'package:immich_mobile/utils/mixins/log.mixin.dart';
|
import 'package:immich_mobile/utils/mixins/log.mixin.dart';
|
||||||
@ -115,7 +115,7 @@ class LoginService with LogMixin {
|
|||||||
ServiceLocator.registerPostGlobalStates();
|
ServiceLocator.registerPostGlobalStates();
|
||||||
|
|
||||||
// Fetch server features
|
// Fetch server features
|
||||||
await di<ServerFeatureConfigProvider>().getFeatures();
|
await di<ServerInfoProvider>().fetchFeatures();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<User?> handlePostLogin() async {
|
Future<User?> handlePostLogin() async {
|
||||||
@ -132,6 +132,7 @@ class LoginService with LogMixin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ServiceLocator.registerCurrentUser(user);
|
ServiceLocator.registerCurrentUser(user);
|
||||||
|
await di<ServerInfoProvider>().fetchServerDisk();
|
||||||
|
|
||||||
// sync assets in background
|
// sync assets in background
|
||||||
unawaited(di<AssetSyncService>().performFullRemoteSyncIsolate(user));
|
unawaited(di<AssetSyncService>().performFullRemoteSyncIsolate(user));
|
||||||
|
@ -59,20 +59,41 @@ class _AppThemeBuilder extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
// Static colors
|
// Static colors
|
||||||
if (theme != AppTheme.dynamic) {
|
if (theme != AppTheme.dynamic) {
|
||||||
final lightTheme = AppTheme.generateThemeData(theme.lightSchema);
|
return builder(
|
||||||
final darkTheme = AppTheme.generateThemeData(theme.darkSchema);
|
context,
|
||||||
|
theme.generateThemeData(),
|
||||||
return builder(context, lightTheme, darkTheme);
|
theme.generateThemeData(brightness: Brightness.dark),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dynamic color builder
|
// Dynamic color builder
|
||||||
return DynamicColorBuilder(builder: (lightDynamic, darkDynamic) {
|
return DynamicColorBuilder(builder: (lightDynamic, darkDynamic) {
|
||||||
final lightTheme =
|
if (lightDynamic == null || darkDynamic == null) {
|
||||||
AppTheme.generateThemeData(lightDynamic ?? theme.lightSchema);
|
final defaultTheme = AppTheme.blue;
|
||||||
final darkTheme =
|
return builder(
|
||||||
AppTheme.generateThemeData(darkDynamic ?? theme.darkSchema);
|
context,
|
||||||
|
defaultTheme.generateThemeData(),
|
||||||
|
defaultTheme.generateThemeData(brightness: Brightness.dark),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return builder(context, lightTheme, darkTheme);
|
final primaryLight = lightDynamic.primary;
|
||||||
|
final primaryDark = darkDynamic.primary;
|
||||||
|
|
||||||
|
final lightColor = ColorScheme.fromSeed(seedColor: primaryLight);
|
||||||
|
final darkColor = ColorScheme.fromSeed(
|
||||||
|
seedColor: primaryDark,
|
||||||
|
brightness: Brightness.dark,
|
||||||
|
);
|
||||||
|
|
||||||
|
return builder(
|
||||||
|
context,
|
||||||
|
AppTheme.generateThemeDataForColorScheme(lightColor),
|
||||||
|
AppTheme.generateThemeDataForColorScheme(
|
||||||
|
darkColor,
|
||||||
|
brightness: Brightness.dark,
|
||||||
|
),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,7 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:immich_mobile/presentation/components/appbar/app_bar_dialog.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/components/common/gap.widget.dart';
|
import 'package:immich_mobile/presentation/components/common/gap.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/components/common/user_avatar.widget.dart';
|
import 'package:immich_mobile/presentation/components/common/user_avatar.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/components/image/immich_logo.widget.dart';
|
import 'package:immich_mobile/presentation/components/image/immich_logo.widget.dart';
|
||||||
@ -13,6 +16,11 @@ class ImAppBar extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
@override
|
@override
|
||||||
Size get preferredSize => const Size.fromHeight(kToolbarHeight);
|
Size get preferredSize => const Size.fromHeight(kToolbarHeight);
|
||||||
|
|
||||||
|
static void showAppBarDialog(BuildContext context) => unawaited(showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (_) => const ImAppBarDialog(),
|
||||||
|
));
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return AppBar(
|
return AppBar(
|
||||||
@ -20,7 +28,7 @@ class ImAppBar extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
title: Row(
|
title: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
ImLogo(dimension: SizeConstants.xm),
|
ImLogo(dimension: SizeConstants.xxm),
|
||||||
SizedGap.sw(),
|
SizedGap.sw(),
|
||||||
ImLogoText(fontSize: 20),
|
ImLogoText(fontSize: 20),
|
||||||
],
|
],
|
||||||
@ -28,11 +36,14 @@ class ImAppBar extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
actions: [
|
actions: [
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(right: SizeConstants.m),
|
padding: const EdgeInsets.only(right: SizeConstants.m),
|
||||||
|
child: InkWell(
|
||||||
|
onTap: () => showAppBarDialog(context),
|
||||||
child: ImUserAvatar(
|
child: ImUserAvatar(
|
||||||
user: di<CurrentUserProvider>().value,
|
user: di<CurrentUserProvider>().value,
|
||||||
radius: SizeConstants.m,
|
radius: SizeConstants.m,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
backgroundColor: context.theme.appBarTheme.backgroundColor,
|
backgroundColor: context.theme.appBarTheme.backgroundColor,
|
||||||
centerTitle: false,
|
centerTitle: false,
|
@ -0,0 +1,189 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:immich_mobile/domain/services/login.service.dart';
|
||||||
|
import 'package:immich_mobile/i18n/strings.g.dart';
|
||||||
|
import 'package:immich_mobile/presentation/components/common/user_avatar.widget.dart';
|
||||||
|
import 'package:immich_mobile/presentation/components/image/immich_logo.widget.dart';
|
||||||
|
import 'package:immich_mobile/presentation/router/router.dart';
|
||||||
|
import 'package:immich_mobile/presentation/states/app_info.state.dart';
|
||||||
|
import 'package:immich_mobile/presentation/states/current_user.state.dart';
|
||||||
|
import 'package:immich_mobile/presentation/states/server_info.state.dart';
|
||||||
|
import 'package:immich_mobile/presentation/theme/app_typography.dart';
|
||||||
|
import 'package:immich_mobile/service_locator.dart';
|
||||||
|
import 'package:immich_mobile/utils/constants/size_constants.dart';
|
||||||
|
import 'package:immich_mobile/utils/extensions/build_context.extension.dart';
|
||||||
|
import 'package:immich_mobile/utils/extensions/color.extension.dart';
|
||||||
|
import 'package:immich_mobile/utils/extensions/number.extension.dart';
|
||||||
|
import 'package:immich_mobile/utils/immich_api_client.dart';
|
||||||
|
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
||||||
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
|
part 'app_bar_dialog_actions.widget.dart';
|
||||||
|
part 'app_bar_dialog_server.widget.dart';
|
||||||
|
part 'app_bar_dialog_storage.widget.dart';
|
||||||
|
part 'app_bar_dialog_version.widget.dart';
|
||||||
|
|
||||||
|
class ImAppBarDialog extends StatelessWidget {
|
||||||
|
const ImAppBarDialog({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Dialog(
|
||||||
|
insetPadding: EdgeInsets.only(
|
||||||
|
left: context.isTablet ? 100 : 15,
|
||||||
|
top: context.isTablet ? 15 : 0,
|
||||||
|
right: context.isTablet ? 100 : 15,
|
||||||
|
bottom: context.isTablet ? 15 : 100,
|
||||||
|
),
|
||||||
|
shape: const RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(SizeConstants.xm)),
|
||||||
|
),
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: const Padding(
|
||||||
|
padding: EdgeInsets.all(SizeConstants.xs),
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.only(bottom: SizeConstants.xm),
|
||||||
|
child: _DialogTitleSection(),
|
||||||
|
),
|
||||||
|
_DialogProfileSection(),
|
||||||
|
_DialogStorageSection(),
|
||||||
|
_DialogServerSection(),
|
||||||
|
_DialogVersionMessage(),
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.only(top: 3),
|
||||||
|
child: _DialogActionLogs(),
|
||||||
|
),
|
||||||
|
_DialogActionSettings(),
|
||||||
|
_DialogActionSignOut(),
|
||||||
|
_DialogFooter(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DialogTitleSection extends StatelessWidget {
|
||||||
|
const _DialogTitleSection();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Padding(
|
||||||
|
padding: EdgeInsets.only(left: SizeConstants.xs, top: SizeConstants.xs),
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.topLeft,
|
||||||
|
child: InkWell(
|
||||||
|
onTap: () => unawaited(context.maybePop()),
|
||||||
|
child: Icon(Symbols.close_rounded, size: SizeConstants.xm),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Center(child: ImLogoText(fontSize: SizeConstants.m)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DialogHighlightedSection extends StatelessWidget {
|
||||||
|
final BorderRadiusGeometry? borderRadius;
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
const _DialogHighlightedSection({this.borderRadius, required this.child});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
// ignore: avoid-wrapping-in-padding
|
||||||
|
return Padding(
|
||||||
|
padding: EdgeInsets.only(bottom: 3),
|
||||||
|
child: Container(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: SizeConstants.s),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: context.colorScheme.surface,
|
||||||
|
borderRadius: borderRadius,
|
||||||
|
),
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DialogProfileSection extends StatelessWidget {
|
||||||
|
const _DialogProfileSection();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final user = di<CurrentUserProvider>().value;
|
||||||
|
|
||||||
|
return _DialogHighlightedSection(
|
||||||
|
borderRadius: const BorderRadius.only(
|
||||||
|
topLeft: Radius.circular(SizeConstants.xxs),
|
||||||
|
topRight: Radius.circular(SizeConstants.xxs),
|
||||||
|
),
|
||||||
|
child: ListTile(
|
||||||
|
leading: ImUserAvatar(user: user),
|
||||||
|
title: Text(
|
||||||
|
user.name,
|
||||||
|
style: AppTypography.titleMedium.copyWith(
|
||||||
|
color: context.colorScheme.primary,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
subtitle: Text(
|
||||||
|
user.email,
|
||||||
|
style: AppTypography.titleMedium.copyWith(
|
||||||
|
color: context.colorScheme.onSurface.darken(
|
||||||
|
amount: RatioConstants.oneThird,
|
||||||
|
),
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
minLeadingWidth: SizeConstants.xl,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DialogFooter extends StatelessWidget {
|
||||||
|
const _DialogFooter();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: SizeConstants.s),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
InkWell(
|
||||||
|
onTap: () => unawaited(launchUrl(
|
||||||
|
Uri.parse('https://immich.app'),
|
||||||
|
mode: LaunchMode.externalApplication,
|
||||||
|
)),
|
||||||
|
child:
|
||||||
|
Text(context.t.common.components.appbar.footer_documentation),
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
width: 20,
|
||||||
|
child: Text("•", textAlign: TextAlign.center),
|
||||||
|
),
|
||||||
|
InkWell(
|
||||||
|
onTap: () => unawaited(launchUrl(
|
||||||
|
Uri.parse('https://github.com/immich-app/immich'),
|
||||||
|
mode: LaunchMode.externalApplication,
|
||||||
|
)),
|
||||||
|
child: Text(context.t.common.components.appbar.footer_github),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,73 @@
|
|||||||
|
part of 'app_bar_dialog.widget.dart';
|
||||||
|
|
||||||
|
class _DialogAction extends StatelessWidget {
|
||||||
|
final IconData icon;
|
||||||
|
final String label;
|
||||||
|
final VoidCallback onTap;
|
||||||
|
|
||||||
|
const _DialogAction({
|
||||||
|
required this.icon,
|
||||||
|
required this.label,
|
||||||
|
required this.onTap,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ListTile(
|
||||||
|
leading: Padding(
|
||||||
|
padding: EdgeInsets.only(left: 4),
|
||||||
|
child: Icon(icon, size: 22),
|
||||||
|
),
|
||||||
|
title: Text(label),
|
||||||
|
dense: true,
|
||||||
|
visualDensity: VisualDensity.standard,
|
||||||
|
contentPadding: const EdgeInsets.only(left: 30),
|
||||||
|
onTap: onTap,
|
||||||
|
minLeadingWidth: 40,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DialogActionLogs extends StatelessWidget {
|
||||||
|
const _DialogActionLogs();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return _DialogAction(
|
||||||
|
icon: Symbols.article_rounded,
|
||||||
|
label: context.t.common.components.appbar.action_logs,
|
||||||
|
onTap: () => unawaited(context.navigateTo(LogsRoute())),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DialogActionSettings extends StatelessWidget {
|
||||||
|
const _DialogActionSettings();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return _DialogAction(
|
||||||
|
icon: Symbols.settings_rounded,
|
||||||
|
label: context.t.common.components.appbar.action_settings,
|
||||||
|
onTap: () => unawaited(context.navigateTo(SettingsRoute())),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DialogActionSignOut extends StatelessWidget {
|
||||||
|
const _DialogActionSignOut();
|
||||||
|
|
||||||
|
Future<void> _onLogout() async {
|
||||||
|
await di<LoginService>().logout();
|
||||||
|
await di<AppRouter>().replaceAll([const LoginRoute()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return _DialogAction(
|
||||||
|
icon: Symbols.logout_rounded,
|
||||||
|
label: context.t.common.components.appbar.action_signout,
|
||||||
|
onTap: () => unawaited(_onLogout()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,123 @@
|
|||||||
|
part of 'app_bar_dialog.widget.dart';
|
||||||
|
|
||||||
|
class _DialogServerSection extends StatelessWidget {
|
||||||
|
const _DialogServerSection();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return const _DialogHighlightedSection(
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.symmetric(
|
||||||
|
vertical: SizeConstants.xxs,
|
||||||
|
horizontal: SizeConstants.s,
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
_DialogServerAppVersion(),
|
||||||
|
_DialogServerEntryDivider(),
|
||||||
|
_DialogServerVersion(),
|
||||||
|
_DialogServerEntryDivider(),
|
||||||
|
_DialogServerUrl(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DialogServerEntryDivider extends StatelessWidget {
|
||||||
|
const _DialogServerEntryDivider();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return const Padding(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: SizeConstants.s),
|
||||||
|
child: Divider(thickness: 1),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DialogServerEntry extends StatelessWidget {
|
||||||
|
final String label;
|
||||||
|
final String value;
|
||||||
|
|
||||||
|
const _DialogServerEntry({required this.label, required this.value});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(left: SizeConstants.xs),
|
||||||
|
child: Text(
|
||||||
|
label,
|
||||||
|
style: TextStyle(fontWeight: FontWeight.w400),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
flex: 0,
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.only(right: SizeConstants.xs),
|
||||||
|
width: context.width * RatioConstants.half,
|
||||||
|
child: Text(
|
||||||
|
value,
|
||||||
|
style: TextStyle(
|
||||||
|
color: context.colorScheme.onSurface.darken(
|
||||||
|
amount: RatioConstants.oneThird,
|
||||||
|
),
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.end,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DialogServerAppVersion extends StatelessWidget {
|
||||||
|
const _DialogServerAppVersion();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final version = di<AppInfoProvider>().value.versionString;
|
||||||
|
|
||||||
|
return _DialogServerEntry(
|
||||||
|
label: context.t.common.components.appbar.app_version_label,
|
||||||
|
value: version,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DialogServerVersion extends StatelessWidget {
|
||||||
|
const _DialogServerVersion();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final version = di<ServerInfoProvider>().value.version;
|
||||||
|
|
||||||
|
return _DialogServerEntry(
|
||||||
|
label: context.t.common.components.appbar.server_version_label,
|
||||||
|
value: "${version.major}.${version.minor}.${version.patch}",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DialogServerUrl extends StatelessWidget {
|
||||||
|
const _DialogServerUrl();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final serverUrl = di<ImApiClient>().basePath.replaceAll("/api", "");
|
||||||
|
|
||||||
|
return _DialogServerEntry(
|
||||||
|
label: context.t.common.components.appbar.server_url_label,
|
||||||
|
value: serverUrl,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,63 @@
|
|||||||
|
part of 'app_bar_dialog.widget.dart';
|
||||||
|
|
||||||
|
class _DialogStorageSection extends StatelessWidget {
|
||||||
|
const _DialogStorageSection();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final user = di<CurrentUserProvider>().value;
|
||||||
|
|
||||||
|
final int availableSizeInBytes;
|
||||||
|
final int usedSizeInBytes;
|
||||||
|
if (user.quotaSizeInBytes > 0) {
|
||||||
|
availableSizeInBytes = user.quotaSizeInBytes;
|
||||||
|
usedSizeInBytes = user.quotaUsageInBytes;
|
||||||
|
} else {
|
||||||
|
final storage = di<ServerInfoProvider>().value.disk;
|
||||||
|
availableSizeInBytes = storage.diskSizeInBytes;
|
||||||
|
usedSizeInBytes = storage.diskUseInBytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
final percentageUsed = usedSizeInBytes / availableSizeInBytes;
|
||||||
|
|
||||||
|
return _DialogHighlightedSection(
|
||||||
|
child: ListTile(
|
||||||
|
leading: Padding(
|
||||||
|
padding: EdgeInsets.only(left: SizeConstants.s),
|
||||||
|
child: Icon(
|
||||||
|
Symbols.hard_drive_rounded,
|
||||||
|
color: context.colorScheme.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
title: Text(
|
||||||
|
context.t.common.components.appbar.server_storage,
|
||||||
|
style: AppTypography.titleMedium.copyWith(
|
||||||
|
fontSize: SizeConstants.xxs,
|
||||||
|
fontWeight: FontWeight.w400,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
subtitle: Padding(
|
||||||
|
padding: EdgeInsets.only(top: SizeConstants.s),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
LinearProgressIndicator(
|
||||||
|
value: percentageUsed,
|
||||||
|
minHeight: 5,
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(10.0)),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.only(top: SizeConstants.s),
|
||||||
|
child: Text(context.t.common.components.appbar.storage_used(
|
||||||
|
used: usedSizeInBytes.formatAsSize(noOfDecimals: 1),
|
||||||
|
total: availableSizeInBytes.formatAsSize(),
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
minLeadingWidth: SizeConstants.xl,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,68 @@
|
|||||||
|
part of 'app_bar_dialog.widget.dart';
|
||||||
|
|
||||||
|
class _DialogVersionMessage extends StatefulWidget {
|
||||||
|
const _DialogVersionMessage();
|
||||||
|
|
||||||
|
@override
|
||||||
|
State createState() => _DialogVersionMessageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DialogVersionMessageState extends State<_DialogVersionMessage>
|
||||||
|
with SingleTickerProviderStateMixin {
|
||||||
|
late final AnimationController _controller = AnimationController(
|
||||||
|
duration: Durations.medium2,
|
||||||
|
vsync: this,
|
||||||
|
);
|
||||||
|
late final Animation<double> _animation = CurvedAnimation(
|
||||||
|
parent: _controller,
|
||||||
|
curve: Curves.fastEaseInToSlowEaseOut,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_controller.forward();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_controller.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final appInfo = di<AppInfoProvider>().value;
|
||||||
|
|
||||||
|
final String message;
|
||||||
|
if (appInfo.isVersionMismatch) {
|
||||||
|
message = context.t[appInfo.versionMismatchError];
|
||||||
|
} else {
|
||||||
|
message = context.t.common.components.appbar.app_version_ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
return SizeTransition(
|
||||||
|
sizeFactor: _animation,
|
||||||
|
axisAlignment: 1.0,
|
||||||
|
child: _DialogHighlightedSection(
|
||||||
|
borderRadius: const BorderRadius.only(
|
||||||
|
bottomLeft: Radius.circular(SizeConstants.xxs),
|
||||||
|
bottomRight: Radius.circular(SizeConstants.xxs),
|
||||||
|
),
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.all(SizeConstants.m),
|
||||||
|
width: double.infinity,
|
||||||
|
child: Text(
|
||||||
|
message,
|
||||||
|
style: TextStyle(
|
||||||
|
color: context.colorScheme.primary,
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -7,13 +7,23 @@ class SizedGap extends SizedBox {
|
|||||||
|
|
||||||
// Widgets to be used in Column
|
// Widgets to be used in Column
|
||||||
const SizedGap.sh({super.key}) : super(height: SizeConstants.s);
|
const SizedGap.sh({super.key}) : super(height: SizeConstants.s);
|
||||||
|
const SizedGap.xsh({super.key}) : super(height: SizeConstants.xs);
|
||||||
|
const SizedGap.xxsh({super.key}) : super(height: SizeConstants.xxs);
|
||||||
const SizedGap.mh({super.key}) : super(height: SizeConstants.m);
|
const SizedGap.mh({super.key}) : super(height: SizeConstants.m);
|
||||||
|
const SizedGap.xmh({super.key}) : super(height: SizeConstants.xm);
|
||||||
|
const SizedGap.xxmh({super.key}) : super(height: SizeConstants.xxm);
|
||||||
const SizedGap.lh({super.key}) : super(height: SizeConstants.l);
|
const SizedGap.lh({super.key}) : super(height: SizeConstants.l);
|
||||||
const SizedGap.xlh({super.key}) : super(height: SizeConstants.xl);
|
const SizedGap.xlh({super.key}) : super(height: SizeConstants.xl);
|
||||||
|
const SizedGap.xxlh({super.key}) : super(height: SizeConstants.xxl);
|
||||||
|
|
||||||
// Widgets to be used in Row
|
// Widgets to be used in Row
|
||||||
const SizedGap.sw({super.key}) : super(width: SizeConstants.s);
|
const SizedGap.sw({super.key}) : super(width: SizeConstants.s);
|
||||||
|
const SizedGap.xsw({super.key}) : super(width: SizeConstants.xs);
|
||||||
|
const SizedGap.xxsw({super.key}) : super(width: SizeConstants.xxs);
|
||||||
const SizedGap.mw({super.key}) : super(width: SizeConstants.m);
|
const SizedGap.mw({super.key}) : super(width: SizeConstants.m);
|
||||||
|
const SizedGap.xmw({super.key}) : super(width: SizeConstants.xm);
|
||||||
|
const SizedGap.xxmw({super.key}) : super(width: SizeConstants.xxm);
|
||||||
const SizedGap.lw({super.key}) : super(width: SizeConstants.l);
|
const SizedGap.lw({super.key}) : super(width: SizeConstants.l);
|
||||||
const SizedGap.xlw({super.key}) : super(width: SizeConstants.xl);
|
const SizedGap.xlw({super.key}) : super(width: SizeConstants.xl);
|
||||||
|
const SizedGap.xxlw({super.key}) : super(width: SizeConstants.xxl);
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,36 @@
|
|||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:immich_mobile/presentation/components/common/gap.widget.dart';
|
||||||
|
import 'package:immich_mobile/utils/constants/size_constants.dart';
|
||||||
|
import 'package:immich_mobile/utils/extensions/build_context.extension.dart';
|
||||||
|
|
||||||
|
class ImPageEmptyIndicator extends StatelessWidget {
|
||||||
|
final IconData icon;
|
||||||
|
final String? message;
|
||||||
|
final Widget? subtitle;
|
||||||
|
|
||||||
|
const ImPageEmptyIndicator({
|
||||||
|
super.key,
|
||||||
|
required this.icon,
|
||||||
|
this.message,
|
||||||
|
this.subtitle,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
icon,
|
||||||
|
size: SizeConstants.xl,
|
||||||
|
color: context.colorScheme.primary,
|
||||||
|
),
|
||||||
|
const SizedGap.mh(),
|
||||||
|
if (message != null) Text(message!),
|
||||||
|
if (subtitle != null) subtitle!,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -45,7 +45,7 @@ class ImUserAvatar extends StatelessWidget {
|
|||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
placeholder: (_, __) => Image.memory(
|
placeholder: (_, __) => Image.memory(
|
||||||
kTransparentImage,
|
kTransparentImage,
|
||||||
semanticLabel: 'Transparent',
|
semanticLabel: 'Transparent Image',
|
||||||
),
|
),
|
||||||
fadeInDuration: const Duration(milliseconds: 300),
|
fadeInDuration: const Duration(milliseconds: 300),
|
||||||
errorWidget: (_, error, stackTrace) => SizedBox.square(),
|
errorWidget: (_, error, stackTrace) => SizedBox.square(),
|
||||||
|
@ -3,17 +3,18 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:flutter_list_view/flutter_list_view.dart';
|
import 'package:flutter_list_view/flutter_list_view.dart';
|
||||||
import 'package:immich_mobile/domain/models/render_list_element.model.dart';
|
import 'package:immich_mobile/domain/models/render_list_element.model.dart';
|
||||||
|
import 'package:immich_mobile/i18n/strings.g.dart';
|
||||||
|
import 'package:immich_mobile/presentation/components/common/page_empty.widget.dart';
|
||||||
|
import 'package:immich_mobile/presentation/components/grid/asset_grid.state.dart';
|
||||||
|
import 'package:immich_mobile/presentation/components/grid/asset_render_grid.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/components/grid/draggable_scrollbar.dart';
|
import 'package:immich_mobile/presentation/components/grid/draggable_scrollbar.dart';
|
||||||
import 'package:immich_mobile/presentation/components/grid/immich_asset_grid.state.dart';
|
import 'package:immich_mobile/utils/constants/size_constants.dart';
|
||||||
import 'package:immich_mobile/presentation/components/image/immich_image.widget.dart';
|
|
||||||
import 'package:immich_mobile/presentation/components/image/immich_thumbnail.widget.dart';
|
|
||||||
import 'package:immich_mobile/utils/extensions/async_snapshot.extension.dart';
|
|
||||||
import 'package:immich_mobile/utils/extensions/build_context.extension.dart';
|
import 'package:immich_mobile/utils/extensions/build_context.extension.dart';
|
||||||
|
import 'package:immich_mobile/utils/extensions/color.extension.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:material_symbols_icons/symbols.dart';
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
|
|
||||||
part 'immich_asset_grid_header.widget.dart';
|
part 'asset_grid_header.widget.dart';
|
||||||
part 'immich_asset_render_grid.widget.dart';
|
|
||||||
|
|
||||||
class ImAssetGrid extends StatefulWidget {
|
class ImAssetGrid extends StatefulWidget {
|
||||||
/// The padding for the grid
|
/// The padding for the grid
|
||||||
@ -66,6 +67,10 @@ class _ImAssetGridState extends State<ImAssetGrid> {
|
|||||||
builder: (_, state) {
|
builder: (_, state) {
|
||||||
final elements = state.renderList.elements;
|
final elements = state.renderList.elements;
|
||||||
|
|
||||||
|
if (state.renderList.totalCount == 0) {
|
||||||
|
return const _ImGridEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
// Append padding if required
|
// Append padding if required
|
||||||
if (widget.topPadding != null &&
|
if (widget.topPadding != null &&
|
||||||
elements.firstOrNull is! RenderListPaddingElement) {
|
elements.firstOrNull is! RenderListPaddingElement) {
|
||||||
@ -94,7 +99,7 @@ class _ImAssetGridState extends State<ImAssetGrid> {
|
|||||||
RenderListMonthHeaderElement() =>
|
RenderListMonthHeaderElement() =>
|
||||||
_MonthHeader(text: section.header),
|
_MonthHeader(text: section.header),
|
||||||
RenderListDayHeaderElement() => Text(section.header),
|
RenderListDayHeaderElement() => Text(section.header),
|
||||||
RenderListAssetElement() => _StaticGrid(
|
RenderListAssetElement() => ImStaticGrid(
|
||||||
section: section,
|
section: section,
|
||||||
isDragging: state.isDragScrolling,
|
isDragging: state.isDragScrolling,
|
||||||
),
|
),
|
||||||
@ -137,3 +142,21 @@ class _ImAssetGridState extends State<ImAssetGrid> {
|
|||||||
.isAtSameMomentAs(current.renderList.modifiedTime),
|
.isAtSameMomentAs(current.renderList.modifiedTime),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _ImGridEmpty extends StatelessWidget {
|
||||||
|
const _ImGridEmpty();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ImPageEmptyIndicator(
|
||||||
|
icon: Symbols.photo_camera_rounded,
|
||||||
|
subtitle: SizedBox(
|
||||||
|
width: context.width * RatioConstants.twoThird,
|
||||||
|
child: Text(
|
||||||
|
context.t.common.components.grid_empty_message,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
part of 'immich_asset_grid.widget.dart';
|
part of 'asset_grid.widget.dart';
|
||||||
|
|
||||||
class _HeaderText extends StatelessWidget {
|
class _HeaderText extends StatelessWidget {
|
||||||
final String text;
|
final String text;
|
||||||
@ -22,7 +22,8 @@ class _HeaderText extends StatelessWidget {
|
|||||||
const Spacer(),
|
const Spacer(),
|
||||||
Icon(
|
Icon(
|
||||||
Symbols.check_circle_rounded,
|
Symbols.check_circle_rounded,
|
||||||
color: context.colorScheme.onSurface,
|
color: context.colorScheme.onSurface
|
||||||
|
.darken(amount: RatioConstants.oneThird),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
@ -1,10 +1,20 @@
|
|||||||
part of 'immich_asset_grid.widget.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/render_list_element.model.dart';
|
||||||
|
import 'package:immich_mobile/presentation/components/grid/asset_grid.state.dart';
|
||||||
|
import 'package:immich_mobile/presentation/components/image/immich_image.widget.dart';
|
||||||
|
import 'package:immich_mobile/presentation/components/image/immich_thumbnail.widget.dart';
|
||||||
|
import 'package:immich_mobile/utils/extensions/async_snapshot.extension.dart';
|
||||||
|
|
||||||
class _StaticGrid extends StatelessWidget {
|
class ImStaticGrid extends StatelessWidget {
|
||||||
final RenderListAssetElement section;
|
final RenderListAssetElement section;
|
||||||
final bool isDragging;
|
final bool isDragging;
|
||||||
|
|
||||||
const _StaticGrid({required this.section, required this.isDragging});
|
const ImStaticGrid({
|
||||||
|
super.key,
|
||||||
|
required this.section,
|
||||||
|
required this.isDragging,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
@ -1,6 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:immich_mobile/domain/models/asset.model.dart';
|
import 'package:immich_mobile/domain/models/asset.model.dart';
|
||||||
import 'package:immich_mobile/presentation/components/image/immich_image.widget.dart';
|
import 'package:immich_mobile/presentation/components/image/immich_image.widget.dart';
|
||||||
|
import 'package:immich_mobile/utils/constants/size_constants.dart';
|
||||||
import 'package:material_symbols_icons/symbols.dart';
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
|
|
||||||
IconData _getStorageIcon(Asset asset) {
|
IconData _getStorageIcon(Asset asset) {
|
||||||
@ -77,7 +78,7 @@ class _PadAlignedIcon extends StatelessWidget {
|
|||||||
alignment: alignment,
|
alignment: alignment,
|
||||||
child: Icon(
|
child: Icon(
|
||||||
icon,
|
icon,
|
||||||
size: 20,
|
size: SizeConstants.xm,
|
||||||
fill: (filled != null && filled!) ? 1 : null,
|
fill: (filled != null && filled!) ? 1 : null,
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
),
|
),
|
||||||
|
@ -2,9 +2,9 @@ import 'package:auto_route/auto_route.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:immich_mobile/domain/utils/renderlist_providers.dart';
|
import 'package:immich_mobile/domain/utils/renderlist_providers.dart';
|
||||||
import 'package:immich_mobile/presentation/components/appbar/immich_app_bar.widget.dart';
|
import 'package:immich_mobile/presentation/components/appbar/app_bar.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/components/grid/immich_asset_grid.state.dart';
|
import 'package:immich_mobile/presentation/components/grid/asset_grid.state.dart';
|
||||||
import 'package:immich_mobile/presentation/components/grid/immich_asset_grid.widget.dart';
|
import 'package:immich_mobile/presentation/components/grid/asset_grid.widget.dart';
|
||||||
import 'package:immich_mobile/utils/extensions/build_context.extension.dart';
|
import 'package:immich_mobile/utils/extensions/build_context.extension.dart';
|
||||||
|
|
||||||
@RoutePage()
|
@RoutePage()
|
||||||
|
@ -10,10 +10,11 @@ import 'package:immich_mobile/presentation/modules/login/models/login_page.model
|
|||||||
import 'package:immich_mobile/presentation/modules/login/states/login_page.state.dart';
|
import 'package:immich_mobile/presentation/modules/login/states/login_page.state.dart';
|
||||||
import 'package:immich_mobile/presentation/modules/login/widgets/login_form.widget.dart';
|
import 'package:immich_mobile/presentation/modules/login/widgets/login_form.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/router/router.dart';
|
import 'package:immich_mobile/presentation/router/router.dart';
|
||||||
|
import 'package:immich_mobile/presentation/states/app_info.state.dart';
|
||||||
|
import 'package:immich_mobile/service_locator.dart';
|
||||||
import 'package:immich_mobile/utils/constants/size_constants.dart';
|
import 'package:immich_mobile/utils/constants/size_constants.dart';
|
||||||
import 'package:immich_mobile/utils/extensions/build_context.extension.dart';
|
import 'package:immich_mobile/utils/extensions/build_context.extension.dart';
|
||||||
import 'package:material_symbols_icons/symbols.dart';
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
import 'package:package_info_plus/package_info_plus.dart';
|
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
@RoutePage()
|
@RoutePage()
|
||||||
@ -93,17 +94,16 @@ class _LoginPageState extends State<LoginPage>
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
final version = di<AppInfoProvider>().value.versionString;
|
||||||
|
|
||||||
final Widget bottom = Padding(
|
final Widget bottom = Padding(
|
||||||
padding: const EdgeInsets.only(bottom: SizeConstants.s),
|
padding: const EdgeInsets.only(bottom: SizeConstants.s),
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
FutureBuilder(
|
DefaultTextStyle.merge(
|
||||||
future: PackageInfo.fromPlatform(),
|
|
||||||
builder: (_, snap) => DefaultTextStyle.merge(
|
|
||||||
style: TextStyle(color: context.theme.colorScheme.outline),
|
style: TextStyle(color: context.theme.colorScheme.outline),
|
||||||
child: Text(snap.data?.version ?? ''),
|
child: Text(version),
|
||||||
),
|
|
||||||
),
|
),
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => unawaited(context.navigateRoot(const LogsRoute())),
|
onPressed: () => unawaited(context.navigateRoot(const LogsRoute())),
|
||||||
|
@ -11,7 +11,7 @@ import 'package:immich_mobile/presentation/components/input/text_button.widget.d
|
|||||||
import 'package:immich_mobile/presentation/components/input/text_form_field.widget.dart';
|
import 'package:immich_mobile/presentation/components/input/text_form_field.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/modules/login/models/login_page.model.dart';
|
import 'package:immich_mobile/presentation/modules/login/models/login_page.model.dart';
|
||||||
import 'package:immich_mobile/presentation/modules/login/states/login_page.state.dart';
|
import 'package:immich_mobile/presentation/modules/login/states/login_page.state.dart';
|
||||||
import 'package:immich_mobile/presentation/states/server_feature_config.state.dart';
|
import 'package:immich_mobile/presentation/states/server_info.state.dart';
|
||||||
import 'package:immich_mobile/service_locator.dart';
|
import 'package:immich_mobile/service_locator.dart';
|
||||||
import 'package:material_symbols_icons/symbols.dart';
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
|
|
||||||
@ -132,7 +132,7 @@ class _CredentialsFormState extends State<_CredentialsForm> {
|
|||||||
builder: (_, isValidationInProgress) => isValidationInProgress
|
builder: (_, isValidationInProgress) => isValidationInProgress
|
||||||
? const ImLoadingIndicator()
|
? const ImLoadingIndicator()
|
||||||
: ValueListenableBuilder(
|
: ValueListenableBuilder(
|
||||||
valueListenable: di<ServerFeatureConfigProvider>(),
|
valueListenable: di<ServerInfoProvider>(),
|
||||||
builder: (_, state, __) => Column(
|
builder: (_, state, __) => Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
@ -6,7 +6,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:immich_mobile/domain/interfaces/log.interface.dart';
|
import 'package:immich_mobile/domain/interfaces/log.interface.dart';
|
||||||
import 'package:immich_mobile/domain/models/log.model.dart';
|
import 'package:immich_mobile/domain/models/log.model.dart';
|
||||||
import 'package:immich_mobile/i18n/strings.g.dart';
|
import 'package:immich_mobile/i18n/strings.g.dart';
|
||||||
import 'package:immich_mobile/presentation/components/common/gap.widget.dart';
|
import 'package:immich_mobile/presentation/components/common/page_empty.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/components/common/skeletonized_future_builder.widget.dart';
|
import 'package:immich_mobile/presentation/components/common/skeletonized_future_builder.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/components/scaffold/adaptive_route_appbar.widget.dart';
|
import 'package:immich_mobile/presentation/components/scaffold/adaptive_route_appbar.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/components/scaffold/adaptive_route_wrapper.widget.dart';
|
import 'package:immich_mobile/presentation/components/scaffold/adaptive_route_wrapper.widget.dart';
|
||||||
@ -183,19 +183,9 @@ class _LogListEmpty extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Center(
|
return ImPageEmptyIndicator(
|
||||||
child: Column(
|
icon: Symbols.comments_disabled_rounded,
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
message: context.t.logs.no_logs_message,
|
||||||
children: [
|
|
||||||
Icon(
|
|
||||||
Symbols.comments_disabled_rounded,
|
|
||||||
size: 50,
|
|
||||||
color: context.colorScheme.primary,
|
|
||||||
),
|
|
||||||
const SizedGap.mh(),
|
|
||||||
Text(context.t.logs.no_logs),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,10 @@ class AboutSettingsPage extends StatelessWidget {
|
|||||||
onTap: () => showLicensePage(
|
onTap: () => showLicensePage(
|
||||||
context: context,
|
context: context,
|
||||||
applicationName: context.t.immich,
|
applicationName: context.t.immich,
|
||||||
applicationIcon: const ImLogo(dimension: SizeConstants.xl),
|
applicationIcon: const Padding(
|
||||||
|
padding: EdgeInsets.only(top: SizeConstants.s),
|
||||||
|
child: ImLogo(dimension: SizeConstants.xl),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -2,6 +2,7 @@ import 'package:auto_route/auto_route.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_adaptive_scaffold/flutter_adaptive_scaffold.dart';
|
import 'package:flutter_adaptive_scaffold/flutter_adaptive_scaffold.dart';
|
||||||
import 'package:immich_mobile/i18n/strings.g.dart';
|
import 'package:immich_mobile/i18n/strings.g.dart';
|
||||||
|
import 'package:immich_mobile/presentation/components/appbar/app_bar.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/components/common/immich_navigation_rail.widget.dart';
|
import 'package:immich_mobile/presentation/components/common/immich_navigation_rail.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/components/common/user_avatar.widget.dart';
|
import 'package:immich_mobile/presentation/components/common/user_avatar.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/components/image/immich_logo.widget.dart';
|
import 'package:immich_mobile/presentation/components/image/immich_logo.widget.dart';
|
||||||
@ -97,12 +98,15 @@ class _TabControllerAdaptiveScaffold extends StatelessWidget {
|
|||||||
.toList(),
|
.toList(),
|
||||||
selectedIndex: selectedIndex,
|
selectedIndex: selectedIndex,
|
||||||
backgroundColor: navRailTheme.backgroundColor,
|
backgroundColor: navRailTheme.backgroundColor,
|
||||||
leading: ImUserAvatar(
|
leading: InkWell(
|
||||||
|
onTap: () => ImAppBar.showAppBarDialog(context),
|
||||||
|
child: ImUserAvatar(
|
||||||
user: di<CurrentUserProvider>().value,
|
user: di<CurrentUserProvider>().value,
|
||||||
dimension: SizeConstants.m,
|
dimension: SizeConstants.m,
|
||||||
radius: SizeConstants.m,
|
radius: SizeConstants.m,
|
||||||
),
|
),
|
||||||
trailing: ImLogo(dimension: SizeConstants.xm),
|
),
|
||||||
|
trailing: ImLogo(dimension: SizeConstants.xxm),
|
||||||
onDestinationSelected: onSelectedIndexChange,
|
onDestinationSelected: onSelectedIndexChange,
|
||||||
selectedIconTheme: navRailTheme.selectedIconTheme,
|
selectedIconTheme: navRailTheme.selectedIconTheme,
|
||||||
unselectedIconTheme: navRailTheme.unselectedIconTheme,
|
unselectedIconTheme: navRailTheme.unselectedIconTheme,
|
||||||
|
@ -32,11 +32,11 @@ class AppRouter extends RootStackRouter {
|
|||||||
List<AutoRoute> get routes => [
|
List<AutoRoute> get routes => [
|
||||||
AutoRoute(
|
AutoRoute(
|
||||||
page: SplashScreenWrapperRoute.page,
|
page: SplashScreenWrapperRoute.page,
|
||||||
|
initial: true,
|
||||||
children: [
|
children: [
|
||||||
AutoRoute(page: SplashScreenRoute.page, initial: true),
|
AutoRoute(page: SplashScreenRoute.page, initial: true),
|
||||||
AutoRoute(page: LoginRoute.page),
|
AutoRoute(page: LoginRoute.page),
|
||||||
],
|
],
|
||||||
initial: true,
|
|
||||||
),
|
),
|
||||||
AutoRoute(page: LogsWrapperRoute.page, children: [
|
AutoRoute(page: LogsWrapperRoute.page, children: [
|
||||||
AutoRoute(page: LogsRoute.page),
|
AutoRoute(page: LogsRoute.page),
|
||||||
|
112
mobile-v2/lib/presentation/states/app_info.state.dart
Normal file
112
mobile-v2/lib/presentation/states/app_info.state.dart
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/server-info/server_version.model.dart';
|
||||||
|
import 'package:immich_mobile/utils/extensions/string.extension.dart';
|
||||||
|
import 'package:package_info_plus/package_info_plus.dart';
|
||||||
|
|
||||||
|
class AppInfoProvider extends ValueNotifier<AppInfo> {
|
||||||
|
AppInfoProvider() : super(const AppInfo.initial()) {
|
||||||
|
unawaited(_getAppVersion());
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _getAppVersion() async {
|
||||||
|
final version = (await PackageInfo.fromPlatform()).version;
|
||||||
|
|
||||||
|
final segments = version.split(".");
|
||||||
|
|
||||||
|
final major = segments.firstOrNull ?? '1';
|
||||||
|
final minor = segments.elementAtOrNull(1) ?? '0';
|
||||||
|
final patch = segments.elementAtOrNull(2)?.replaceAll("-DEBUG", "") ?? '0';
|
||||||
|
|
||||||
|
value = value.copyWith(
|
||||||
|
versionString: version,
|
||||||
|
version: ServerVersion(
|
||||||
|
major: major.parseInt(),
|
||||||
|
minor: minor.parseInt(),
|
||||||
|
patch: patch.parseInt(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void checkVersionMismatch(ServerVersion? serverVersion) {
|
||||||
|
if (serverVersion == null) {
|
||||||
|
value = value.copyWith(
|
||||||
|
isVersionMismatch: true,
|
||||||
|
versionMismatchError:
|
||||||
|
"common.components.appbar.server_version_common_error",
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String? errorMessage;
|
||||||
|
if (value.version.major != serverVersion.major) {
|
||||||
|
errorMessage = value.version.major > serverVersion.major
|
||||||
|
? "common.components.appbar.server_version_major_error"
|
||||||
|
: "common.components.appbar.app_version_major_error";
|
||||||
|
} else if (value.version.minor != serverVersion.minor) {
|
||||||
|
errorMessage = value.version.minor > serverVersion.minor
|
||||||
|
? "common.components.appbar.server_version_minor_error"
|
||||||
|
: "common.components.appbar.app_version_minor_error";
|
||||||
|
}
|
||||||
|
|
||||||
|
value = value.copyWith(
|
||||||
|
isVersionMismatch: errorMessage != null,
|
||||||
|
versionMismatchError: errorMessage ??
|
||||||
|
"common.components.appbar.server_version_common_error",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class AppInfo {
|
||||||
|
final String versionString;
|
||||||
|
final ServerVersion version;
|
||||||
|
final bool isVersionMismatch;
|
||||||
|
final String versionMismatchError;
|
||||||
|
|
||||||
|
const AppInfo({
|
||||||
|
required this.versionString,
|
||||||
|
required this.version,
|
||||||
|
required this.isVersionMismatch,
|
||||||
|
required this.versionMismatchError,
|
||||||
|
});
|
||||||
|
|
||||||
|
const AppInfo.initial()
|
||||||
|
: versionString = '1.0.0',
|
||||||
|
version = const ServerVersion.initial(),
|
||||||
|
isVersionMismatch = false,
|
||||||
|
versionMismatchError = '';
|
||||||
|
|
||||||
|
AppInfo copyWith({
|
||||||
|
String? versionString,
|
||||||
|
ServerVersion? version,
|
||||||
|
bool? isVersionMismatch,
|
||||||
|
String? versionMismatchError,
|
||||||
|
}) {
|
||||||
|
return AppInfo(
|
||||||
|
versionString: versionString ?? this.versionString,
|
||||||
|
version: version ?? this.version,
|
||||||
|
isVersionMismatch: isVersionMismatch ?? this.isVersionMismatch,
|
||||||
|
versionMismatchError: versionMismatchError ?? this.versionMismatchError,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() =>
|
||||||
|
'AppInfo(versionString: $versionString, isVersionMismatch: $isVersionMismatch, versionMismatchError: $versionMismatchError)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(covariant AppInfo other) {
|
||||||
|
if (identical(this, other)) return true;
|
||||||
|
|
||||||
|
return other.versionString == versionString &&
|
||||||
|
other.isVersionMismatch == isVersionMismatch &&
|
||||||
|
other.versionMismatchError == versionMismatchError;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode =>
|
||||||
|
versionString.hashCode ^
|
||||||
|
isVersionMismatch.hashCode ^
|
||||||
|
versionMismatchError.hashCode;
|
||||||
|
}
|
@ -1,28 +0,0 @@
|
|||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:immich_mobile/domain/interfaces/api/server_api.interface.dart';
|
|
||||||
import 'package:immich_mobile/domain/models/server-info/server_feature_config.model.dart';
|
|
||||||
|
|
||||||
class ServerFeatureConfigProvider extends ValueNotifier<ServerFeatureConfig> {
|
|
||||||
final IServerApiRepository _serverApiRepository;
|
|
||||||
|
|
||||||
ServerFeatureConfigProvider({required IServerApiRepository serverApiRepo})
|
|
||||||
: _serverApiRepository = serverApiRepo,
|
|
||||||
super(const ServerFeatureConfig.initial());
|
|
||||||
|
|
||||||
Future<void> getFeatures() async =>
|
|
||||||
await Future.wait([_getFeatures(), _getConfig()]);
|
|
||||||
|
|
||||||
Future<void> _getFeatures() async {
|
|
||||||
final features = await _serverApiRepository.getServerFeatures();
|
|
||||||
if (features != null) {
|
|
||||||
value = value.copyWith(features: features);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _getConfig() async {
|
|
||||||
final config = await _serverApiRepository.getServerConfig();
|
|
||||||
if (config != null) {
|
|
||||||
value = value.copyWith(config: config);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
45
mobile-v2/lib/presentation/states/server_info.state.dart
Normal file
45
mobile-v2/lib/presentation/states/server_info.state.dart
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:immich_mobile/domain/interfaces/api/server_api.interface.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/server-info/server_info.model.dart';
|
||||||
|
import 'package:immich_mobile/presentation/states/app_info.state.dart';
|
||||||
|
import 'package:immich_mobile/service_locator.dart';
|
||||||
|
|
||||||
|
class ServerInfoProvider extends ValueNotifier<ServerInfo> {
|
||||||
|
final IServerApiRepository _serverApiRepository;
|
||||||
|
|
||||||
|
ServerInfoProvider({required IServerApiRepository serverApiRepo})
|
||||||
|
: _serverApiRepository = serverApiRepo,
|
||||||
|
super(const ServerInfo.initial());
|
||||||
|
|
||||||
|
Future<void> fetchFeatures() async =>
|
||||||
|
await Future.wait([_getFeatures(), _getConfig(), _getVersion()]);
|
||||||
|
|
||||||
|
Future<void> _getFeatures() async {
|
||||||
|
final features = await _serverApiRepository.getServerFeatures();
|
||||||
|
if (features != null) {
|
||||||
|
value = value.copyWith(features: features);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _getConfig() async {
|
||||||
|
final config = await _serverApiRepository.getServerConfig();
|
||||||
|
if (config != null) {
|
||||||
|
value = value.copyWith(config: config);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _getVersion() async {
|
||||||
|
final version = await _serverApiRepository.getServerVersion();
|
||||||
|
di<AppInfoProvider>().checkVersionMismatch(version);
|
||||||
|
if (version != null) {
|
||||||
|
value = value.copyWith(version: version);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> fetchServerDisk() async {
|
||||||
|
final disk = await _serverApiRepository.getServerDiskInfo();
|
||||||
|
if (disk != null) {
|
||||||
|
value = value.copyWith(disk: disk);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -4,72 +4,12 @@ import 'package:flutter/material.dart';
|
|||||||
abstract final class AppColors {
|
abstract final class AppColors {
|
||||||
const AppColors();
|
const AppColors();
|
||||||
|
|
||||||
/// Blue color
|
static final blueLight = ColorScheme.fromSeed(
|
||||||
static const ColorScheme blueLight = ColorScheme(
|
seedColor: Color(0xff1145a4),
|
||||||
brightness: Brightness.light,
|
brightness: Brightness.light,
|
||||||
primary: Color(0xff1145a4),
|
|
||||||
onPrimary: Color(0xffffffff),
|
|
||||||
primaryContainer: Color(0xffdae2ff),
|
|
||||||
onPrimaryContainer: Color(0xff001848),
|
|
||||||
secondary: Color(0xff4b73d3),
|
|
||||||
onSecondary: Color(0xfffefbff),
|
|
||||||
secondaryContainer: Color(0xffeef0ff),
|
|
||||||
onSecondaryContainer: Color(0xff001848),
|
|
||||||
tertiary: Color(0xff814b81),
|
|
||||||
onTertiary: Color(0xfffffbff),
|
|
||||||
tertiaryContainer: Color(0xffffd6fa),
|
|
||||||
onTertiaryContainer: Color(0xff340439),
|
|
||||||
error: Color(0xffba1a1a),
|
|
||||||
onError: Color(0xfffffbff),
|
|
||||||
errorContainer: Color(0xffffdad6),
|
|
||||||
onErrorContainer: Color(0xff410002),
|
|
||||||
surface: Color(0xFFF0EFF4),
|
|
||||||
onSurface: Color(0xff1a1b21),
|
|
||||||
surfaceContainer: Color(0xfffefbff),
|
|
||||||
surfaceContainerHigh: Color(0xFFE0E1EA),
|
|
||||||
surfaceContainerHighest: Color(0xffe0e2ef),
|
|
||||||
onSurfaceVariant: Color(0xff444651),
|
|
||||||
outline: Color(0xff747782),
|
|
||||||
outlineVariant: Color(0xffc4c6d3),
|
|
||||||
shadow: Color(0xff000000),
|
|
||||||
scrim: Color(0xff000000),
|
|
||||||
inverseSurface: Color(0xff2f3036),
|
|
||||||
onInverseSurface: Color(0xfff1f0f7),
|
|
||||||
inversePrimary: Color(0xffb2c5ff),
|
|
||||||
surfaceTint: Color(0xff06409f),
|
|
||||||
);
|
);
|
||||||
|
static final blueDark = ColorScheme.fromSeed(
|
||||||
static const ColorScheme blueDark = ColorScheme(
|
seedColor: Color(0xff001b3d),
|
||||||
brightness: Brightness.dark,
|
brightness: Brightness.dark,
|
||||||
primary: Color(0xffa9c7ff),
|
|
||||||
onPrimary: Color(0xff001b3d),
|
|
||||||
primaryContainer: Color(0xff00468c),
|
|
||||||
onPrimaryContainer: Color(0xffd6e3ff),
|
|
||||||
secondary: Color(0xffd6e3ff),
|
|
||||||
onSecondary: Color(0xff001b3d),
|
|
||||||
secondaryContainer: Color(0xff003063),
|
|
||||||
onSecondaryContainer: Color(0xffd6e3ff),
|
|
||||||
tertiary: Color(0xffeab4f6),
|
|
||||||
onTertiary: Color(0xff310540),
|
|
||||||
tertiaryContainer: Color(0xff61356e),
|
|
||||||
onTertiaryContainer: Color(0xfffad7ff),
|
|
||||||
error: Color(0xffffb4ab),
|
|
||||||
onError: Color(0xff410002),
|
|
||||||
errorContainer: Color(0xff93000a),
|
|
||||||
onErrorContainer: Color(0xffffb4ab),
|
|
||||||
surface: Color(0xFF15181C),
|
|
||||||
onSurface: Color(0xffe2e2e9),
|
|
||||||
surfaceContainer: Color(0xff1a1e22),
|
|
||||||
surfaceContainerHigh: Color(0xFF2C3138),
|
|
||||||
surfaceContainerHighest: Color(0xff424852),
|
|
||||||
onSurfaceVariant: Color(0xffc2c6d2),
|
|
||||||
outline: Color(0xff8c919c),
|
|
||||||
outlineVariant: Color(0xff424751),
|
|
||||||
shadow: Color(0xff000000),
|
|
||||||
scrim: Color(0xff000000),
|
|
||||||
inverseSurface: Color(0xffe1e1e9),
|
|
||||||
onInverseSurface: Color(0xff2e3036),
|
|
||||||
inversePrimary: Color(0xff005db7),
|
|
||||||
surfaceTint: Color(0xffa9c7ff),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -5,16 +5,24 @@ import 'package:immich_mobile/utils/extensions/material_state.extension.dart';
|
|||||||
import 'package:material_symbols_icons/symbols.dart';
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
|
|
||||||
enum AppTheme {
|
enum AppTheme {
|
||||||
blue._(AppColors.blueLight, AppColors.blueDark),
|
blue,
|
||||||
// Fallback color for dynamic theme for non-supported platforms
|
dynamic;
|
||||||
dynamic._(AppColors.blueLight, AppColors.blueDark);
|
|
||||||
|
|
||||||
final ColorScheme lightSchema;
|
ColorScheme getColorScheme({Brightness brightness = Brightness.light}) {
|
||||||
final ColorScheme darkSchema;
|
if (brightness == Brightness.dark) {
|
||||||
|
return switch (this) {
|
||||||
|
blue || dynamic => AppColors.blueDark,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return switch (this) {
|
||||||
|
blue || dynamic => AppColors.blueLight,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const AppTheme._(this.lightSchema, this.darkSchema);
|
static ThemeData generateThemeDataForColorScheme(
|
||||||
|
ColorScheme color, {
|
||||||
static ThemeData generateThemeData(ColorScheme color) {
|
Brightness brightness = Brightness.light,
|
||||||
|
}) {
|
||||||
return ThemeData(
|
return ThemeData(
|
||||||
inputDecorationTheme: InputDecorationTheme(
|
inputDecorationTheme: InputDecorationTheme(
|
||||||
hintStyle: const TextStyle(
|
hintStyle: const TextStyle(
|
||||||
@ -39,9 +47,16 @@ enum AppTheme {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
colorScheme: color,
|
colorScheme: color,
|
||||||
|
brightness: brightness,
|
||||||
|
dialogBackgroundColor: color.surfaceContainer,
|
||||||
primaryColor: color.primary,
|
primaryColor: color.primary,
|
||||||
scaffoldBackgroundColor: color.surface,
|
scaffoldBackgroundColor: color.surface,
|
||||||
iconTheme: const IconThemeData(size: 24, weight: 500, opticalSize: 24),
|
iconTheme: IconThemeData(
|
||||||
|
size: 24,
|
||||||
|
weight: 500,
|
||||||
|
opticalSize: 24,
|
||||||
|
color: color.onSurface,
|
||||||
|
),
|
||||||
textTheme: TextTheme(
|
textTheme: TextTheme(
|
||||||
displayLarge: AppTypography.displayLarge,
|
displayLarge: AppTypography.displayLarge,
|
||||||
displayMedium: AppTypography.displayMedium,
|
displayMedium: AppTypography.displayMedium,
|
||||||
@ -64,13 +79,13 @@ enum AppTheme {
|
|||||||
closeButtonIconBuilder: (_) => Icon(Symbols.close_rounded),
|
closeButtonIconBuilder: (_) => Icon(Symbols.close_rounded),
|
||||||
),
|
),
|
||||||
appBarTheme: AppBarTheme(
|
appBarTheme: AppBarTheme(
|
||||||
backgroundColor: color.surfaceContainerLowest,
|
backgroundColor: color.surface,
|
||||||
iconTheme: IconThemeData(size: 22, color: color.onSurface),
|
iconTheme: IconThemeData(size: 22, color: color.onSurface),
|
||||||
titleTextStyle:
|
titleTextStyle:
|
||||||
AppTypography.titleLarge.copyWith(color: color.onSurface),
|
AppTypography.titleLarge.copyWith(color: color.onSurface),
|
||||||
),
|
),
|
||||||
navigationBarTheme: NavigationBarThemeData(
|
navigationBarTheme: NavigationBarThemeData(
|
||||||
backgroundColor: color.surfaceContainer,
|
backgroundColor: color.surfaceContainerLow,
|
||||||
indicatorColor: color.primary,
|
indicatorColor: color.primary,
|
||||||
iconTheme: WidgetStateProperty.resolveWith(
|
iconTheme: WidgetStateProperty.resolveWith(
|
||||||
(Set<WidgetState> states) {
|
(Set<WidgetState> states) {
|
||||||
@ -82,7 +97,7 @@ enum AppTheme {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
navigationRailTheme: NavigationRailThemeData(
|
navigationRailTheme: NavigationRailThemeData(
|
||||||
backgroundColor: color.surfaceContainer,
|
backgroundColor: color.surfaceContainerLow,
|
||||||
elevation: 3,
|
elevation: 3,
|
||||||
unselectedIconTheme: IconThemeData(
|
unselectedIconTheme: IconThemeData(
|
||||||
weight: 500,
|
weight: 500,
|
||||||
@ -115,4 +130,13 @@ enum AppTheme {
|
|||||||
textSelectionTheme: TextSelectionThemeData(cursorColor: color.primary),
|
textSelectionTheme: TextSelectionThemeData(cursorColor: color.primary),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ThemeData generateThemeData({Brightness brightness = Brightness.light}) {
|
||||||
|
final color = getColorScheme(brightness: brightness);
|
||||||
|
|
||||||
|
return AppTheme.generateThemeDataForColorScheme(
|
||||||
|
color,
|
||||||
|
brightness: brightness,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -39,10 +39,11 @@ import 'package:immich_mobile/domain/services/hash.service.dart';
|
|||||||
import 'package:immich_mobile/domain/services/login.service.dart';
|
import 'package:immich_mobile/domain/services/login.service.dart';
|
||||||
import 'package:immich_mobile/platform/messages.g.dart';
|
import 'package:immich_mobile/platform/messages.g.dart';
|
||||||
import 'package:immich_mobile/presentation/router/router.dart';
|
import 'package:immich_mobile/presentation/router/router.dart';
|
||||||
|
import 'package:immich_mobile/presentation/states/app_info.state.dart';
|
||||||
import 'package:immich_mobile/presentation/states/app_theme.state.dart';
|
import 'package:immich_mobile/presentation/states/app_theme.state.dart';
|
||||||
import 'package:immich_mobile/presentation/states/current_user.state.dart';
|
import 'package:immich_mobile/presentation/states/current_user.state.dart';
|
||||||
import 'package:immich_mobile/presentation/states/gallery_permission.state.dart';
|
import 'package:immich_mobile/presentation/states/gallery_permission.state.dart';
|
||||||
import 'package:immich_mobile/presentation/states/server_feature_config.state.dart';
|
import 'package:immich_mobile/presentation/states/server_info.state.dart';
|
||||||
import 'package:immich_mobile/utils/immich_api_client.dart';
|
import 'package:immich_mobile/utils/immich_api_client.dart';
|
||||||
|
|
||||||
final di = GetIt.I;
|
final di = GetIt.I;
|
||||||
@ -156,11 +157,12 @@ abstract final class ServiceLocator {
|
|||||||
() => AppThemeProvider(settingsService: di()),
|
() => AppThemeProvider(settingsService: di()),
|
||||||
);
|
);
|
||||||
_registerSingleton(GalleryPermissionProvider());
|
_registerSingleton(GalleryPermissionProvider());
|
||||||
|
_registerSingleton(AppInfoProvider());
|
||||||
}
|
}
|
||||||
|
|
||||||
static void registerPostGlobalStates() {
|
static void registerPostGlobalStates() {
|
||||||
_registerLazySingleton<ServerFeatureConfigProvider>(
|
_registerLazySingleton<ServerInfoProvider>(
|
||||||
() => ServerFeatureConfigProvider(serverApiRepo: di()),
|
() => ServerInfoProvider(serverApiRepo: di()),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,21 +5,31 @@ abstract final class SizeConstants {
|
|||||||
const SizeConstants._();
|
const SizeConstants._();
|
||||||
|
|
||||||
static const s = 8.0;
|
static const s = 8.0;
|
||||||
|
static const xs = 11.0;
|
||||||
|
static const xxs = 14.0;
|
||||||
static const m = 16.0;
|
static const m = 16.0;
|
||||||
static const xm = 25.0;
|
static const xm = 20.0;
|
||||||
|
static const xxm = 25.0;
|
||||||
static const l = 32.0;
|
static const l = 32.0;
|
||||||
static const xl = 64.0;
|
static const xl = 48.0;
|
||||||
|
static const xxl = 64.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract final class RatioConstants {
|
abstract final class RatioConstants {
|
||||||
const RatioConstants._();
|
const RatioConstants._();
|
||||||
|
|
||||||
|
// 0.75
|
||||||
|
static const threeFourth = 3 / 4;
|
||||||
|
// 0.6
|
||||||
|
static const twoThird = 2 / 3;
|
||||||
// 0.5
|
// 0.5
|
||||||
static const oneHalf = 1 / 2;
|
static const half = 1 / 2;
|
||||||
// 0.3
|
// 0.3
|
||||||
static const oneThird = 1 / 3;
|
static const oneThird = 1 / 3;
|
||||||
// 0.25
|
// 0.25
|
||||||
static const quarter = 1 / 4;
|
static const quarter = 1 / 4;
|
||||||
// 0.15
|
// 0.15
|
||||||
static const halfQuarter = 3 / 20;
|
static const halfQuarter = 3 / 20;
|
||||||
|
// 0.10
|
||||||
|
static const oneTenth = 1 / 10;
|
||||||
}
|
}
|
||||||
|
24
mobile-v2/lib/utils/extensions/number.extension.dart
Normal file
24
mobile-v2/lib/utils/extensions/number.extension.dart
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import 'dart:math';
|
||||||
|
|
||||||
|
extension NumberToSizeExtension on num {
|
||||||
|
String formatAsSize({int noOfDecimals = 0}) {
|
||||||
|
const List<String> units = [
|
||||||
|
'B',
|
||||||
|
'KB',
|
||||||
|
'MB',
|
||||||
|
'GB',
|
||||||
|
'TB',
|
||||||
|
'PB',
|
||||||
|
'EB',
|
||||||
|
'ZB',
|
||||||
|
'YB',
|
||||||
|
];
|
||||||
|
if (this == 0) return '0 B';
|
||||||
|
final index = (log(this) / log(1024)).floor();
|
||||||
|
final byteIndex = index.clamp(0, units.length - 1);
|
||||||
|
|
||||||
|
final size = (this / pow(1024, byteIndex)).round();
|
||||||
|
// ignore: avoid-unsafe-collection-methods
|
||||||
|
return '${size.toStringAsFixed(noOfDecimals)} ${units[byteIndex]}';
|
||||||
|
}
|
||||||
|
}
|
@ -1,3 +1,4 @@
|
|||||||
extension StringNumberUtils on String {
|
extension StringNumberUtils on String {
|
||||||
int? tryParseInt() => int.tryParse(this);
|
int? tryParseInt() => int.tryParse(this);
|
||||||
|
int parseInt() => int.parse(this);
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user