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
This commit is contained in:
Adam Gastineau
2026-07-24 23:34:27 +05:30
committed by GitHub
parent 829b4e748e
commit 9abe72aced
2 changed files with 140 additions and 40 deletions
+139 -39
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"
)
)
}
)
+1 -1
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"