jellyfin_status_strip.dart 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178
  1. import 'dart:async';
  2. import 'package:flutter/material.dart';
  3. import 'package:intl/intl.dart';
  4. import 'package:provider/provider.dart';
  5. import '../../../core/theme/app_colors.dart';
  6. import '../../../core/widgets/app_icon.dart';
  7. import '../../../core/widgets/pressable.dart';
  8. import '../../home/gps_speed_controller.dart';
  9. import '../../home/obd_controller.dart';
  10. import '../../home/telemetry_settings_controller.dart';
  11. import '../../home/weather_controller.dart';
  12. /// Time, speed, temperature and a Home shortcut for the Jellyfin pages.
  13. class JellyfinStatusStrip extends StatefulWidget {
  14. const JellyfinStatusStrip({super.key});
  15. @override
  16. State<JellyfinStatusStrip> createState() => _JellyfinStatusStripState();
  17. }
  18. class _JellyfinStatusStripState extends State<JellyfinStatusStrip> {
  19. late DateTime _now = DateTime.now();
  20. Timer? _timer;
  21. @override
  22. void initState() {
  23. super.initState();
  24. _scheduleTick();
  25. }
  26. void _scheduleTick() {
  27. _timer?.cancel();
  28. final now = DateTime.now();
  29. final next = DateTime(
  30. now.year,
  31. now.month,
  32. now.day,
  33. now.hour,
  34. now.minute,
  35. ).add(const Duration(minutes: 1));
  36. _timer = Timer(next.difference(now) + const Duration(milliseconds: 80), () {
  37. if (!mounted) return;
  38. setState(() => _now = DateTime.now());
  39. _scheduleTick();
  40. });
  41. }
  42. @override
  43. void dispose() {
  44. _timer?.cancel();
  45. super.dispose();
  46. }
  47. @override
  48. Widget build(BuildContext context) {
  49. final colors = context.colors;
  50. final theme = Theme.of(context);
  51. final telemetry = context.watch<TelemetrySettingsController>();
  52. final obd = context.watch<ObdController>();
  53. final gps = context.watch<GpsSpeedController>();
  54. final weather = context.watch<WeatherController>();
  55. final useObdSpeed =
  56. telemetry.speedEnabled && telemetry.speedUseObd && obd.hasTelemetry;
  57. final speed = !telemetry.speedEnabled
  58. ? null
  59. : useObdSpeed
  60. ? obd.speedKmh
  61. : gps.speedKmh;
  62. final useObdTemperature =
  63. telemetry.temperatureUseObd && obd.ambientCelsius != null;
  64. final temperature = !telemetry.temperatureEnabled
  65. ? null
  66. : useObdTemperature
  67. ? obd.ambientCelsius
  68. : (weather.hasTemperature ? weather.temperatureCelsius : null);
  69. final locale = Localizations.localeOf(context).toLanguageTag();
  70. final use24Hour = MediaQuery.alwaysUse24HourFormatOf(context);
  71. final time = (use24Hour ? DateFormat.Hm(locale) : DateFormat.jm(locale))
  72. .format(_now);
  73. return Container(
  74. key: const ValueKey('jellyfin-status-strip'),
  75. padding: const EdgeInsets.fromLTRB(16, 8, 8, 8),
  76. decoration: BoxDecoration(
  77. color: colors.surface.withValues(alpha: 0.82),
  78. borderRadius: BorderRadius.circular(999),
  79. border: Border.all(color: colors.border),
  80. ),
  81. child: Row(
  82. mainAxisSize: MainAxisSize.min,
  83. children: [
  84. Text(
  85. time,
  86. style: theme.textTheme.labelLarge?.copyWith(
  87. color: colors.ink,
  88. fontSize: 16,
  89. fontFeatures: const [FontFeature.tabularFigures()],
  90. ),
  91. ),
  92. const SizedBox(width: 14),
  93. _StripMetric(
  94. iconAsset: 'assets/icons/gauge.svg',
  95. text: speed == null ? '-- km/h' : '${speed.round()} km/h',
  96. available: speed != null,
  97. ),
  98. const SizedBox(width: 14),
  99. _StripMetric(
  100. iconAsset: 'assets/icons/thermometer.svg',
  101. text: temperature == null ? '--°C' : '${temperature.round()}°C',
  102. available: temperature != null,
  103. ),
  104. const SizedBox(width: 10),
  105. Pressable(
  106. onTap: () =>
  107. Navigator.of(context).popUntil((route) => route.isFirst),
  108. semanticLabel: 'Go to home',
  109. pressedScale: 0.92,
  110. child: Container(
  111. width: 42,
  112. height: 42,
  113. decoration: BoxDecoration(
  114. color: colors.surfaceStrong,
  115. shape: BoxShape.circle,
  116. border: Border.all(color: colors.border),
  117. ),
  118. alignment: Alignment.center,
  119. child: AppIcon(
  120. 'assets/icons/home.svg',
  121. size: 21,
  122. color: colors.ink,
  123. ),
  124. ),
  125. ),
  126. ],
  127. ),
  128. );
  129. }
  130. }
  131. class _StripMetric extends StatelessWidget {
  132. const _StripMetric({
  133. required this.iconAsset,
  134. required this.text,
  135. required this.available,
  136. });
  137. final String iconAsset;
  138. final String text;
  139. final bool available;
  140. @override
  141. Widget build(BuildContext context) {
  142. final colors = context.colors;
  143. final theme = Theme.of(context);
  144. return Row(
  145. mainAxisSize: MainAxisSize.min,
  146. children: [
  147. AppIcon(
  148. iconAsset,
  149. size: 18,
  150. color: available ? colors.inkMuted : colors.inkFaint,
  151. ),
  152. const SizedBox(width: 6),
  153. Text(
  154. text,
  155. style: theme.textTheme.labelLarge?.copyWith(
  156. color: available ? colors.ink : colors.inkFaint,
  157. fontFeatures: const [FontFeature.tabularFigures()],
  158. ),
  159. ),
  160. ],
  161. );
  162. }
  163. }