feat: appbar

This commit is contained in:
shenlong-tanwen 2024-10-27 23:43:58 +05:30
parent 5385d43c8c
commit 8450c8cc4f
40 changed files with 1150 additions and 211 deletions

View File

@ -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

View File

@ -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"
}
}
}
}

View File

@ -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();
}

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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_);

View File

@ -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));

View File

@ -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,
),
);
});
}
}

View File

@ -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,9 +36,12 @@ class ImAppBar extends StatelessWidget implements PreferredSizeWidget {
actions: [
Padding(
padding: const EdgeInsets.only(right: SizeConstants.m),
child: ImUserAvatar(
user: di<CurrentUserProvider>().value,
radius: SizeConstants.m,
child: InkWell(
onTap: () => showAppBarDialog(context),
child: ImUserAvatar(
user: di<CurrentUserProvider>().value,
radius: SizeConstants.m,
),
),
),
],

View File

@ -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),
),
],
),
);
}
}

View File

@ -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()),
);
}
}

View File

@ -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,
);
}
}

View File

@ -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,
),
);
}
}

View File

@ -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,
),
),
),
);
}
}

View File

@ -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);
}

View File

@ -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!,
],
),
);
}
}

View File

@ -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(),

View File

@ -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,
),
),
);
}
}

View File

@ -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),
),
],
),

View File

@ -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) {

View File

@ -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,
),

View File

@ -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()

View File

@ -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(
style: TextStyle(color: context.theme.colorScheme.outline),
child: Text(snap.data?.version ?? ''),
),
DefaultTextStyle.merge(
style: TextStyle(color: context.theme.colorScheme.outline),
child: Text(version),
),
TextButton(
onPressed: () => unawaited(context.navigateRoot(const LogsRoute())),

View File

@ -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,

View File

@ -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,
);
}
}

View File

@ -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),
),
),
),
);

View File

@ -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(
user: di<CurrentUserProvider>().value,
dimension: SizeConstants.m,
radius: SizeConstants.m,
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,

View File

@ -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),

View 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;
}

View File

@ -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);
}
}
}

View 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);
}
}
}

View File

@ -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),
);
}

View File

@ -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,
);
}
}

View File

@ -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()),
);
}

View File

@ -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;
}

View 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]}';
}
}

View File

@ -1,3 +1,4 @@
extension StringNumberUtils on String {
int? tryParseInt() => int.tryParse(this);
int parseInt() => int.parse(this);
}