feat(ui): show a system-format clock on TV home and in the player
The clock renders through the existing formatClockTime helper driven by MediaQuery.alwaysUse24HourFormatOf, so it follows the OS 12/24-hour setting instead of introducing an app preference. It re-arms a one-shot timer onto each wall-clock minute boundary rather than polling, and resyncs on resume because a suspended process runs no timers. The player header is shared by the mobile and desktop/TV controls, so one insertion point covers every form factor: the player is fullscreen everywhere, so it never has an OS clock to defer to. Home is the exception and only gets one on TV, where a leanback app hides the system clock; a phone status bar and a desktop menu bar already show the time.
This commit is contained in:
@@ -22,6 +22,7 @@ import '../utils/content_utils.dart';
|
||||
import '../widgets/cycling_media_backdrop.dart';
|
||||
import '../widgets/optimized_media_image.dart' show ClearLogoImage, blurArtwork;
|
||||
import '../widgets/toolbar_scrim.dart';
|
||||
import '../widgets/system_clock.dart';
|
||||
import '../providers/discover_provider.dart';
|
||||
import '../providers/multi_server_provider.dart';
|
||||
import '../providers/watch_state_store.dart';
|
||||
@@ -748,6 +749,14 @@ class _DiscoverScreenState extends State<DiscoverScreen>
|
||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(color: foregroundColor, fontWeight: .bold),
|
||||
),
|
||||
const Spacer(),
|
||||
// TV only: a fullscreen leanback app hides the system clock, while a
|
||||
// phone status bar and a desktop menu bar already show one.
|
||||
if (PlatformDetector.isTV()) ...[
|
||||
SystemClock(
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(color: foregroundColor, fontWeight: .w500),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
],
|
||||
Consumer2<WatchTogetherProvider, CompanionRemoteProvider>(
|
||||
builder: (context, watchTogether, companionRemote, _) {
|
||||
final isDesktop = PlatformDetector.shouldActAsRemoteHost(context);
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../utils/formatters.dart';
|
||||
|
||||
/// Live wall-clock label that follows the system 12/24-hour preference.
|
||||
///
|
||||
/// The displayed value comes from [formatClockTime] with
|
||||
/// `MediaQuery.alwaysUse24HourFormatOf`, so it matches the format every other
|
||||
/// time-of-day label in the app uses and flips as soon as the OS setting does.
|
||||
///
|
||||
/// Ticking is a one-shot timer re-armed onto each wall-clock minute boundary
|
||||
/// rather than a one-second poll: one rebuild per minute, and the minute never
|
||||
/// flips a fraction of a second late. A suspended process runs no timers, so
|
||||
/// the clock also resynchronises on resume instead of waiting out the boundary
|
||||
/// that elapsed while it was away.
|
||||
class SystemClock extends StatefulWidget {
|
||||
const SystemClock({super.key, this.style, this.now = DateTime.now});
|
||||
|
||||
/// Text style for the clock label. The two chrome surfaces that host it use
|
||||
/// different foregrounds, so the caller owns colour and size.
|
||||
final TextStyle? style;
|
||||
|
||||
/// Wall-clock source. Overridden only by tests, which need the minute
|
||||
/// rollover to be deterministic.
|
||||
final DateTime Function() now;
|
||||
|
||||
@override
|
||||
State<SystemClock> createState() => _SystemClockState();
|
||||
}
|
||||
|
||||
class _SystemClockState extends State<SystemClock> with WidgetsBindingObserver {
|
||||
Timer? _tick;
|
||||
late DateTime _now;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
_now = widget.now();
|
||||
_scheduleTick();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_tick?.cancel();
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
if (state == AppLifecycleState.resumed) _tickNow();
|
||||
}
|
||||
|
||||
void _scheduleTick() {
|
||||
final now = _now;
|
||||
final nextMinute = DateTime(now.year, now.month, now.day, now.hour, now.minute).add(const Duration(minutes: 1));
|
||||
_tick?.cancel();
|
||||
_tick = Timer(nextMinute.difference(now), _tickNow);
|
||||
}
|
||||
|
||||
void _tickNow() {
|
||||
if (!mounted) return;
|
||||
setState(() => _now = widget.now());
|
||||
_scheduleTick();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Text(
|
||||
formatClockTime(_now, is24Hour: MediaQuery.alwaysUse24HourFormatOf(context)),
|
||||
style: widget.style,
|
||||
maxLines: 1,
|
||||
softWrap: false,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import '../../../i18n/strings.g.dart';
|
||||
import '../../../watch_together/widgets/watch_together_overlay.dart';
|
||||
import '../../../watch_together/providers/watch_together_provider.dart';
|
||||
import '../../app_bar_back_button.dart';
|
||||
import '../../system_clock.dart';
|
||||
|
||||
/// Header layout style for video controls
|
||||
enum VideoHeaderStyle {
|
||||
@@ -68,6 +69,12 @@ class VideoControlsHeader extends StatelessWidget {
|
||||
);
|
||||
},
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 8),
|
||||
child: SystemClock(
|
||||
style: const TextStyle(color: Colors.white70, fontSize: 14, fontWeight: .w500),
|
||||
),
|
||||
),
|
||||
?trailing,
|
||||
],
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user