Compare commits

...

4 Commits

Author SHA1 Message Date
renovate[bot]
1f94cb47b8 chore(deps): update dependency java to v25 2026-07-24 18:38:07 +00:00
shenlong
7b023d9a71 chore: pump flutter to 3.44.8 (#30205)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-07-24 18:32:13 +00:00
Adam Gastineau
9abe72aced fix(mobile): align iOS widget text box with widget's corners (#30176)
* fix(mobile): align iOS widget text box with widget's corners

* Improve image centering and fix formatting

* Remove duplicate preview
2026-07-24 23:34:27 +05:30
shenlong
829b4e748e feat: button longpress handler (#30200)
* feat: button longpress handler

* review suggestion

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-07-24 23:34:21 +05:30
11 changed files with 248 additions and 154 deletions

View File

@@ -184,33 +184,6 @@ checksum = "sha256:b5e1d2a1ad3c03229ddc89823848f4a1c11f9c6402a51fa26f0aaa5f1d7a2
url = "https://github.com/WebAssembly/binaryen/releases/download/version_124/binaryen-version_124-x86_64-windows.tar.gz"
url_api = "https://api.github.com/repos/WebAssembly/binaryen/releases/assets/288925833"
[[tools.java]]
version = "21.0.2"
backend = "core:java"
[tools.java.options]
shorthand_vendor = "openjdk"
[tools.java."platforms.linux-arm64"]
checksum = "sha256:08db1392a48d4eb5ea5315cf8f18b89dbaf36cda663ba882cf03c704c9257ec2"
url = "https://download.java.net/java/GA/jdk21.0.2/f2283984656d49d69e91c558476027ac/13/GPL/openjdk-21.0.2_linux-aarch64_bin.tar.gz"
[tools.java."platforms.linux-x64"]
checksum = "sha256:a2def047a73941e01a73739f92755f86b895811afb1f91243db214cff5bdac3f"
url = "https://download.java.net/java/GA/jdk21.0.2/f2283984656d49d69e91c558476027ac/13/GPL/openjdk-21.0.2_linux-x64_bin.tar.gz"
[tools.java."platforms.macos-arm64"]
checksum = "sha256:b3d588e16ec1e0ef9805d8a696591bd518a5cea62567da8f53b5ce32d11d22e4"
url = "https://download.java.net/java/GA/jdk21.0.2/f2283984656d49d69e91c558476027ac/13/GPL/openjdk-21.0.2_macos-aarch64_bin.tar.gz"
[tools.java."platforms.macos-x64"]
checksum = "sha256:8fd09e15dc406387a0aba70bf5d99692874e999bf9cd9208b452b5d76ac922d3"
url = "https://download.java.net/java/GA/jdk21.0.2/f2283984656d49d69e91c558476027ac/13/GPL/openjdk-21.0.2_macos-x64_bin.tar.gz"
[tools.java."platforms.windows-x64"]
checksum = "sha256:b6c17e747ae78cdd6de4d7532b3164b277daee97c007d3eaa2b39cca99882664"
url = "https://download.java.net/java/GA/jdk21.0.2/f2283984656d49d69e91c558476027ac/13/GPL/openjdk-21.0.2_windows-x64_bin.zip"
[[tools.node]]
version = "24.15.0"
backend = "core:node"

View File

@@ -23,7 +23,7 @@ opentofu = "1.12.4"
"github:extism/cli" = "1.6.3"
"github:webassembly/binaryen" = "version_124"
"github:extism/js-pdk" = "1.6.0"
java = "21.0.2"
java = "25.0.3+9.0.LTS"
[tools."github:jellyfin/jellyfin-ffmpeg"]
version = "7.1.3-6"

View File

@@ -17,50 +17,67 @@ struct ImmichWidgetView: View {
var entry: ImageEntry
var body: some View {
if entry.image == nil {
Image("LaunchImage")
.tintedWidgetImageModifier()
.overlay(alignment: .bottom) {
if let error = entry.metadata.error?.errorDescription {
Text(error)
.minimumScaleFactor(0.25)
.multilineTextAlignment(.center)
.foregroundStyle(.secondary)
.fixedSize()
.alignmentGuide(.bottom) { dimensions in
// Place the text below the bottom of the image
dimensions[.top] - 8
}
}
}
if let image = entry.image {
ImmichWidgetContentView(image: image, subtitle: entry.metadata.subtitle, deepLink: entry.metadata.deepLink)
} else {
ZStack(alignment: .leading) {
Color.clear.overlay(
Image(uiImage: entry.image!)
.resizable()
.tintedWidgetImageModifier()
.scaledToFill()
)
VStack {
Spacer()
if let subtitle = entry.metadata.subtitle {
Text(subtitle)
.foregroundColor(.white)
.padding(8)
.background(Color.black.opacity(0.6))
.cornerRadius(8)
.font(.system(size: 16))
}
}
.padding(16)
}
.widgetURL(entry.metadata.deepLink)
ImmichWidgetLoadingView(message: entry.metadata.error?.errorDescription)
}
}
}
private struct ImmichWidgetLoadingView: View {
let message: String?
var body: some View {
let messageText = Text(message ?? "")
.minimumScaleFactor(0.25)
.multilineTextAlignment(.center)
.foregroundStyle(.secondary)
VStack(spacing: 8) {
// This is used as a nicer way to center the image, rather than using offsets
messageText.hidden()
Image("LaunchImage")
.tintedWidgetImageModifier()
messageText
}
}
}
private struct ImmichWidgetContentView: View {
let image: UIImage
let subtitle: String?
let deepLink: URL?
var body: some View {
ZStack(alignment: .leading) {
Color.clear.overlay(
Image(uiImage: image)
.resizable()
.tintedWidgetImageModifier()
.scaledToFill()
)
VStack {
Spacer()
if let subtitle {
Text(subtitle)
.foregroundColor(.white)
.padding(6)
.background(ContainerRelativeShape().fill(Color.black.opacity(0.6)))
.font(.system(size: 16))
}
}
.padding(16)
}
.widgetURL(deepLink)
}
}
#Preview(
"Medium",
as: .systemMedium,
widget: {
ImmichRandomWidget()
@@ -69,10 +86,93 @@ struct ImmichWidgetView: View {
let date = Date()
ImageEntry(
date: date,
image: UIImage(named: "ImmichLogo"),
image: UIImage(named: "LaunchImage"),
metadata: EntryMetadata(
subtitle: "1 year ago"
)
)
}
)
#Preview(
"Medium No Data",
as: .systemMedium,
widget: {
ImmichRandomWidget()
},
timeline: {
let date = Date()
ImageEntry(
date: date,
image: nil
)
}
)
#Preview(
"Medium No Data Error",
as: .systemMedium,
widget: {
ImmichRandomWidget()
},
timeline: {
let date = Date()
ImageEntry(
date: date,
image: nil,
metadata: EntryMetadata(error: WidgetError.fetchFailed)
)
}
)
#Preview(
"Small",
as: .systemSmall,
widget: {
ImmichRandomWidget()
},
timeline: {
let date = Date()
ImageEntry(
date: date,
image: UIImage(named: "LaunchImage"),
metadata: EntryMetadata(
subtitle: "Yesterday"
)
)
}
)
#Preview(
"Small No Data Error",
as: .systemSmall,
widget: {
ImmichRandomWidget()
},
timeline: {
let date = Date()
ImageEntry(
date: date,
image: nil,
metadata: EntryMetadata(error: WidgetError.fetchFailed)
)
}
)
#Preview(
"Large",
as: .systemLarge,
widget: {
ImmichRandomWidget()
},
timeline: {
let date = Date()
ImageEntry(
date: date,
image: UIImage(named: "LaunchImage"),
metadata: EntryMetadata(
subtitle: "2000 seconds ago"
)
)
}
)

View File

@@ -25,7 +25,7 @@ extension WidgetError: LocalizedError {
return "Login to Immich"
case .fetchFailed:
return "Unable to connect to your Immich instance"
return "Unable to connect to Immich"
case .albumNotFound:
return "Album not found"

View File

@@ -1,29 +1,30 @@
# @generated - this file is auto-generated by `mise lock` https://mise.en.dev/dev-tools/mise-lock.html
[[tools."aqua:flutter/flutter"]]
version = "3.44.6"
version = "3.44.8"
backend = "aqua:flutter/flutter"
[tools."aqua:flutter/flutter"."platforms.linux-arm64"]
url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.44.6-stable.tar.xz"
url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.44.8-stable.tar.xz"
[tools."aqua:flutter/flutter"."platforms.linux-arm64-musl"]
url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.44.6-stable.tar.xz"
url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.44.8-stable.tar.xz"
[tools."aqua:flutter/flutter"."platforms.linux-x64"]
url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.44.6-stable.tar.xz"
url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.44.8-stable.tar.xz"
[tools."aqua:flutter/flutter"."platforms.linux-x64-musl"]
url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.44.6-stable.tar.xz"
url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.44.8-stable.tar.xz"
[tools."aqua:flutter/flutter"."platforms.macos-arm64"]
url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/macos/flutter_macos_arm64_3.44.6-stable.zip"
checksum = "blake3:79c780876c64f5a66c015845f544bdb20275f7f3bf819e6c831f23df3e96b8db"
url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/macos/flutter_macos_arm64_3.44.8-stable.zip"
[tools."aqua:flutter/flutter"."platforms.macos-x64"]
url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/macos/flutter_macos_3.44.6-stable.zip"
url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/macos/flutter_macos_3.44.8-stable.zip"
[tools."aqua:flutter/flutter"."platforms.windows-x64"]
url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/windows/flutter_windows_3.44.6-stable.zip"
url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/windows/flutter_windows_3.44.8-stable.zip"
[[tools."github:CQLabs/homebrew-dcm"]]
version = "1.37.0"
@@ -133,54 +134,3 @@ asset_pattern = "dcm-macos-arm-release.zip"
checksum = "sha256:30bede64367d09067093cc57af6ec9496d7717898138ded5cb98a16ac8dd9d93"
url = "https://github.com/CQLabs/homebrew-dcm/releases/download/1.37.0/dcm-macos-arm-release.zip"
url_api = "https://api.github.com/repos/CQLabs/homebrew-dcm/releases/assets/404543757"
[[tools.java]]
version = "21.0.2"
backend = "core:java"
[tools.java."platforms.linux-arm64"]
checksum = "sha256:08db1392a48d4eb5ea5315cf8f18b89dbaf36cda663ba882cf03c704c9257ec2"
url = "https://download.java.net/java/GA/jdk21.0.2/f2283984656d49d69e91c558476027ac/13/GPL/openjdk-21.0.2_linux-aarch64_bin.tar.gz"
[tools.java."platforms.linux-x64"]
checksum = "sha256:a2def047a73941e01a73739f92755f86b895811afb1f91243db214cff5bdac3f"
url = "https://download.java.net/java/GA/jdk21.0.2/f2283984656d49d69e91c558476027ac/13/GPL/openjdk-21.0.2_linux-x64_bin.tar.gz"
[tools.java."platforms.macos-arm64"]
checksum = "sha256:b3d588e16ec1e0ef9805d8a696591bd518a5cea62567da8f53b5ce32d11d22e4"
url = "https://download.java.net/java/GA/jdk21.0.2/f2283984656d49d69e91c558476027ac/13/GPL/openjdk-21.0.2_macos-aarch64_bin.tar.gz"
[tools.java."platforms.macos-x64"]
checksum = "sha256:8fd09e15dc406387a0aba70bf5d99692874e999bf9cd9208b452b5d76ac922d3"
url = "https://download.java.net/java/GA/jdk21.0.2/f2283984656d49d69e91c558476027ac/13/GPL/openjdk-21.0.2_macos-x64_bin.tar.gz"
[tools.java."platforms.windows-x64"]
checksum = "sha256:b6c17e747ae78cdd6de4d7532b3164b277daee97c007d3eaa2b39cca99882664"
url = "https://download.java.net/java/GA/jdk21.0.2/f2283984656d49d69e91c558476027ac/13/GPL/openjdk-21.0.2_windows-x64_bin.zip"
[[tools.java]]
version = "21.0.2"
backend = "core:java"
[tools.java.options]
shorthand_vendor = "openjdk"
[tools.java."platforms.linux-arm64"]
checksum = "sha256:08db1392a48d4eb5ea5315cf8f18b89dbaf36cda663ba882cf03c704c9257ec2"
url = "https://download.java.net/java/GA/jdk21.0.2/f2283984656d49d69e91c558476027ac/13/GPL/openjdk-21.0.2_linux-aarch64_bin.tar.gz"
[tools.java."platforms.linux-x64"]
checksum = "sha256:a2def047a73941e01a73739f92755f86b895811afb1f91243db214cff5bdac3f"
url = "https://download.java.net/java/GA/jdk21.0.2/f2283984656d49d69e91c558476027ac/13/GPL/openjdk-21.0.2_linux-x64_bin.tar.gz"
[tools.java."platforms.macos-arm64"]
checksum = "sha256:b3d588e16ec1e0ef9805d8a696591bd518a5cea62567da8f53b5ce32d11d22e4"
url = "https://download.java.net/java/GA/jdk21.0.2/f2283984656d49d69e91c558476027ac/13/GPL/openjdk-21.0.2_macos-aarch64_bin.tar.gz"
[tools.java."platforms.macos-x64"]
checksum = "sha256:8fd09e15dc406387a0aba70bf5d99692874e999bf9cd9208b452b5d76ac922d3"
url = "https://download.java.net/java/GA/jdk21.0.2/f2283984656d49d69e91c558476027ac/13/GPL/openjdk-21.0.2_macos-x64_bin.tar.gz"
[tools.java."platforms.windows-x64"]
checksum = "sha256:b6c17e747ae78cdd6de4d7532b3164b277daee97c007d3eaa2b39cca99882664"
url = "https://download.java.net/java/GA/jdk21.0.2/f2283984656d49d69e91c558476027ac/13/GPL/openjdk-21.0.2_windows-x64_bin.zip"

View File

@@ -1,6 +1,6 @@
[tools]
"aqua:flutter/flutter" = "3.44.6"
java = "21.0.2"
"aqua:flutter/flutter" = "3.44.8"
java = "25.0.3+9.0.LTS"
[tools."github:CQLabs/homebrew-dcm"]
version = "1.37.0"

View File

@@ -1,4 +1,5 @@
import 'dart:async';
import 'dart:core';
import 'package:flutter/material.dart';
import 'package:immich_ui/src/constants.dart';
@@ -8,6 +9,7 @@ class ImmichColumnButton extends StatefulWidget {
final IconData icon;
final String label;
final FutureOr<void> Function() onPressed;
final FutureOr<void> Function()? onLongPress;
final bool disabled;
final bool? loading;
@@ -16,6 +18,7 @@ class ImmichColumnButton extends StatefulWidget {
required this.icon,
required this.label,
required this.onPressed,
this.onLongPress,
this.disabled = false,
this.loading,
});
@@ -25,26 +28,44 @@ class ImmichColumnButton extends StatefulWidget {
}
class _ImmichColumnButtonState extends State<ImmichColumnButton> {
bool _loading = false;
bool get _isLoading => widget.loading ?? _loading;
bool _running = false;
bool get _isLoading => widget.loading ?? _running;
bool get _isDisabled => widget.disabled || _isLoading;
Future<void> _onPressed() async {
setState(() => _loading = true);
Future<void> _runAction(FutureOr<void> Function() action) async {
setState(() => _running = true);
try {
await widget.onPressed();
await action();
} finally {
if (mounted) {
setState(() => _loading = false);
setState(() => _running = false);
}
}
}
Future<void>? _onPressed() {
if (_isDisabled) {
return null;
}
return _runAction(widget.onPressed);
}
Future<void>? _onLongPress() {
if (_isDisabled || widget.onLongPress == null) {
return null;
}
return _runAction(widget.onLongPress!);
}
@override
Widget build(BuildContext context) {
final foreground = context.colorOverride ?? Theme.of(context).colorScheme.onSurface;
return TextButton(
onPressed: widget.disabled || _isLoading ? null : _onPressed,
onPressed: _onPressed,
onLongPress: _onLongPress,
style: TextButton.styleFrom(
foregroundColor: foreground,
padding: const .symmetric(horizontal: ImmichSpacing.sm, vertical: ImmichSpacing.md),

View File

@@ -7,6 +7,7 @@ import 'package:immich_ui/src/internal.dart';
class ImmichIconButton extends StatefulWidget {
final IconData icon;
final FutureOr<void> Function() onPressed;
final FutureOr<void> Function()? onLongPress;
final ImmichVariant variant;
final ImmichColor color;
final bool disabled;
@@ -16,6 +17,7 @@ class ImmichIconButton extends StatefulWidget {
super.key,
required this.icon,
required this.onPressed,
this.onLongPress,
this.color = .primary,
this.variant = .filled,
this.disabled = false,
@@ -27,20 +29,37 @@ class ImmichIconButton extends StatefulWidget {
}
class _ImmichIconButtonState extends State<ImmichIconButton> {
bool _loading = false;
bool get _isLoading => widget.loading ?? _loading;
bool _running = false;
bool get _isLoading => widget.loading ?? _running;
bool get _isDisabled => widget.disabled || _isLoading;
Future<void> _onPressed() async {
setState(() => _loading = true);
Future<void> _runAction(FutureOr<void> Function() action) async {
setState(() => _running = true);
try {
await widget.onPressed();
await action();
} finally {
if (mounted) {
setState(() => _loading = false);
setState(() => _running = false);
}
}
}
Future<void>? _onPressed() {
if (_isDisabled) {
return null;
}
return _runAction(widget.onPressed);
}
Future<void>? _onLongPress() {
if (_isDisabled || widget.onLongPress == null) {
return null;
}
return _runAction(widget.onLongPress!);
}
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
@@ -73,7 +92,8 @@ class _ImmichIconButtonState extends State<ImmichIconButton> {
child: CircularProgressIndicator(strokeWidth: ImmichBorderWidth.md),
)
: Icon(widget.icon),
onPressed: widget.disabled || _isLoading ? null : _onPressed,
onPressed: _onPressed,
onLongPress: _onLongPress,
style: IconButton.styleFrom(backgroundColor: background, foregroundColor: foreground),
);
}

View File

@@ -7,6 +7,7 @@ class ImmichTextButton extends StatefulWidget {
final String labelText;
final IconData? icon;
final FutureOr<void> Function() onPressed;
final FutureOr<void> Function()? onLongPress;
final ImmichVariant variant;
final bool expanded;
final bool disabled;
@@ -17,6 +18,7 @@ class ImmichTextButton extends StatefulWidget {
required this.labelText,
this.icon,
required this.onPressed,
this.onLongPress,
this.variant = .filled,
this.expanded = true,
@@ -29,20 +31,37 @@ class ImmichTextButton extends StatefulWidget {
}
class _ImmichTextButtonState extends State<ImmichTextButton> {
bool _loading = false;
bool get _isLoading => widget.loading ?? _loading;
bool _running = false;
bool get _isLoading => widget.loading ?? _running;
bool get _isDisabled => widget.disabled || _isLoading;
Future<void> _onPressed() async {
setState(() => _loading = true);
Future<void> _runAction(FutureOr<void> Function() action) async {
setState(() => _running = true);
try {
await widget.onPressed();
await action();
} finally {
if (mounted) {
setState(() => _loading = false);
setState(() => _running = false);
}
}
}
Future<void>? _onPressed() {
if (_isDisabled) {
return null;
}
return _runAction(widget.onPressed);
}
Future<void>? _onLongPress() {
if (_isDisabled || widget.onLongPress == null) {
return null;
}
return _runAction(widget.onLongPress!);
}
@override
Widget build(BuildContext context) {
final Widget? icon = _isLoading
@@ -59,11 +78,22 @@ class _ImmichTextButtonState extends State<ImmichTextButton> {
style: const .new(fontSize: ImmichTextSize.body, fontWeight: .bold),
);
final style = ElevatedButton.styleFrom(padding: const .symmetric(vertical: ImmichSpacing.md));
final onPressed = widget.disabled || _isLoading ? null : _onPressed;
final button = switch (widget.variant) {
ImmichVariant.filled => ElevatedButton.icon(style: style, onPressed: onPressed, icon: icon, label: label),
ImmichVariant.ghost => TextButton.icon(style: style, onPressed: onPressed, icon: icon, label: label),
ImmichVariant.filled => ElevatedButton.icon(
style: style,
onPressed: _onPressed,
onLongPress: _onLongPress,
icon: icon,
label: label,
),
ImmichVariant.ghost => TextButton.icon(
style: style,
onPressed: _onPressed,
onLongPress: _onLongPress,
icon: icon,
label: label,
),
};
if (widget.expanded) {

View File

@@ -2014,4 +2014,4 @@ packages:
version: "3.1.3"
sdks:
dart: ">=3.12.0 <4.0.0"
flutter: "3.44.6"
flutter: "3.44.8"

View File

@@ -6,7 +6,7 @@ version: 3.0.3+3056
environment:
sdk: '>=3.12.0 <4.0.0'
flutter: 3.44.6
flutter: 3.44.8
dependencies:
async: ^2.13.1