3 Commits
Author SHA1 Message Date
MineTecandClaude Opus 5.5 5a8fed24bb added ios privacy manifests, fixed permission texts and updated the xcode project
Privacy manifests for the app, widget and share extension declare the
App Group UserDefaults access (CA92.1, 1C8F.1) required for App Store
submission. Photo library and camera usage texts corrected. Project
format and SwiftPM pins (Firebase 12.19) as updated by Xcode 27 / Flutter
3.47.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 12:51:28 +02:00
MineTecandClaude Opus 5.5 b413fe05de fixed timetable ruler and day header overflowing at large system text sizes
Both have fixed geometry, so their text scaling is capped at 1.3. Ruler
times shrink instead of wrapping into the period number and are only
shown when they fit at the current scale.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 12:51:28 +02:00
MineTecandClaude Opus 5.5 38a651f916 fixed missing period times in the ios widget ruler and unreadable tiles on tinted and clear home screens
A .systemLarge widget only gets ~27pt per period, below the 30pt threshold
shared with Android, so iOS now shows the times from 24pt with a tighter
cell. In non-full-color rendering modes the opaque tiles and the today
circle flattened into solid slabs hiding their text; they are drawn
translucent with an outline there and use the light-text palette.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 12:51:28 +02:00
12 changed files with 195 additions and 58 deletions

No files matched your search

+9 -6
View File
@@ -3,11 +3,12 @@
archiveVersion = 1;
classes = {
};
objectVersion = 54;
objectVersion = 70;
objects = {
/* Begin PBXBuildFile section */
034BD2FF7A860C6DC2FED514 /* Pods_Share_Extension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4F2428AC5384E0EF8DAB462A /* Pods_Share_Extension.framework */; };
0B36AA42294848D99E4E0742 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = AFE007D963441C0650CD3ED9 /* PrivacyInfo.xcprivacy */; };
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
3321F80F2FB1C00C0011C712 /* Share Extension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 3321F8052FB1C00C0011C712 /* Share Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
33FDB0982EE9ABDC000B2391 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 33FDB0972EE9ABDC000B2391 /* GoogleService-Info.plist */; };
@@ -77,19 +78,19 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
12B96C78930441C73F123636 /* Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; name = Info.plist; path = Info.plist; sourceTree = "<group>"; };
12B96C78930441C73F123636 /* Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
17E2EE012C4361AC05DCA4C9 /* NotificationServiceExtension-Release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "NotificationServiceExtension-Release.xcconfig"; path = "Flutter/NotificationServiceExtension-Release.xcconfig"; sourceTree = "<group>"; };
2960F029246C39E2A03F6D87 /* NotificationService.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NotificationService.swift; path = NotificationService.swift; sourceTree = "<group>"; };
2960F029246C39E2A03F6D87 /* NotificationService.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = "<group>"; };
3321F8052FB1C00C0011C712 /* Share Extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "Share Extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; };
33FDB0972EE9ABDC000B2391 /* GoogleService-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = "<group>"; };
36C6C71327C68B43F522F5B2 /* NotificationServiceExtension-Debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "NotificationServiceExtension-Debug.xcconfig"; path = "Flutter/NotificationServiceExtension-Debug.xcconfig"; sourceTree = "<group>"; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
4509EC31CB08BA9BF367AF6C /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
4F2428AC5384E0EF8DAB462A /* Pods_Share_Extension.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Share_Extension.framework; sourceTree = BUILT_PRODUCTS_DIR; };
509DCCD474353408FE5806C5 /* PEM.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = PEM.swift; path = PEM.swift; sourceTree = "<group>"; };
5C2F4C79DD573778092882AB /* NotificationServiceExtension.entitlements */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.entitlements; name = NotificationServiceExtension.entitlements; path = NotificationServiceExtension.entitlements; sourceTree = "<group>"; };
509DCCD474353408FE5806C5 /* PEM.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PEM.swift; sourceTree = "<group>"; };
5C2F4C79DD573778092882AB /* NotificationServiceExtension.entitlements */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.entitlements; path = NotificationServiceExtension.entitlements; sourceTree = "<group>"; };
60E1803A3FB28FCC6F435E99 /* Pods-Share Extension.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Share Extension.release.xcconfig"; path = "Target Support Files/Pods-Share Extension/Pods-Share Extension.release.xcconfig"; sourceTree = "<group>"; };
64801C012A9112D500E8B558 /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = "<group>"; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
@@ -107,6 +108,7 @@
AA0101020000000011111111 /* TimetableWidgetExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = TimetableWidgetExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
AA0102020000000022222222 /* SceneDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
AA6B03D1433E7395021F7730 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
AFE007D963441C0650CD3ED9 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
BB0001010000000011111111 /* ShareExtension-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "ShareExtension-Debug.xcconfig"; path = "Flutter/ShareExtension-Debug.xcconfig"; sourceTree = "<group>"; };
BB0001020000000011111111 /* ShareExtension-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "ShareExtension-Release.xcconfig"; path = "Flutter/ShareExtension-Release.xcconfig"; sourceTree = "<group>"; };
BB0001030000000011111111 /* ShareExtension-Profile.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "ShareExtension-Profile.xcconfig"; path = "Flutter/ShareExtension-Profile.xcconfig"; sourceTree = "<group>"; };
@@ -222,7 +224,6 @@
12B96C78930441C73F123636 /* Info.plist */,
5C2F4C79DD573778092882AB /* NotificationServiceExtension.entitlements */,
);
name = NotificationServiceExtension;
path = NotificationServiceExtension;
sourceTree = SOURCE_ROOT;
};
@@ -304,6 +305,7 @@
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
AA0102020000000022222222 /* SceneDelegate.swift */,
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
AFE007D963441C0650CD3ED9 /* PrivacyInfo.xcprivacy */,
);
path = Runner;
sourceTree = "<group>";
@@ -469,6 +471,7 @@
33FDB0982EE9ABDC000B2391 /* GoogleService-Info.plist in Resources */,
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
0B36AA42294848D99E4E0742 /* PrivacyInfo.xcprivacy in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -50,8 +50,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/firebase/firebase-ios-sdk",
"state" : {
"revision" : "42e81d245e30e49ea6a5830cf2842d44a1591270",
"version" : "12.15.0"
"revision" : "27eaab3918e0bf78711cf1abf240577176326432",
"version" : "12.19.0"
}
},
{
@@ -59,8 +59,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/googleads/google-ads-on-device-conversion-ios-sdk",
"state" : {
"revision" : "dc39082d8881109d35b94b1c122164c0e8d08a55",
"version" : "3.6.1"
"revision" : "0208e2681ec81f8a9c81084696f60fcce26cb7ee",
"version" : "3.7.0"
}
},
{
@@ -68,8 +68,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/GoogleAppMeasurement.git",
"state" : {
"revision" : "144855f40d8668927f256a3045f7fdc4c3f4338b",
"version" : "12.15.0"
"revision" : "b8455dd2ff55cc269fda7a4f9eac6c1e3588c53f",
"version" : "12.19.2"
}
},
{
@@ -86,8 +86,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/GoogleUtilities.git",
"state" : {
"revision" : "9f183ae842be978784f2963a343682e0c46d8fb3",
"version" : "8.1.2"
"revision" : "92c8f6dc3ac375d6febdfcb3db68bc3d10633db3",
"version" : "8.1.3"
}
},
{
+2 -2
View File
@@ -42,9 +42,9 @@
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSCameraUsageDescription</key>
<string>Um Fotos direkt aus der App aufnehmen und teilen zu können wird Zugriff auf die Kamera benötigt.</string>
<string>Um Fotos direkt aus der App aufnehmen und teilen zu können, wird Zugriff auf die Kamera benötigt.</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>Um Medien mit anderen zu teilen wird Zugriff zu deine Dateien benötigt.</string>
<string>Um Fotos und Videos mit anderen zu teilen, wird Zugriff auf deine Mediathek benötigt.</string>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
+24
View File
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSPrivacyTracking</key>
<false/>
<key>NSPrivacyTrackingDomains</key>
<array/>
<key>NSPrivacyCollectedDataTypes</key>
<array/>
<key>NSPrivacyAccessedAPITypes</key>
<array>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>CA92.1</string>
<string>1C8F.1</string>
</array>
</dict>
</array>
</dict>
</plist>
+24
View File
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSPrivacyTracking</key>
<false/>
<key>NSPrivacyTrackingDomains</key>
<array/>
<key>NSPrivacyCollectedDataTypes</key>
<array/>
<key>NSPrivacyAccessedAPITypes</key>
<array>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>CA92.1</string>
<string>1C8F.1</string>
</array>
</dict>
</array>
</dict>
</plist>
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSPrivacyTracking</key>
<false/>
<key>NSPrivacyTrackingDomains</key>
<array/>
<key>NSPrivacyCollectedDataTypes</key>
<array/>
<key>NSPrivacyAccessedAPITypes</key>
<array>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>CA92.1</string>
<string>1C8F.1</string>
</array>
</dict>
</array>
</dict>
</plist>
@@ -147,7 +147,8 @@ struct PeriodRuler: View {
.font(.system(size: timeFont))
.foregroundStyle(palette.textSecondary)
.lineLimit(1)
.padding(.vertical, 2)
.minimumScaleFactor(0.8)
.padding(.vertical, 1)
}
}
.frame(width: RULER_WIDTH, height: height)
@@ -304,6 +305,8 @@ private struct LessonTile: View {
let metrics: TileMetrics
let layout: TileTextLayout
@Environment(\.widgetRenderingMode) private var renderingMode
private static let cancelledRed =
Color(red: 244 / 255, green: 67 / 255, blue: 54 / 255).opacity(200 / 255)
@@ -326,7 +329,7 @@ private struct LessonTile: View {
.padding(.vertical, TILE_PADDING_V)
.frame(maxWidth: .infinity, alignment: .topLeading)
.frame(height: height, alignment: .topLeading)
.background(lesson.tileColor)
.background { tileBackground(shape) }
.clipShape(shape)
.overlay(alignment: .bottomTrailing) {
if let count = lesson.siblingCount, count > 0 {
@@ -357,6 +360,22 @@ private struct LessonTile: View {
.padding(.horizontal, TILE_MARGIN_H)
}
/// Tinted / clear home screens flatten an opaque colour block into a
/// solid slab the white text disappears on; a translucent fill with an
/// outline keeps tile and text apart in every rendering mode.
@ViewBuilder
private func tileBackground(_ shape: RoundedRectangle) -> some View {
if renderingMode == .fullColor {
lesson.tileColor
} else {
ZStack {
shape.fill(Color.white.opacity(0.18))
shape.strokeBorder(Color.white.opacity(0.55), lineWidth: 1)
}
.widgetAccentable()
}
}
private var titleRow: some View {
HStack(alignment: .center, spacing: 3) {
Text(layout.title)
@@ -47,7 +47,10 @@ let TITLE_WRAP_BELOW_FONT: CGFloat = 8
let BADGE_WIDTH: CGFloat = 16
let BADGE_HEIGHT: CGFloat = 11
let RULER_TIMES_MIN_CELL: CGFloat = 30
/// Lower than Android's 30dp: a .systemLarge widget only gets ~27pt per
/// period, so iOS packs the ruler cell tighter (see PeriodRuler.cell)
/// instead of dropping the times.
let RULER_TIMES_MIN_CELL: CGFloat = 24
let BREAK_ICON_MIN_HEIGHT: CGFloat = 12
/// Space the holiday column keeps free above and below the rotated name for
@@ -4,6 +4,7 @@ import WidgetKit
struct TimetableWeekView: View {
let entry: TimetableEntry
@Environment(\.widgetPalette) private var palette
@Environment(\.widgetRenderingMode) private var renderingMode
var body: some View {
ZStack {
@@ -115,13 +116,23 @@ struct TimetableWeekView: View {
.lineLimit(1)
Text("\(Calendar.current.component(.day, from: day))")
.font(.system(size: 11, weight: isToday ? .bold : .regular))
.foregroundStyle(isToday ? palette.onAccent : palette.textPrimary)
.foregroundStyle(isToday ? todayNumberColor : palette.textPrimary)
.lineLimit(1)
.frame(width: 18, height: 18)
.background(Circle().fill(isToday ? palette.accent : Color.clear))
.background(Circle().fill(isToday ? todayCircleColor : Color.clear))
}
}
// An opaque accent circle flattens to the same shade as its number on
// tinted / clear home screens; keep the circle translucent there.
private var todayCircleColor: Color {
renderingMode == .fullColor ? palette.accent : Color.white.opacity(0.3)
}
private var todayNumberColor: Color {
renderingMode == .fullColor ? palette.onAccent : palette.textPrimary
}
private func placeholder(_ message: String) -> some View {
VStack(spacing: 4) {
Text("Marianum Stundenplan")
@@ -201,12 +201,15 @@ private struct WidgetSurface<Content: View>: View {
let entry: TimetableEntry
@ViewBuilder let content: () -> Content
@Environment(\.colorScheme) private var colorScheme
@Environment(\.widgetRenderingMode) private var renderingMode
var body: some View {
let palette = WidgetPalette.resolve(
themeMode: entry.themeMode,
colorScheme: colorScheme
)
// Tinted / clear home screens map every colour to its luminance, so
// the light palette's near-black text would vanish — use the dark
// palette's light text there regardless of the theme setting.
let palette = renderingMode == .fullColor
? WidgetPalette.resolve(themeMode: entry.themeMode, colorScheme: colorScheme)
: .dark
return AnyView(
background(content: content(), palette: palette)
.environment(\.widgetPalette, palette)
@@ -13,17 +13,19 @@ class _DayHeaderStrip extends StatelessWidget {
});
@override
Widget build(BuildContext context) => Row(
children: [
SizedBox(width: rulerWidth),
for (var d = 0; d < 5; d++)
Expanded(
child: _DayHeaderCell(
date: weekStart.addDays(d),
today: today,
// Fixed-height strip with a 28px day circle: unclamped accessibility
// sizes overflow it.
Widget build(BuildContext context) => MediaQuery.withClampedTextScaling(
maxScaleFactor: 1.3,
child: Row(
children: [
SizedBox(width: rulerWidth),
for (var d = 0; d < 5; d++)
Expanded(
child: _DayHeaderCell(date: weekStart.addDays(d), today: today),
),
),
],
],
),
);
}
@@ -95,20 +95,24 @@ class _PeriodRuler extends StatelessWidget {
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return SizedBox(
width: width,
child: Stack(
clipBehavior: Clip.none,
children: [
for (final period in schedule.periods)
Positioned(
top: layout.topOf(period),
height: layout.heightOf(period),
left: 0,
right: 0,
child: _PeriodLabel(period: period, theme: theme),
),
],
// Fixed ruler width and period heights: past this the labels overlap.
return MediaQuery.withClampedTextScaling(
maxScaleFactor: 1.3,
child: SizedBox(
width: width,
child: Stack(
clipBehavior: Clip.none,
children: [
for (final period in schedule.periods)
Positioned(
top: layout.topOf(period),
height: layout.heightOf(period),
left: 0,
right: 0,
child: _PeriodLabel(period: period, theme: theme),
),
],
),
),
);
}
@@ -155,7 +159,11 @@ class _PeriodLabel extends StatelessWidget {
return LayoutBuilder(
builder: (context, constraints) {
final showTimes = constraints.maxHeight >= 38;
// Both times plus the number, at the current text scale (9 + 9 + 14
// plus the 3px insets and some air — 38px at scale 1).
final scaler = MediaQuery.textScalerOf(context);
final needed = 2 * scaler.scale(9) + scaler.scale(14) + 6;
final showTimes = constraints.maxHeight >= needed;
return DecoratedBox(
decoration: BoxDecoration(
border: Border(top: BorderSide(color: dividerColor, width: 0.5)),
@@ -169,11 +177,10 @@ class _PeriodLabel extends StatelessWidget {
top: 3,
left: 0,
right: 0,
child: Text(
child: _timeText(
_format(period.start),
style: timeStyle,
textAlign: TextAlign.center,
textHeightBehavior: tightTextHeight,
timeStyle,
tightTextHeight,
),
),
Text(
@@ -191,11 +198,10 @@ class _PeriodLabel extends StatelessWidget {
bottom: 3,
left: 0,
right: 0,
child: Text(
child: _timeText(
_format(period.end),
style: timeStyle,
textAlign: TextAlign.center,
textHeightBehavior: tightTextHeight,
timeStyle,
tightTextHeight,
),
),
],
@@ -205,6 +211,24 @@ class _PeriodLabel extends StatelessWidget {
);
}
/// The ruler is narrow: a scaled-up time must shrink, never wrap into the
/// period number.
static Widget _timeText(
String text,
TextStyle? style,
TextHeightBehavior heightBehavior,
) => FittedBox(
fit: BoxFit.scaleDown,
child: Text(
text,
style: style,
maxLines: 1,
softWrap: false,
textAlign: TextAlign.center,
textHeightBehavior: heightBehavior,
),
);
static String _format(TimeOfDay t) =>
'${t.hour.toString().padLeft(2, '0')}:${t.minute.toString().padLeft(2, '0')}';
}