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:
|
||||
last:
|
||||
- child
|
||||
- children
|
||||
- avoid-accessing-other-classes-private-members
|
||||
- avoid-assigning-to-static-field
|
||||
- avoid-assignments-as-conditions
|
||||
|
@ -38,7 +38,7 @@
|
||||
},
|
||||
"logs": {
|
||||
"title": "Logs",
|
||||
"no_logs": "No logs available",
|
||||
"no_logs_message": "No logs available",
|
||||
"detail": {
|
||||
"title": "Log Detail",
|
||||
"message_heading": "MESSAGE",
|
||||
@ -49,6 +49,27 @@
|
||||
},
|
||||
"common": {
|
||||
"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_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';
|
||||
|
||||
abstract interface class IServerApiRepository {
|
||||
/// Pings and check if server is reachable
|
||||
@ -10,4 +12,10 @@ abstract interface class IServerApiRepository {
|
||||
|
||||
/// Fetches the server configuration and settings
|
||||
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/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';
|
||||
import 'package:immich_mobile/utils/mixins/log.mixin.dart';
|
||||
import 'package:openapi/api.dart' as api;
|
||||
|
||||
@ -40,6 +42,32 @@ class ServerApiRepository with LogMixin implements IServerApiRepository {
|
||||
}
|
||||
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(
|
||||
@ -50,3 +78,14 @@ ServerFeatures _fromFeatureDto(api.ServerFeaturesDto dto) => ServerFeatures(
|
||||
hasPasswordLogin: dto.passwordLogin,
|
||||
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/asset_sync.service.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/utils/immich_api_client.dart';
|
||||
import 'package:immich_mobile/utils/mixins/log.mixin.dart';
|
||||
@ -115,7 +115,7 @@ class LoginService with LogMixin {
|
||||
ServiceLocator.registerPostGlobalStates();
|
||||
|
||||
// Fetch server features
|
||||
await di<ServerFeatureConfigProvider>().getFeatures();
|
||||
await di<ServerInfoProvider>().fetchFeatures();
|
||||
}
|
||||
|
||||
Future<User?> handlePostLogin() async {
|
||||
@ -132,6 +132,7 @@ class LoginService with LogMixin {
|
||||
}
|
||||
|
||||
ServiceLocator.registerCurrentUser(user);
|
||||
await di<ServerInfoProvider>().fetchServerDisk();
|
||||
|
||||
// sync assets in background
|
||||
unawaited(di<AssetSyncService>().performFullRemoteSyncIsolate(user));
|
||||
|
@ -59,20 +59,41 @@ class _AppThemeBuilder extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
// Static colors
|
||||
if (theme != AppTheme.dynamic) {
|
||||
final lightTheme = AppTheme.generateThemeData(theme.lightSchema);
|
||||
final darkTheme = AppTheme.generateThemeData(theme.darkSchema);
|
||||
|
||||
return builder(context, lightTheme, darkTheme);
|
||||
return builder(
|
||||
context,
|
||||
theme.generateThemeData(),
|
||||
theme.generateThemeData(brightness: Brightness.dark),
|
||||
);
|
||||
}
|
||||
|
||||
// Dynamic color builder
|
||||
return DynamicColorBuilder(builder: (lightDynamic, darkDynamic) {
|
||||
final lightTheme =
|
||||
AppTheme.generateThemeData(lightDynamic ?? theme.lightSchema);
|
||||
final darkTheme =
|
||||
AppTheme.generateThemeData(darkDynamic ?? theme.darkSchema);
|
||||
if (lightDynamic == null || darkDynamic == null) {
|
||||
final defaultTheme = AppTheme.blue;
|
||||
return builder(
|
||||
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: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/user_avatar.widget.dart';
|
||||
import 'package:immich_mobile/presentation/components/image/immich_logo.widget.dart';
|
||||
@ -13,6 +16,11 @@ class ImAppBar extends StatelessWidget implements PreferredSizeWidget {
|
||||
@override
|
||||
Size get preferredSize => const Size.fromHeight(kToolbarHeight);
|
||||
|
||||
static void showAppBarDialog(BuildContext context) => unawaited(showDialog(
|
||||
context: context,
|
||||
builder: (_) => const ImAppBarDialog(),
|
||||
));
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AppBar(
|
||||
@ -20,7 +28,7 @@ class ImAppBar extends StatelessWidget implements PreferredSizeWidget {
|
||||
title: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
ImLogo(dimension: SizeConstants.xm),
|
||||
ImLogo(dimension: SizeConstants.xxm),
|
||||
SizedGap.sw(),
|
||||
ImLogoText(fontSize: 20),
|
||||
],
|
||||
@ -28,11 +36,14 @@ class ImAppBar extends StatelessWidget implements PreferredSizeWidget {
|
||||
actions: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: SizeConstants.m),
|
||||
child: InkWell(
|
||||
onTap: () => showAppBarDialog(context),
|
||||
child: ImUserAvatar(
|
||||
user: di<CurrentUserProvider>().value,
|
||||
radius: SizeConstants.m,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
backgroundColor: context.theme.appBarTheme.backgroundColor,
|
||||
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
|
||||
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.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.xlh({super.key}) : super(height: SizeConstants.xl);
|
||||
const SizedGap.xxlh({super.key}) : super(height: SizeConstants.xxl);
|
||||
|
||||
// Widgets to be used in Row
|
||||
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.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.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,
|
||||
placeholder: (_, __) => Image.memory(
|
||||
kTransparentImage,
|
||||
semanticLabel: 'Transparent',
|
||||
semanticLabel: 'Transparent Image',
|
||||
),
|
||||
fadeInDuration: const Duration(milliseconds: 300),
|
||||
errorWidget: (_, error, stackTrace) => SizedBox.square(),
|
||||
|
@ -3,17 +3,18 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.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/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/immich_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';
|
||||
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:intl/intl.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
|
||||
part 'immich_asset_grid_header.widget.dart';
|
||||
part 'immich_asset_render_grid.widget.dart';
|
||||
part 'asset_grid_header.widget.dart';
|
||||
|
||||
class ImAssetGrid extends StatefulWidget {
|
||||
/// The padding for the grid
|
||||
@ -66,6 +67,10 @@ class _ImAssetGridState extends State<ImAssetGrid> {
|
||||
builder: (_, state) {
|
||||
final elements = state.renderList.elements;
|
||||
|
||||
if (state.renderList.totalCount == 0) {
|
||||
return const _ImGridEmpty();
|
||||
}
|
||||
|
||||
// Append padding if required
|
||||
if (widget.topPadding != null &&
|
||||
elements.firstOrNull is! RenderListPaddingElement) {
|
||||
@ -94,7 +99,7 @@ class _ImAssetGridState extends State<ImAssetGrid> {
|
||||
RenderListMonthHeaderElement() =>
|
||||
_MonthHeader(text: section.header),
|
||||
RenderListDayHeaderElement() => Text(section.header),
|
||||
RenderListAssetElement() => _StaticGrid(
|
||||
RenderListAssetElement() => ImStaticGrid(
|
||||
section: section,
|
||||
isDragging: state.isDragScrolling,
|
||||
),
|
||||
@ -137,3 +142,21 @@ class _ImAssetGridState extends State<ImAssetGrid> {
|
||||
.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 {
|
||||
final String text;
|
||||
@ -22,7 +22,8 @@ class _HeaderText extends StatelessWidget {
|
||||
const Spacer(),
|
||||
Icon(
|
||||
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 bool isDragging;
|
||||
|
||||
const _StaticGrid({required this.section, required this.isDragging});
|
||||
const ImStaticGrid({
|
||||
super.key,
|
||||
required this.section,
|
||||
required this.isDragging,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.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/utils/constants/size_constants.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
|
||||
IconData _getStorageIcon(Asset asset) {
|
||||
@ -77,7 +78,7 @@ class _PadAlignedIcon extends StatelessWidget {
|
||||
alignment: alignment,
|
||||
child: Icon(
|
||||
icon,
|
||||
size: 20,
|
||||
size: SizeConstants.xm,
|
||||
fill: (filled != null && filled!) ? 1 : null,
|
||||
color: Colors.white,
|
||||
),
|
||||
|
@ -2,9 +2,9 @@ import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.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/grid/immich_asset_grid.state.dart';
|
||||
import 'package:immich_mobile/presentation/components/grid/immich_asset_grid.widget.dart';
|
||||
import 'package:immich_mobile/presentation/components/appbar/app_bar.widget.dart';
|
||||
import 'package:immich_mobile/presentation/components/grid/asset_grid.state.dart';
|
||||
import 'package:immich_mobile/presentation/components/grid/asset_grid.widget.dart';
|
||||
import 'package:immich_mobile/utils/extensions/build_context.extension.dart';
|
||||
|
||||
@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/widgets/login_form.widget.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/extensions/build_context.extension.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
@RoutePage()
|
||||
@ -93,17 +94,16 @@ class _LoginPageState extends State<LoginPage>
|
||||
),
|
||||
);
|
||||
|
||||
final version = di<AppInfoProvider>().value.versionString;
|
||||
|
||||
final Widget bottom = Padding(
|
||||
padding: const EdgeInsets.only(bottom: SizeConstants.s),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
FutureBuilder(
|
||||
future: PackageInfo.fromPlatform(),
|
||||
builder: (_, snap) => DefaultTextStyle.merge(
|
||||
DefaultTextStyle.merge(
|
||||
style: TextStyle(color: context.theme.colorScheme.outline),
|
||||
child: Text(snap.data?.version ?? ''),
|
||||
),
|
||||
child: Text(version),
|
||||
),
|
||||
TextButton(
|
||||
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/modules/login/models/login_page.model.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:material_symbols_icons/symbols.dart';
|
||||
|
||||
@ -132,7 +132,7 @@ class _CredentialsFormState extends State<_CredentialsForm> {
|
||||
builder: (_, isValidationInProgress) => isValidationInProgress
|
||||
? const ImLoadingIndicator()
|
||||
: ValueListenableBuilder(
|
||||
valueListenable: di<ServerFeatureConfigProvider>(),
|
||||
valueListenable: di<ServerInfoProvider>(),
|
||||
builder: (_, state, __) => Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
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/models/log.model.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/scaffold/adaptive_route_appbar.widget.dart';
|
||||
import 'package:immich_mobile/presentation/components/scaffold/adaptive_route_wrapper.widget.dart';
|
||||
@ -183,19 +183,9 @@ class _LogListEmpty extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Symbols.comments_disabled_rounded,
|
||||
size: 50,
|
||||
color: context.colorScheme.primary,
|
||||
),
|
||||
const SizedGap.mh(),
|
||||
Text(context.t.logs.no_logs),
|
||||
],
|
||||
),
|
||||
return ImPageEmptyIndicator(
|
||||
icon: Symbols.comments_disabled_rounded,
|
||||
message: context.t.logs.no_logs_message,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -19,7 +19,10 @@ class AboutSettingsPage extends StatelessWidget {
|
||||
onTap: () => showLicensePage(
|
||||
context: context,
|
||||
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_adaptive_scaffold/flutter_adaptive_scaffold.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/user_avatar.widget.dart';
|
||||
import 'package:immich_mobile/presentation/components/image/immich_logo.widget.dart';
|
||||
@ -97,12 +98,15 @@ class _TabControllerAdaptiveScaffold extends StatelessWidget {
|
||||
.toList(),
|
||||
selectedIndex: selectedIndex,
|
||||
backgroundColor: navRailTheme.backgroundColor,
|
||||
leading: ImUserAvatar(
|
||||
leading: InkWell(
|
||||
onTap: () => ImAppBar.showAppBarDialog(context),
|
||||
child: ImUserAvatar(
|
||||
user: di<CurrentUserProvider>().value,
|
||||
dimension: SizeConstants.m,
|
||||
radius: SizeConstants.m,
|
||||
),
|
||||
trailing: ImLogo(dimension: SizeConstants.xm),
|
||||
),
|
||||
trailing: ImLogo(dimension: SizeConstants.xxm),
|
||||
onDestinationSelected: onSelectedIndexChange,
|
||||
selectedIconTheme: navRailTheme.selectedIconTheme,
|
||||
unselectedIconTheme: navRailTheme.unselectedIconTheme,
|
||||
|
@ -32,11 +32,11 @@ class AppRouter extends RootStackRouter {
|
||||
List<AutoRoute> get routes => [
|
||||
AutoRoute(
|
||||
page: SplashScreenWrapperRoute.page,
|
||||
initial: true,
|
||||
children: [
|
||||
AutoRoute(page: SplashScreenRoute.page, initial: true),
|
||||
AutoRoute(page: LoginRoute.page),
|
||||
],
|
||||
initial: true,
|
||||
),
|
||||
AutoRoute(page: LogsWrapperRoute.page, children: [
|
||||
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 {
|
||||
const AppColors();
|
||||
|
||||
/// Blue color
|
||||
static const ColorScheme blueLight = ColorScheme(
|
||||
static final blueLight = ColorScheme.fromSeed(
|
||||
seedColor: Color(0xff1145a4),
|
||||
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 const ColorScheme blueDark = ColorScheme(
|
||||
static final blueDark = ColorScheme.fromSeed(
|
||||
seedColor: Color(0xff001b3d),
|
||||
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';
|
||||
|
||||
enum AppTheme {
|
||||
blue._(AppColors.blueLight, AppColors.blueDark),
|
||||
// Fallback color for dynamic theme for non-supported platforms
|
||||
dynamic._(AppColors.blueLight, AppColors.blueDark);
|
||||
blue,
|
||||
dynamic;
|
||||
|
||||
final ColorScheme lightSchema;
|
||||
final ColorScheme darkSchema;
|
||||
ColorScheme getColorScheme({Brightness brightness = Brightness.light}) {
|
||||
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 generateThemeData(ColorScheme color) {
|
||||
static ThemeData generateThemeDataForColorScheme(
|
||||
ColorScheme color, {
|
||||
Brightness brightness = Brightness.light,
|
||||
}) {
|
||||
return ThemeData(
|
||||
inputDecorationTheme: InputDecorationTheme(
|
||||
hintStyle: const TextStyle(
|
||||
@ -39,9 +47,16 @@ enum AppTheme {
|
||||
),
|
||||
),
|
||||
colorScheme: color,
|
||||
brightness: brightness,
|
||||
dialogBackgroundColor: color.surfaceContainer,
|
||||
primaryColor: color.primary,
|
||||
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(
|
||||
displayLarge: AppTypography.displayLarge,
|
||||
displayMedium: AppTypography.displayMedium,
|
||||
@ -64,13 +79,13 @@ enum AppTheme {
|
||||
closeButtonIconBuilder: (_) => Icon(Symbols.close_rounded),
|
||||
),
|
||||
appBarTheme: AppBarTheme(
|
||||
backgroundColor: color.surfaceContainerLowest,
|
||||
backgroundColor: color.surface,
|
||||
iconTheme: IconThemeData(size: 22, color: color.onSurface),
|
||||
titleTextStyle:
|
||||
AppTypography.titleLarge.copyWith(color: color.onSurface),
|
||||
),
|
||||
navigationBarTheme: NavigationBarThemeData(
|
||||
backgroundColor: color.surfaceContainer,
|
||||
backgroundColor: color.surfaceContainerLow,
|
||||
indicatorColor: color.primary,
|
||||
iconTheme: WidgetStateProperty.resolveWith(
|
||||
(Set<WidgetState> states) {
|
||||
@ -82,7 +97,7 @@ enum AppTheme {
|
||||
),
|
||||
),
|
||||
navigationRailTheme: NavigationRailThemeData(
|
||||
backgroundColor: color.surfaceContainer,
|
||||
backgroundColor: color.surfaceContainerLow,
|
||||
elevation: 3,
|
||||
unselectedIconTheme: IconThemeData(
|
||||
weight: 500,
|
||||
@ -115,4 +130,13 @@ enum AppTheme {
|
||||
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/platform/messages.g.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/current_user.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';
|
||||
|
||||
final di = GetIt.I;
|
||||
@ -156,11 +157,12 @@ abstract final class ServiceLocator {
|
||||
() => AppThemeProvider(settingsService: di()),
|
||||
);
|
||||
_registerSingleton(GalleryPermissionProvider());
|
||||
_registerSingleton(AppInfoProvider());
|
||||
}
|
||||
|
||||
static void registerPostGlobalStates() {
|
||||
_registerLazySingleton<ServerFeatureConfigProvider>(
|
||||
() => ServerFeatureConfigProvider(serverApiRepo: di()),
|
||||
_registerLazySingleton<ServerInfoProvider>(
|
||||
() => ServerInfoProvider(serverApiRepo: di()),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -5,21 +5,31 @@ abstract final class SizeConstants {
|
||||
const SizeConstants._();
|
||||
|
||||
static const s = 8.0;
|
||||
static const xs = 11.0;
|
||||
static const xxs = 14.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 xl = 64.0;
|
||||
static const xl = 48.0;
|
||||
static const xxl = 64.0;
|
||||
}
|
||||
|
||||
abstract final class RatioConstants {
|
||||
const RatioConstants._();
|
||||
|
||||
// 0.75
|
||||
static const threeFourth = 3 / 4;
|
||||
// 0.6
|
||||
static const twoThird = 2 / 3;
|
||||
// 0.5
|
||||
static const oneHalf = 1 / 2;
|
||||
static const half = 1 / 2;
|
||||
// 0.3
|
||||
static const oneThird = 1 / 3;
|
||||
// 0.25
|
||||
static const quarter = 1 / 4;
|
||||
// 0.15
|
||||
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 {
|
||||
int? tryParseInt() => int.tryParse(this);
|
||||
int parseInt() => int.parse(this);
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user