Source Code added

This commit is contained in:
Fr4nz D13trich 2026-02-02 15:06:40 +01:00
parent 800376eafd
commit 9efa9bc6dd
3912 changed files with 754770 additions and 2 deletions

View file

@ -0,0 +1,294 @@
import 'dart:async';
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/models/backup/backup_state.model.dart';
import 'package:immich_mobile/providers/asset.provider.dart';
import 'package:immich_mobile/providers/auth.provider.dart';
import 'package:immich_mobile/providers/backup/backup.provider.dart';
import 'package:immich_mobile/providers/backup/manual_upload.provider.dart';
import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart';
import 'package:immich_mobile/providers/locale_provider.dart';
import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/providers/websocket.provider.dart';
import 'package:immich_mobile/pages/common/settings.page.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/utils/bytes_units.dart';
import 'package:immich_mobile/widgets/common/app_bar_dialog/app_bar_profile_info.dart';
import 'package:immich_mobile/widgets/common/app_bar_dialog/app_bar_server_info.dart';
import 'package:immich_mobile/widgets/common/confirm_dialog.dart';
import 'package:immich_mobile/widgets/common/immich_logo.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:url_launcher/url_launcher.dart';
class ImmichAppBarDialog extends HookConsumerWidget {
const ImmichAppBarDialog({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
ref.watch(localeProvider);
BackUpState backupState = ref.watch(backupProvider);
final theme = context.themeData;
bool isHorizontal = !context.isMobile;
final horizontalPadding = isHorizontal ? 100.0 : 20.0;
final user = ref.watch(currentUserProvider);
final isLoggingOut = useState(false);
final isReadonlyModeEnabled = ref.watch(readonlyModeProvider);
useEffect(() {
ref.read(backupProvider.notifier).updateDiskInfo();
ref.read(currentUserProvider.notifier).refresh();
return null;
}, []);
buildTopRow() {
return SizedBox(
height: 56,
child: Stack(
alignment: Alignment.centerLeft,
children: [
IconButton(onPressed: () => context.pop(), icon: const Icon(Icons.close, size: 20)),
Align(
alignment: Alignment.center,
child: Padding(
padding: const EdgeInsets.only(bottom: 4),
child: Image.asset(
context.isDarkTheme ? 'assets/immich-text-dark.png' : 'assets/immich-text-light.png',
height: 16,
),
),
),
],
),
);
}
buildActionButton(IconData icon, String text, Function() onTap, {Widget? trailing}) {
return ListTile(
dense: true,
visualDensity: VisualDensity.standard,
contentPadding: const EdgeInsets.only(left: 30, right: 30),
minLeadingWidth: 40,
leading: SizedBox(child: Icon(icon, color: theme.textTheme.labelLarge?.color?.withAlpha(250), size: 20)),
title: Text(
text,
style: theme.textTheme.labelLarge?.copyWith(color: theme.textTheme.labelLarge?.color?.withAlpha(250)),
).tr(),
onTap: onTap,
trailing: trailing,
);
}
buildSettingButton() {
return buildActionButton(Icons.settings_outlined, "settings", () => context.pushRoute(const SettingsRoute()));
}
buildFreeUpSpaceButton() {
return buildActionButton(
Icons.cleaning_services_outlined,
"free_up_space",
() => context.pushRoute(SettingsSubRoute(section: SettingSection.freeUpSpace)),
);
}
buildAppLogButton() {
return buildActionButton(
Icons.assignment_outlined,
"profile_drawer_app_logs",
() => context.pushRoute(const AppLogRoute()),
);
}
buildSignOutButton() {
return buildActionButton(
Icons.logout_rounded,
"sign_out",
() async {
if (isLoggingOut.value) {
return;
}
unawaited(
showDialog(
context: context,
builder: (BuildContext ctx) {
return ConfirmDialog(
title: "app_bar_signout_dialog_title",
content: "app_bar_signout_dialog_content",
ok: "yes",
onOk: () async {
isLoggingOut.value = true;
await ref.read(authProvider.notifier).logout().whenComplete(() => isLoggingOut.value = false);
ref.read(manualUploadProvider.notifier).cancelBackup();
ref.read(backupProvider.notifier).cancelBackup();
unawaited(ref.read(assetProvider.notifier).clearAllAssets());
ref.read(websocketProvider.notifier).disconnect();
unawaited(context.replaceRoute(const LoginRoute()));
},
);
},
),
);
},
trailing: isLoggingOut.value
? const SizedBox.square(dimension: 20, child: CircularProgressIndicator(strokeWidth: 2))
: null,
);
}
Widget buildStorageInformation() {
var percentage = backupState.serverInfo.diskUsagePercentage / 100;
var usedDiskSpace = backupState.serverInfo.diskUse;
var totalDiskSpace = backupState.serverInfo.diskSize;
if (user != null && user.hasQuota) {
usedDiskSpace = formatBytes(user.quotaUsageInBytes);
totalDiskSpace = formatBytes(user.quotaSizeInBytes);
percentage = user.quotaUsageInBytes / user.quotaSizeInBytes;
}
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 10.0, vertical: 3),
child: Container(
padding: const EdgeInsets.symmetric(vertical: 4),
decoration: BoxDecoration(color: context.colorScheme.surface),
child: ListTile(
minLeadingWidth: 50,
leading: Icon(Icons.storage_rounded, color: theme.primaryColor),
title: Text(
"backup_controller_page_server_storage",
style: context.textTheme.labelLarge?.copyWith(fontWeight: FontWeight.w500),
).tr(),
isThreeLine: true,
subtitle: Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: LinearProgressIndicator(
minHeight: 10.0,
value: percentage,
borderRadius: const BorderRadius.all(Radius.circular(10.0)),
),
),
Padding(
padding: const EdgeInsets.only(top: 12.0),
child: const Text(
'backup_controller_page_storage_format',
).tr(namedArgs: {'used': usedDiskSpace, 'total': totalDiskSpace}),
),
],
),
),
),
),
);
}
buildFooter() {
return Padding(
padding: const EdgeInsets.only(top: 10, bottom: 20),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
InkWell(
onTap: () {
context.pop();
launchUrl(Uri.parse('https://docs.immich.app'), mode: LaunchMode.externalApplication);
},
child: Text("documentation", style: context.textTheme.bodySmall).tr(),
),
const SizedBox(width: 20, child: Text("", textAlign: TextAlign.center)),
InkWell(
onTap: () {
context.pop();
launchUrl(Uri.parse('https://github.com/immich-app/immich'), mode: LaunchMode.externalApplication);
},
child: Text("profile_drawer_github", style: context.textTheme.bodySmall).tr(),
),
const SizedBox(width: 20, child: Text("", textAlign: TextAlign.center)),
InkWell(
onTap: () async {
context.pop();
final packageInfo = await PackageInfo.fromPlatform();
showLicensePage(
context: context,
applicationIcon: const Padding(
padding: EdgeInsetsGeometry.symmetric(vertical: 10),
child: ImmichLogo(size: 40),
),
applicationVersion: packageInfo.version,
);
},
child: Text("licenses", style: context.textTheme.bodySmall).tr(),
),
],
),
);
}
buildReadonlyMessage() {
return Padding(
padding: const EdgeInsets.only(left: 10.0, right: 10.0),
child: ListTile(
dense: true,
visualDensity: VisualDensity.standard,
contentPadding: const EdgeInsets.only(left: 20, right: 20),
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(10))),
minLeadingWidth: 20,
tileColor: theme.primaryColor.withAlpha(80),
title: Text(
"profile_drawer_readonly_mode",
style: theme.textTheme.labelLarge?.copyWith(color: theme.textTheme.labelLarge?.color?.withAlpha(250)),
textAlign: TextAlign.center,
).tr(),
),
);
}
return Dismissible(
behavior: HitTestBehavior.translucent,
direction: DismissDirection.down,
onDismissed: (_) => context.pop(),
key: const Key('app_bar_dialog'),
child: Dialog(
clipBehavior: Clip.hardEdge,
alignment: Alignment.topCenter,
insetPadding: EdgeInsets.only(
top: isHorizontal ? 20 : 40,
left: horizontalPadding,
right: horizontalPadding,
bottom: isHorizontal ? 20 : 100,
),
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(20))),
child: SizedBox(
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(padding: const EdgeInsets.symmetric(horizontal: 8), child: buildTopRow()),
const AppBarProfileInfoBox(),
buildStorageInformation(),
const AppBarServerInfo(),
if (Store.isBetaTimelineEnabled && isReadonlyModeEnabled) buildReadonlyMessage(),
buildAppLogButton(),
buildFreeUpSpaceButton(),
buildSettingButton(),
buildSignOutButton(),
buildFooter(),
],
),
),
),
),
);
}
}

View file

@ -0,0 +1,129 @@
import 'dart:async';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:image_picker/image_picker.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/theme_extensions.dart';
import 'package:immich_mobile/providers/auth.provider.dart';
import 'package:immich_mobile/providers/backup/backup.provider.dart';
import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart';
import 'package:immich_mobile/providers/upload_profile_image.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/widgets/common/immich_loading_indicator.dart';
import 'package:immich_mobile/widgets/common/user_circle_avatar.dart';
class AppBarProfileInfoBox extends HookConsumerWidget {
const AppBarProfileInfoBox({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final authState = ref.watch(authProvider);
final uploadProfileImageStatus = ref.watch(uploadProfileImageProvider).status;
final isReadonlyModeEnabled = ref.watch(readonlyModeProvider);
final user = ref.watch(currentUserProvider);
buildUserProfileImage() {
if (user == null) {
return const CircleAvatar(
radius: 20,
backgroundImage: AssetImage('assets/immich-logo.png'),
backgroundColor: Colors.transparent,
);
}
final userImage = UserCircleAvatar(radius: 22, size: 44, user: user);
if (uploadProfileImageStatus == UploadProfileStatus.loading) {
return const SizedBox(height: 40, width: 40, child: ImmichLoadingIndicator(borderRadius: 20));
}
return userImage;
}
pickUserProfileImage() async {
final XFile? image = await ImagePicker().pickImage(source: ImageSource.gallery, maxHeight: 1024, maxWidth: 1024);
if (image != null) {
var success = await ref.watch(uploadProfileImageProvider.notifier).upload(image);
if (success) {
final profileImagePath = ref.read(uploadProfileImageProvider).profileImagePath;
ref.watch(authProvider.notifier).updateUserProfileImagePath(profileImagePath);
if (user != null) {
ref.read(currentUserProvider.notifier).refresh();
}
unawaited(ref.read(backupProvider.notifier).updateDiskInfo());
}
}
}
void toggleReadonlyMode() {
// read only mode is only supported int he beta experience
// TODO: remove this check when the beta UI goes stable
if (!Store.isBetaTimelineEnabled) return;
final isReadonlyModeEnabled = ref.watch(readonlyModeProvider);
ref.read(readonlyModeProvider.notifier).toggleReadonlyMode();
context.scaffoldMessenger.showSnackBar(
SnackBar(
duration: const Duration(seconds: 2),
content: Text(
(isReadonlyModeEnabled ? "readonly_mode_disabled" : "readonly_mode_enabled").tr(),
style: context.textTheme.bodyLarge?.copyWith(color: context.primaryColor),
),
),
);
}
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 10.0),
child: Container(
width: double.infinity,
decoration: BoxDecoration(
color: context.colorScheme.surface,
borderRadius: const BorderRadius.only(topLeft: Radius.circular(10), topRight: Radius.circular(10)),
),
child: ListTile(
minLeadingWidth: 50,
leading: GestureDetector(
onTap: pickUserProfileImage,
onLongPress: toggleReadonlyMode,
child: Stack(
clipBehavior: Clip.none,
children: [
AbsorbPointer(child: buildUserProfileImage()),
if (!isReadonlyModeEnabled)
Positioned(
bottom: -5,
right: -8,
child: Material(
color: context.colorScheme.surfaceContainerHighest,
elevation: 3,
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(50.0))),
child: Padding(
padding: const EdgeInsets.all(5.0),
child: Icon(Icons.camera_alt_outlined, color: context.primaryColor, size: 14),
),
),
),
],
),
),
title: Text(
authState.name,
style: context.textTheme.titleMedium?.copyWith(color: context.primaryColor, fontWeight: FontWeight.w500),
),
subtitle: Text(
authState.userEmail,
style: context.textTheme.bodySmall?.copyWith(color: context.colorScheme.onSurfaceSecondary),
),
),
),
);
}
}

View file

@ -0,0 +1,223 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/theme_extensions.dart';
import 'package:immich_mobile/models/server_info/server_info.model.dart';
import 'package:immich_mobile/providers/locale_provider.dart';
import 'package:immich_mobile/providers/server_info.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/utils/url_helper.dart';
import 'package:immich_mobile/widgets/common/app_bar_dialog/server_update_notification.dart';
import 'package:package_info_plus/package_info_plus.dart';
class AppBarServerInfo extends HookConsumerWidget {
const AppBarServerInfo({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
ref.watch(localeProvider);
ServerInfo serverInfoState = ref.watch(serverInfoProvider);
final user = ref.watch(currentUserProvider);
final bool showVersionWarning = ref.watch(versionWarningPresentProvider(user));
final appInfo = useState({});
const titleFontSize = 12.0;
const contentFontSize = 11.0;
getPackageInfo() async {
PackageInfo packageInfo = await PackageInfo.fromPlatform();
appInfo.value = {"version": packageInfo.version, "buildNumber": packageInfo.buildNumber};
}
useEffect(() {
getPackageInfo();
return null;
}, []);
return Padding(
padding: const EdgeInsets.only(left: 10.0, right: 10.0, bottom: 10.0),
child: Container(
decoration: BoxDecoration(
color: context.colorScheme.surface,
borderRadius: const BorderRadius.only(bottomLeft: Radius.circular(10), bottomRight: Radius.circular(10)),
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
if (showVersionWarning) ...[
const Padding(padding: EdgeInsets.symmetric(horizontal: 8.0), child: ServerUpdateNotification()),
const Padding(padding: EdgeInsets.symmetric(horizontal: 10), child: Divider(thickness: 1)),
],
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Padding(
padding: const EdgeInsets.only(left: 10.0),
child: Text(
"server_info_box_app_version".tr(),
style: TextStyle(
fontSize: titleFontSize,
color: context.textTheme.labelSmall?.color,
fontWeight: FontWeight.w500,
),
),
),
),
Expanded(
flex: 0,
child: Padding(
padding: const EdgeInsets.only(right: 10.0),
child: Text(
"${appInfo.value["version"]} build.${appInfo.value["buildNumber"]}",
style: TextStyle(
fontSize: contentFontSize,
color: context.colorScheme.onSurfaceSecondary,
fontWeight: FontWeight.bold,
),
),
),
),
],
),
const Padding(padding: EdgeInsets.symmetric(horizontal: 10), child: Divider(thickness: 1)),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Padding(
padding: const EdgeInsets.only(left: 10.0),
child: Text(
"server_version".tr(),
style: TextStyle(
fontSize: titleFontSize,
color: context.textTheme.labelSmall?.color,
fontWeight: FontWeight.w500,
),
),
),
),
Expanded(
flex: 0,
child: Padding(
padding: const EdgeInsets.only(right: 10.0),
child: Text(
serverInfoState.serverVersion.major > 0
? "${serverInfoState.serverVersion.major}.${serverInfoState.serverVersion.minor}.${serverInfoState.serverVersion.patch}"
: "--",
style: TextStyle(
fontSize: contentFontSize,
color: context.colorScheme.onSurfaceSecondary,
fontWeight: FontWeight.bold,
),
),
),
),
],
),
const Padding(padding: EdgeInsets.symmetric(horizontal: 10), child: Divider(thickness: 1)),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Padding(
padding: const EdgeInsets.only(left: 10.0),
child: Text(
"server_info_box_server_url".tr(),
style: TextStyle(
fontSize: titleFontSize,
color: context.textTheme.labelSmall?.color,
fontWeight: FontWeight.w500,
),
),
),
),
Expanded(
flex: 0,
child: Container(
width: 200,
padding: const EdgeInsets.only(right: 10.0),
child: Tooltip(
verticalOffset: 0,
decoration: BoxDecoration(
color: context.primaryColor.withValues(alpha: 0.9),
borderRadius: const BorderRadius.all(Radius.circular(10)),
),
textStyle: TextStyle(
color: context.isDarkTheme ? Colors.black : Colors.white,
fontWeight: FontWeight.bold,
),
message: getServerUrl() ?? '--',
preferBelow: false,
triggerMode: TooltipTriggerMode.tap,
child: Text(
getServerUrl() ?? '--',
style: TextStyle(
fontSize: contentFontSize,
color: context.colorScheme.onSurfaceSecondary,
fontWeight: FontWeight.bold,
overflow: TextOverflow.ellipsis,
),
textAlign: TextAlign.end,
),
),
),
),
],
),
const Padding(padding: EdgeInsets.symmetric(horizontal: 10), child: Divider(thickness: 1)),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Padding(
padding: const EdgeInsets.only(left: 10.0),
child: Row(
children: [
if (serverInfoState.versionStatus == VersionStatus.serverOutOfDate)
const Padding(
padding: EdgeInsets.only(right: 5.0),
child: Icon(Icons.info, color: Color.fromARGB(255, 243, 188, 106), size: 12),
),
Text(
"latest_version".tr(),
style: TextStyle(
fontSize: titleFontSize,
color: context.textTheme.labelSmall?.color,
fontWeight: FontWeight.w500,
),
),
],
),
),
),
Expanded(
flex: 0,
child: Padding(
padding: const EdgeInsets.only(right: 10.0),
child: Text(
serverInfoState.latestVersion.major > 0
? "${serverInfoState.latestVersion.major}.${serverInfoState.latestVersion.minor}.${serverInfoState.latestVersion.patch}"
: "--",
style: TextStyle(
fontSize: contentFontSize,
color: context.colorScheme.onSurfaceSecondary,
fontWeight: FontWeight.bold,
),
),
),
),
],
),
],
),
),
),
);
}
}

View file

@ -0,0 +1,85 @@
import 'dart:io';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/constants.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/models/server_info/server_info.model.dart';
import 'package:immich_mobile/providers/server_info.provider.dart';
import 'package:url_launcher/url_launcher_string.dart';
class ServerUpdateNotification extends HookConsumerWidget {
const ServerUpdateNotification({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final serverInfoState = ref.watch(serverInfoProvider);
Color errorColor = const Color.fromARGB(85, 253, 97, 83);
Color infoColor = context.isDarkTheme ? context.primaryColor.withAlpha(55) : context.primaryColor.withAlpha(25);
void openUpdateLink() {
String url;
if (serverInfoState.versionStatus == VersionStatus.serverOutOfDate) {
url = kImmichLatestRelease;
} else {
if (Platform.isIOS) {
url = kImmichAppStoreLink;
} else if (Platform.isAndroid) {
url = kImmichPlayStoreLink;
} else {
// Fallback to latest release for other/unknown platforms
url = kImmichLatestRelease;
}
}
launchUrlString(url, mode: LaunchMode.externalApplication);
}
return SizedBox(
width: double.infinity,
child: Container(
decoration: BoxDecoration(
color: serverInfoState.versionStatus == VersionStatus.error ? errorColor : infoColor,
borderRadius: const BorderRadius.all(Radius.circular(8)),
border: Border.all(
color: serverInfoState.versionStatus == VersionStatus.error
? errorColor
: context.primaryColor.withAlpha(50),
width: 0.75,
),
),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Expanded(
child: Text(
serverInfoState.versionStatus.message,
textAlign: TextAlign.start,
maxLines: 3,
overflow: TextOverflow.ellipsis,
style: context.textTheme.labelLarge,
),
),
if (serverInfoState.versionStatus == VersionStatus.serverOutOfDate ||
serverInfoState.versionStatus == VersionStatus.clientOutOfDate) ...[
const SizedBox(width: 8),
TextButton(
onPressed: openUpdateLink,
style: TextButton.styleFrom(
padding: const EdgeInsets.all(4),
minimumSize: const Size(0, 0),
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
child: serverInfoState.versionStatus == VersionStatus.clientOutOfDate
? Text("action_common_update".tr(context: context))
: Text("view".tr()),
),
],
],
),
),
);
}
}