From c81509eff4b0a1d2a4a050591cdcb29c50911e8e Mon Sep 17 00:00:00 2001 From: Bruno D'Luka Date: Tue, 2 Apr 2024 12:45:07 -0300 Subject: [PATCH 1/6] feat: Store the raw event published date and display the date to the user with the correct timezone offset --- lib/api/events.dart | 16 +++-- lib/models/event.dart | 69 ++++++++++++------- lib/providers/settings_provider.dart | 32 +++++++-- lib/screens/events_browser/events_screen.dart | 1 + .../events_browser/events_screen_desktop.dart | 3 +- lib/screens/players/event_player_desktop.dart | 23 ++++--- lib/screens/settings/application.dart | 20 ++++++ lib/utils/date.dart | 23 +++++++ lib/utils/extensions.dart | 7 +- 9 files changed, 144 insertions(+), 50 deletions(-) create mode 100644 lib/utils/date.dart diff --git a/lib/api/events.dart b/lib/api/events.dart index 42cba4a4..d1268a40 100644 --- a/lib/api/events.dart +++ b/lib/api/events.dart @@ -93,7 +93,7 @@ extension EventsExtension on API { events = ((jsonDecode(response.body) as Map)['entry'] as Iterable) .cast() .map((eventObject) { - final published = DateTime.parse(eventObject['published']).toLocal(); + final published = DateTime.parse(eventObject['published']); final event = Event( server: server, id: () { @@ -115,10 +115,12 @@ extension EventsExtension on API { '-1', ), title: eventObject['title'], + publishedRaw: eventObject['published'], published: published, + updatedRaw: eventObject['updated'] ?? eventObject['published'], updated: eventObject['updated'] == null ? published - : DateTime.parse(eventObject['updated']).toLocal(), + : DateTime.parse(eventObject['updated']), category: eventObject['category']['term'], mediaID: eventObject.containsKey('content') ? int.parse(eventObject['content']['media_id']) @@ -147,12 +149,14 @@ extension EventsExtension on API { deviceID: int.parse((e['category']['term'] as String).split('/').first), title: e['title']['\$t'], + publishedRaw: e['published']['\$t'], published: e['published'] == null || e['published']['\$t'] == null - ? DateTime.now().toLocal() - : DateTime.parse(e['published']['\$t']).toLocal(), + ? DateTime.now() + : DateTime.parse(e['published']['\$t']), + updatedRaw: e['updated']['\$t'] ?? e['published']['\$t'], updated: e['updated'] == null || e['updated']['\$t'] == null - ? DateTime.now().toLocal() - : DateTime.parse(e['updated']['\$t']).toLocal(), + ? DateTime.now() + : DateTime.parse(e['updated']['\$t']), category: e['category']['term'], mediaID: !e.containsKey('content') ? null diff --git a/lib/models/event.dart b/lib/models/event.dart index 191e8661..1760fbd6 100644 --- a/lib/models/event.dart +++ b/lib/models/event.dart @@ -29,7 +29,9 @@ class Event { final int id; final int deviceID; final String title; + final String publishedRaw; final DateTime published; + final String updatedRaw; final DateTime updated; final String? category; final int? mediaID; @@ -40,7 +42,9 @@ class Event { required this.id, required this.deviceID, required this.title, + required this.publishedRaw, required this.published, + required this.updatedRaw, required this.updated, required this.category, required this.mediaID, @@ -52,13 +56,17 @@ class Event { this.id = 1, this.deviceID = 1, this.title = '', + String? publishedRaw, DateTime? published, + String? updatedRaw, DateTime? updated, this.category, this.mediaID, this.mediaURL, }) : server = server ?? ServersProvider.instance.servers.first, + publishedRaw = publishedRaw ?? DateTime.now().toIso8601String(), published = published ?? DateTime.now(), + updatedRaw = updatedRaw ?? DateTime.now().toIso8601String(), updated = updated ?? DateTime.now(); String get deviceName { @@ -86,7 +94,9 @@ class Event { other.id == id && other.deviceID == deviceID && other.title == title && + other.publishedRaw == publishedRaw && other.published == published && + other.updatedRaw == updatedRaw && other.updated == updated && other.category == category && other.mediaID == mediaID && @@ -99,7 +109,9 @@ class Event { id.hashCode ^ deviceID.hashCode ^ title.hashCode ^ + publishedRaw.hashCode ^ published.hashCode ^ + updatedRaw.hashCode ^ updated.hashCode ^ category.hashCode ^ mediaID.hashCode ^ @@ -107,31 +119,8 @@ class Event { } @override - String toString() => - 'Event($id, $deviceID, $title, $published, $updated, $category, $mediaID, $mediaURL)'; - - Event copyWith( - Server? server, - int? id, - int? deviceID, - String? title, - DateTime? published, - DateTime? updated, - String? category, - int? mediaID, - Uri? mediaURL, - ) { - return Event( - server: server ?? this.server, - deviceID: deviceID ?? this.deviceID, - id: id ?? this.id, - title: title ?? this.title, - published: published ?? this.published, - updated: updated ?? this.updated, - category: category ?? this.category, - mediaID: mediaID ?? this.mediaID, - mediaURL: mediaURL ?? this.mediaURL, - ); + String toString() { + return 'Event(server: $server, id: $id, deviceID: $deviceID, title: $title, publishedRaw: $publishedRaw, published: $published, updatedRaw: $updatedRaw, updated: $updated, category: $category, mediaID: $mediaID, mediaURL: $mediaURL)'; } Map toJson() => { @@ -152,7 +141,9 @@ class Event { deviceID: json['deviceID'], id: json['id'], title: json['title'], + publishedRaw: json['published'], published: DateTime.parse(json['published']), + updatedRaw: json['updated'], updated: DateTime.parse(json['updated']), category: json['category'], mediaID: json['mediaID'], @@ -210,6 +201,34 @@ class Event { return EventType.unknown; } } + + Event copyWith({ + Server? server, + int? id, + int? deviceID, + String? title, + String? publishedRaw, + DateTime? published, + String? updatedRaw, + DateTime? updated, + String? category, + int? mediaID, + Uri? mediaURL, + }) { + return Event( + server: server ?? this.server, + id: id ?? this.id, + deviceID: deviceID ?? this.deviceID, + title: title ?? this.title, + publishedRaw: publishedRaw ?? this.publishedRaw, + published: published ?? this.published, + updatedRaw: updatedRaw ?? this.updatedRaw, + updated: updated ?? this.updated, + category: category ?? this.category, + mediaID: mediaID ?? this.mediaID, + mediaURL: mediaURL ?? this.mediaURL, + ); + } } enum EventPriority { diff --git a/lib/providers/settings_provider.dart b/lib/providers/settings_provider.dart index fa8904df..a681741e 100644 --- a/lib/providers/settings_provider.dart +++ b/lib/providers/settings_provider.dart @@ -55,6 +55,10 @@ class _SettingsOption { }); } + T call() { + return value; + } + final T? min; final T? max; @@ -292,6 +296,10 @@ class SettingsProvider extends UnityProvider { def: DateFormat('hh:mm a'), key: 'application.time_format', ); + final kConvertTimeToLocalTimezone = _SettingsOption( + def: true, + key: 'application.convert_time_to_local_timezone', + ); // Window final kLaunchAppOnStartup = _SettingsOption( @@ -417,6 +425,7 @@ class SettingsProvider extends UnityProvider { kLanguageCode.loadData(data), kDateFormat.loadData(data), kTimeFormat.loadData(data), + kConvertTimeToLocalTimezone.loadData(data), kLaunchAppOnStartup.loadData(data), kMinimizeToTray.loadData(data), kAnimationsEnabled.loadData(data), @@ -486,6 +495,8 @@ class SettingsProvider extends UnityProvider { kLanguageCode.key: kLanguageCode.saveAs(kLanguageCode.value), kDateFormat.key: kDateFormat.saveAs(kDateFormat.value), kTimeFormat.key: kTimeFormat.saveAs(kTimeFormat.value), + kConvertTimeToLocalTimezone.key: kConvertTimeToLocalTimezone + .saveAs(kConvertTimeToLocalTimezone.value), kLaunchAppOnStartup.key: kLaunchAppOnStartup.saveAs(kLaunchAppOnStartup.value), kMinimizeToTray.key: kMinimizeToTray.saveAs(kMinimizeToTray.value), @@ -522,8 +533,8 @@ class SettingsProvider extends UnityProvider { /// Formats the date according to the current [dateFormat]. /// /// [toLocal] defines if the date will be converted to local time. Defaults to `true` - String formatDate(DateTime date, {bool toLocal = false}) { - if (toLocal) date = date.toLocal(); + String formatDate(DateTime date) { + if (kConvertTimeToLocalTimezone()) date = date.toLocal(); return kDateFormat.value.format(date); } @@ -531,10 +542,21 @@ class SettingsProvider extends UnityProvider { /// Formats the date according to the current [dateFormat]. /// /// [toLocal] defines if the date will be converted to local time. Defaults to `true` - String formatTime(DateTime time, {bool toLocal = false}) { - if (toLocal) time = time.toLocal(); + String formatTime( + DateTime time, { + DateFormat? pattern, + bool withSeconds = false, + bool? toLocal, + }) { + if (toLocal ?? kConvertTimeToLocalTimezone()) time = time.toLocal(); + + pattern ??= DateFormat(kTimeFormat.value.pattern); + + if (withSeconds) { + pattern = pattern.add_s(); + } - return kTimeFormat.value.format(time); + return pattern.format(time); } void toggleCycling() { diff --git a/lib/screens/events_browser/events_screen.dart b/lib/screens/events_browser/events_screen.dart index 8b1e291c..9bfd7dc1 100644 --- a/lib/screens/events_browser/events_screen.dart +++ b/lib/screens/events_browser/events_screen.dart @@ -32,6 +32,7 @@ import 'package:bluecherry_client/screens/events_browser/filter.dart'; import 'package:bluecherry_client/screens/events_browser/sidebar.dart'; import 'package:bluecherry_client/screens/players/event_player_desktop.dart'; import 'package:bluecherry_client/utils/constants.dart'; +import 'package:bluecherry_client/utils/date.dart'; import 'package:bluecherry_client/utils/extensions.dart'; import 'package:bluecherry_client/utils/methods.dart'; import 'package:bluecherry_client/widgets/desktop_buttons.dart'; diff --git a/lib/screens/events_browser/events_screen_desktop.dart b/lib/screens/events_browser/events_screen_desktop.dart index bd5d12c5..615bf8a9 100644 --- a/lib/screens/events_browser/events_screen_desktop.dart +++ b/lib/screens/events_browser/events_screen_desktop.dart @@ -107,7 +107,8 @@ class EventsScreenDesktop extends StatelessWidget { ), _buildTilePart( child: Text( - '${settings.formatDate(event.updated)} ${settings.formatTime(event.updated).toUpperCase()}', + '${settings.formatDate(timezoneAwareDate(event.publishedRaw))}' + ' ${settings.formatTime(timezoneAwareDate(event.publishedRaw)).toUpperCase()}', ), flex: 2, ), diff --git a/lib/screens/players/event_player_desktop.dart b/lib/screens/players/event_player_desktop.dart index 626ba0cc..9fe6244f 100644 --- a/lib/screens/players/event_player_desktop.dart +++ b/lib/screens/players/event_player_desktop.dart @@ -27,6 +27,7 @@ import 'package:bluecherry_client/providers/downloads_provider.dart'; import 'package:bluecherry_client/providers/settings_provider.dart'; import 'package:bluecherry_client/screens/downloads/indicators.dart'; import 'package:bluecherry_client/screens/layouts/video_status_label.dart'; +import 'package:bluecherry_client/utils/date.dart'; import 'package:bluecherry_client/utils/extensions.dart'; import 'package:bluecherry_client/widgets/collapsable_sidebar.dart'; import 'package:bluecherry_client/widgets/desktop_buttons.dart'; @@ -172,10 +173,7 @@ class _EventPlayerDesktopState extends State { data: SliderThemeData(trackShape: _CustomTrackShape()), child: Material( child: Column(children: [ - WindowButtons( - title: title, - showNavigator: false, - ), + WindowButtons(title: title, showNavigator: false), Expanded( child: Row(children: [ Expanded( @@ -245,8 +243,12 @@ class _EventPlayerDesktopState extends State { snapshot.data ?? videoController.currentPos; return Row(children: [ Text( - DateFormat.Hms() - .format(currentEvent.published.add(pos)), + settings.formatTime( + timezoneAwareDate(currentEvent.publishedRaw) + .add(pos), + pattern: DateFormat.Hms(), + toLocal: false, + ), ), padd, Expanded( @@ -296,8 +298,11 @@ class _EventPlayerDesktopState extends State { ), padd, Text( - DateFormat.Hms().format( - currentEvent.published.add(duration), + settings.formatTime( + timezoneAwareDate(currentEvent.publishedRaw) + .add(duration), + pattern: DateFormat.Hms(), + toLocal: false, ), ), padd, @@ -490,7 +495,7 @@ class EventTile extends StatelessWidget { final loc = AppLocalizations.of(context); final eventType = event.type.locale(context).uppercaseFirst; - final at = settings.formatDate(event.published); + final at = settings.formatDate(timezoneAwareDate(event.publishedRaw)); return SizedBox( width: double.infinity, diff --git a/lib/screens/settings/application.dart b/lib/screens/settings/application.dart index 706a9b66..5b6a19ba 100644 --- a/lib/screens/settings/application.dart +++ b/lib/screens/settings/application.dart @@ -68,6 +68,26 @@ class ApplicationSettings extends StatelessWidget { const LanguageSection(), const DateFormatSection(), const TimeFormatSection(), + CheckboxListTile.adaptive( + value: settings.kConvertTimeToLocalTimezone.value, + onChanged: (v) { + if (v != null) { + settings.kConvertTimeToLocalTimezone.value = v; + } + }, + contentPadding: DesktopSettings.horizontalPadding, + secondary: CircleAvatar( + backgroundColor: Colors.transparent, + foregroundColor: theme.iconTheme.color, + child: const Icon(Icons.history_toggle_off), + ), + title: const Text('Convert dates to local timezone'), + subtitle: const Text( + 'Whether to convert dates to the local timezone. This will affect the ' + 'date and time displayed in the app. This is useful if you are in a ' + 'different timezone than the server.', + ), + ), if (settings.kShowDebugInfo.value) ...[ const SubHeader('Window'), CheckboxListTile.adaptive( diff --git a/lib/utils/date.dart b/lib/utils/date.dart new file mode 100644 index 00000000..5130f973 --- /dev/null +++ b/lib/utils/date.dart @@ -0,0 +1,23 @@ +import 'package:flutter/foundation.dart'; + +/// Convert a date string to a DateTime object, considering the timezone offset. +DateTime timezoneAwareDate(String originalDateString) { + final originalDateTime = DateTime.parse(originalDateString); + + try { + final offsetString = originalDateString.split('-').last; + final parts = offsetString.split(':'); + + // Convert hours and minutes strings to integers + final hours = int.parse(parts[0]); + final minutes = int.parse(parts[1]); + + // Create a Duration object based on the offset sign + final offset = Duration(hours: -hours, minutes: -minutes); + + return originalDateTime.add(offset); + } catch (e) { + debugPrint('Failed to parse date string: $originalDateString'); + return originalDateTime; + } +} diff --git a/lib/utils/extensions.dart b/lib/utils/extensions.dart index 49aaeb9b..4c331974 100644 --- a/lib/utils/extensions.dart +++ b/lib/utils/extensions.dart @@ -132,7 +132,8 @@ extension ServerExtension on List { extension DateTimeExtension on DateTime { /// Returns true if this date is between [first] and [second] /// - /// If [allowSameMoment] is true, then the date can be equal to [first] or [second]. + /// If [allowSameMoment] is true, then the date can be equal to [first] or + /// [second]. bool isInBetween( DateTime first, DateTime second, { @@ -142,9 +143,7 @@ extension DateTimeExtension on DateTime { toLocal().isBefore(second.toLocal()); if (allowSameMoment) return isBetween; - return isBetween || - toLocal().isAtSameMomentAs(first.toLocal()) || - toLocal().isAtSameMomentAs(second.toLocal()); + return isBetween || isAtSameMomentAs(first) || isAtSameMomentAs(second); } } From 58ed4f4544af2c1e1c23db6be61f3c3e64c89c9c Mon Sep 17 00:00:00 2001 From: Bruno D'Luka Date: Tue, 2 Apr 2024 12:56:11 -0300 Subject: [PATCH 2/6] feat: Correctly apply "local time" option --- lib/providers/settings_provider.dart | 18 ++++++++++++++++-- .../events_browser/events_screen_desktop.dart | 4 ++-- lib/screens/settings/application.dart | 8 ++++++-- 3 files changed, 24 insertions(+), 6 deletions(-) diff --git a/lib/providers/settings_provider.dart b/lib/providers/settings_provider.dart index a681741e..622bab04 100644 --- a/lib/providers/settings_provider.dart +++ b/lib/providers/settings_provider.dart @@ -23,6 +23,7 @@ import 'package:bluecherry_client/providers/app_provider_interface.dart'; import 'package:bluecherry_client/providers/downloads_provider.dart'; import 'package:bluecherry_client/providers/update_provider.dart'; import 'package:bluecherry_client/screens/events_timeline/desktop/timeline.dart'; +import 'package:bluecherry_client/utils/date.dart'; import 'package:bluecherry_client/utils/storage.dart'; import 'package:bluecherry_client/utils/video_player.dart'; import 'package:flutter/foundation.dart'; @@ -131,6 +132,11 @@ class _SettingsOption { _value = (await getDefault?.call()) ?? def; } } + + @override + String toString() { + return 'SettingsOption<$T>($key: $_value)'; + } } class SettingsProvider extends UnityProvider { @@ -297,7 +303,7 @@ class SettingsProvider extends UnityProvider { key: 'application.time_format', ); final kConvertTimeToLocalTimezone = _SettingsOption( - def: true, + def: false, key: 'application.convert_time_to_local_timezone', ); @@ -534,11 +540,19 @@ class SettingsProvider extends UnityProvider { /// /// [toLocal] defines if the date will be converted to local time. Defaults to `true` String formatDate(DateTime date) { - if (kConvertTimeToLocalTimezone()) date = date.toLocal(); + if (kConvertTimeToLocalTimezone.value) date = date.toLocal(); return kDateFormat.value.format(date); } + String formatRawTime(String rawDate) { + return kTimeFormat.value.format( + kConvertTimeToLocalTimezone.value + ? DateTime.parse(rawDate).toLocal() + : timezoneAwareDate(rawDate), + ); + } + /// Formats the date according to the current [dateFormat]. /// /// [toLocal] defines if the date will be converted to local time. Defaults to `true` diff --git a/lib/screens/events_browser/events_screen_desktop.dart b/lib/screens/events_browser/events_screen_desktop.dart index 615bf8a9..149d9d0c 100644 --- a/lib/screens/events_browser/events_screen_desktop.dart +++ b/lib/screens/events_browser/events_screen_desktop.dart @@ -107,8 +107,8 @@ class EventsScreenDesktop extends StatelessWidget { ), _buildTilePart( child: Text( - '${settings.formatDate(timezoneAwareDate(event.publishedRaw))}' - ' ${settings.formatTime(timezoneAwareDate(event.publishedRaw)).toUpperCase()}', + '${settings.formatDate(event.published)}' + ' ${settings.formatRawTime(event.publishedRaw).toUpperCase()}', ), flex: 2, ), diff --git a/lib/screens/settings/application.dart b/lib/screens/settings/application.dart index 5b6a19ba..dd98d056 100644 --- a/lib/screens/settings/application.dart +++ b/lib/screens/settings/application.dart @@ -66,6 +66,10 @@ class ApplicationSettings extends StatelessWidget { }, ), const LanguageSection(), + const SubHeader( + 'Date and Time', + padding: DesktopSettings.horizontalPadding, + ), const DateFormatSection(), const TimeFormatSection(), CheckboxListTile.adaptive( @@ -270,7 +274,7 @@ class DateFormatSection extends StatelessWidget { title: loc.dateFormat, description: loc.dateFormatDescription, icon: Icons.calendar_month, - value: '', + value: settings.kDateFormat.value.pattern, values: formats.map((format) { return Option( value: format.pattern, @@ -299,7 +303,7 @@ class TimeFormatSection extends StatelessWidget { title: loc.timeFormat, description: loc.timeFormatDescription, icon: Icons.hourglass_empty, - value: '', + value: settings.kTimeFormat.value.pattern, values: patterns.map((pattern) { return Option( value: pattern.pattern, From 2936a75ee08acbc01834d1335cc031faccdf2dfd Mon Sep 17 00:00:00 2001 From: Bruno D'Luka Date: Tue, 2 Apr 2024 13:04:03 -0300 Subject: [PATCH 3/6] feat: Localization for date and time settings --- lib/l10n/app_en.arb | 4 ++++ lib/l10n/app_fr.arb | 4 ++++ lib/l10n/app_pl.arb | 4 ++++ lib/l10n/app_pt.arb | 4 ++++ lib/screens/settings/application.dart | 19 ++++++++++--------- 5 files changed, 26 insertions(+), 9 deletions(-) diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 299a2d89..0dbe28ff 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -480,15 +480,19 @@ "firstEventInitialPoint": "First event", "hourAgoInitialPoint": "1 hour ago", "@@APPLICATION": {}, + "appearance": "Appearance", "theme": "Theme", "themeDescription": "Change the appearance of the app", "system": "System", "light": "Light", "dark": "Dark", + "dateAndTime": "Date and Time", "dateFormat": "Date Format", "dateFormatDescription": "What format to use for displaying dates", "timeFormat": "Time Format", "timeFormatDescription": "What format to use for displaying time", + "convertToLocalTime": "Convert dates to the local timezone", + "convertToLocalTimeDescription": "This will affect the date and time displayed in the app. This is useful when you are in a different timezone than the server. When disabled, the server timezone will be used.", "@@PRIVACY_AND_SECURITY": {}, "privacyAndSecurity": "Privacy and Security", "allowDataCollection": "Allow Bluecherry to collect usage data", diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 6007bb65..453dd559 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -456,15 +456,19 @@ "firstEventInitialPoint": "First event", "hourAgoInitialPoint": "1 hour ago", "@@APPLICATION": {}, + "appearance": "Appearance", "theme": "Thème", "themeDescription": "Modifier l'apparence de l'application", "system": "Système", "light": "Clair", "dark": "Sombre", + "dateAndTime": "Date and Time", "dateFormat": "Format de la date", "dateFormatDescription": "What format to use for displaying dates", "timeFormat": "Format de l'heure", "timeFormatDescription": "What format to use for displaying time", + "convertToLocalTime": "Convert dates to the local timezone", + "convertToLocalTimeDescription": "Convert all dates to the local timezone. This will affect the date and time displayed in the app. This is useful when you are in a different timezone than the server.", "@@PRIVACY_AND_SECURITY": {}, "privacyAndSecurity": "Privacy and Security", "allowDataCollection": "Allow Bluecherry to collect usage data", diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index 754f634a..9bd4ee3b 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -480,15 +480,19 @@ "firstEventInitialPoint": "First event", "hourAgoInitialPoint": "1 hour ago", "@@APPLICATION": {}, + "appearance": "Appearance", "theme": "Motyw", "themeDescription": "Zmień wygląd aplikacji", "system": "Systemowy", "light": "Jasny", "dark": "Ciemny", + "dateAndTime": "Date and Time", "dateFormat": "Format daty", "dateFormatDescription": "What format to use for displaying dates", "timeFormat": "Format czasu", "timeFormatDescription": "What format to use for displaying time", + "convertToLocalTime": "Convert dates to the local timezone", + "convertToLocalTimeDescription": "This will affect the date and time displayed in the app. This is useful when you are in a different timezone than the server. When disabled, the server timezone will be used.", "@@PRIVACY_AND_SECURITY": {}, "privacyAndSecurity": "Privacy and Security", "allowDataCollection": "Allow Bluecherry to collect usage data", diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index b1bfe1a9..1fbf1cc9 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -480,15 +480,19 @@ "firstEventInitialPoint": "Primeiro evento", "hourAgoInitialPoint": "1 hora atrás", "@@APPLICATION": {}, + "appearance": "Appearance", "theme": "Aparência", "themeDescription": "Mude a aparência do aplicativo", "system": "Padrão do Sistema", "light": "Claro", "dark": "Escuro", + "dateAndTime": "Date and Time", "dateFormat": "Formato da Data", "dateFormatDescription": "Qual formato usar para exibir datas", "timeFormat": "Formato de Hora", "timeFormatDescription": "Qual formato usar para exibir horas", + "convertToLocalTime": "Convert dates to the local timezone", + "convertToLocalTimeDescription": "This will affect the date and time displayed in the app. This is useful when you are in a different timezone than the server. When disabled, the server timezone will be used.", "@@PRIVACY_AND_SECURITY": {}, "privacyAndSecurity": "Privacidade e Segurança", "allowDataCollection": "Permitir que Bluecherry colete dados de uso", diff --git a/lib/screens/settings/application.dart b/lib/screens/settings/application.dart index dd98d056..b464b9f4 100644 --- a/lib/screens/settings/application.dart +++ b/lib/screens/settings/application.dart @@ -36,7 +36,11 @@ class ApplicationSettings extends StatelessWidget { final loc = AppLocalizations.of(context); final theme = Theme.of(context); final settings = context.watch(); - return ListView(padding: DesktopSettings.verticalPadding, children: [ + return ListView(children: [ + SubHeader( + loc.appearance, + padding: DesktopSettings.horizontalPadding, + ), OptionsChooserTile( title: loc.theme, description: loc.themeDescription, @@ -66,8 +70,8 @@ class ApplicationSettings extends StatelessWidget { }, ), const LanguageSection(), - const SubHeader( - 'Date and Time', + SubHeader( + loc.dateAndTime, padding: DesktopSettings.horizontalPadding, ), const DateFormatSection(), @@ -85,12 +89,9 @@ class ApplicationSettings extends StatelessWidget { foregroundColor: theme.iconTheme.color, child: const Icon(Icons.history_toggle_off), ), - title: const Text('Convert dates to local timezone'), - subtitle: const Text( - 'Whether to convert dates to the local timezone. This will affect the ' - 'date and time displayed in the app. This is useful if you are in a ' - 'different timezone than the server.', - ), + title: Text(loc.convertToLocalTime), + subtitle: Text(loc.convertToLocalTimeDescription), + isThreeLine: true, ), if (settings.kShowDebugInfo.value) ...[ const SubHeader('Window'), From 1061f349965ae51ca55aaa0c1f921d79d5347526 Mon Sep 17 00:00:00 2001 From: Bruno D'Luka Date: Tue, 2 Apr 2024 13:16:11 -0300 Subject: [PATCH 4/6] feat: Correctly convert date and time on downloads --- lib/providers/settings_provider.dart | 6 ++++++ lib/screens/downloads/downloads_manager.dart | 2 +- lib/screens/events_browser/events_screen.dart | 1 - 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/providers/settings_provider.dart b/lib/providers/settings_provider.dart index 622bab04..71065ad3 100644 --- a/lib/providers/settings_provider.dart +++ b/lib/providers/settings_provider.dart @@ -573,6 +573,12 @@ class SettingsProvider extends UnityProvider { return pattern.format(time); } + String formatRawDateAndTime(String rawDateTime) { + final date = formatDate(DateTime.parse(rawDateTime)); + final time = formatRawTime(rawDateTime); + return '$date $time'; + } + void toggleCycling() { kLayoutCycleEnabled.value = !kLayoutCycleEnabled.value; save(); diff --git a/lib/screens/downloads/downloads_manager.dart b/lib/screens/downloads/downloads_manager.dart index 56274823..6cf335b3 100644 --- a/lib/screens/downloads/downloads_manager.dart +++ b/lib/screens/downloads/downloads_manager.dart @@ -155,7 +155,7 @@ class _DownloadTileState extends State { final settings = context.watch(); final eventType = widget.event.type.locale(context).uppercaseFirst; - final at = settings.formatDate(widget.event.published); + final at = settings.formatRawDateAndTime(widget.event.publishedRaw); final shape = RoundedRectangleBorder( borderRadius: BorderRadius.circular(8.0), diff --git a/lib/screens/events_browser/events_screen.dart b/lib/screens/events_browser/events_screen.dart index 9bfd7dc1..8b1e291c 100644 --- a/lib/screens/events_browser/events_screen.dart +++ b/lib/screens/events_browser/events_screen.dart @@ -32,7 +32,6 @@ import 'package:bluecherry_client/screens/events_browser/filter.dart'; import 'package:bluecherry_client/screens/events_browser/sidebar.dart'; import 'package:bluecherry_client/screens/players/event_player_desktop.dart'; import 'package:bluecherry_client/utils/constants.dart'; -import 'package:bluecherry_client/utils/date.dart'; import 'package:bluecherry_client/utils/extensions.dart'; import 'package:bluecherry_client/utils/methods.dart'; import 'package:bluecherry_client/widgets/desktop_buttons.dart'; From 5eba7da99ff9d36facb5875cb14c74512dd2540a Mon Sep 17 00:00:00 2001 From: Bruno D'Luka Date: Tue, 2 Apr 2024 13:24:27 -0300 Subject: [PATCH 5/6] feat: Add formatTimeRaw option --- lib/providers/settings_provider.dart | 13 ++++++++++++- .../events_browser/events_screen_desktop.dart | 3 +-- lib/screens/players/event_player_desktop.dart | 17 +++++++---------- 3 files changed, 20 insertions(+), 13 deletions(-) diff --git a/lib/providers/settings_provider.dart b/lib/providers/settings_provider.dart index 71065ad3..6303c916 100644 --- a/lib/providers/settings_provider.dart +++ b/lib/providers/settings_provider.dart @@ -573,9 +573,20 @@ class SettingsProvider extends UnityProvider { return pattern.format(time); } + String formatTimeRaw( + String rawTime, { + DateFormat? pattern, + Duration offset = Duration.zero, + }) { + return formatTime( + timezoneAwareDate(rawTime).add(offset), + pattern: pattern, + ); + } + String formatRawDateAndTime(String rawDateTime) { final date = formatDate(DateTime.parse(rawDateTime)); - final time = formatRawTime(rawDateTime); + final time = formatRawTime(rawDateTime).toUpperCase(); return '$date $time'; } diff --git a/lib/screens/events_browser/events_screen_desktop.dart b/lib/screens/events_browser/events_screen_desktop.dart index 149d9d0c..8e5eba0b 100644 --- a/lib/screens/events_browser/events_screen_desktop.dart +++ b/lib/screens/events_browser/events_screen_desktop.dart @@ -107,8 +107,7 @@ class EventsScreenDesktop extends StatelessWidget { ), _buildTilePart( child: Text( - '${settings.formatDate(event.published)}' - ' ${settings.formatRawTime(event.publishedRaw).toUpperCase()}', + settings.formatRawDateAndTime(event.publishedRaw), ), flex: 2, ), diff --git a/lib/screens/players/event_player_desktop.dart b/lib/screens/players/event_player_desktop.dart index 9fe6244f..fe9e6da9 100644 --- a/lib/screens/players/event_player_desktop.dart +++ b/lib/screens/players/event_player_desktop.dart @@ -27,7 +27,6 @@ import 'package:bluecherry_client/providers/downloads_provider.dart'; import 'package:bluecherry_client/providers/settings_provider.dart'; import 'package:bluecherry_client/screens/downloads/indicators.dart'; import 'package:bluecherry_client/screens/layouts/video_status_label.dart'; -import 'package:bluecherry_client/utils/date.dart'; import 'package:bluecherry_client/utils/extensions.dart'; import 'package:bluecherry_client/widgets/collapsable_sidebar.dart'; import 'package:bluecherry_client/widgets/desktop_buttons.dart'; @@ -243,11 +242,10 @@ class _EventPlayerDesktopState extends State { snapshot.data ?? videoController.currentPos; return Row(children: [ Text( - settings.formatTime( - timezoneAwareDate(currentEvent.publishedRaw) - .add(pos), + settings.formatTimeRaw( + currentEvent.publishedRaw, + offset: pos, pattern: DateFormat.Hms(), - toLocal: false, ), ), padd, @@ -298,11 +296,10 @@ class _EventPlayerDesktopState extends State { ), padd, Text( - settings.formatTime( - timezoneAwareDate(currentEvent.publishedRaw) - .add(duration), + settings.formatTimeRaw( + currentEvent.publishedRaw, + offset: duration, pattern: DateFormat.Hms(), - toLocal: false, ), ), padd, @@ -495,7 +492,7 @@ class EventTile extends StatelessWidget { final loc = AppLocalizations.of(context); final eventType = event.type.locale(context).uppercaseFirst; - final at = settings.formatDate(timezoneAwareDate(event.publishedRaw)); + final at = settings.formatRawDateAndTime(event.publishedRaw); return SizedBox( width: double.infinity, From a245a1e13de882c1c0859e43b3da4b2bd0a410bc Mon Sep 17 00:00:00 2001 From: Bruno D'Luka Date: Tue, 2 Apr 2024 13:26:13 -0300 Subject: [PATCH 6/6] feat: Abstract date formatting functions into the date utilities file --- lib/providers/settings_provider.dart | 55 ------------------ lib/screens/downloads/downloads_manager.dart | 1 + lib/screens/events_browser/events_screen.dart | 1 + lib/screens/layouts/video_status_label.dart | 1 + lib/screens/players/event_player_desktop.dart | 1 + lib/screens/settings/general.dart | 1 + lib/utils/date.dart | 58 +++++++++++++++++++ 7 files changed, 63 insertions(+), 55 deletions(-) diff --git a/lib/providers/settings_provider.dart b/lib/providers/settings_provider.dart index 6303c916..49ede215 100644 --- a/lib/providers/settings_provider.dart +++ b/lib/providers/settings_provider.dart @@ -23,7 +23,6 @@ import 'package:bluecherry_client/providers/app_provider_interface.dart'; import 'package:bluecherry_client/providers/downloads_provider.dart'; import 'package:bluecherry_client/providers/update_provider.dart'; import 'package:bluecherry_client/screens/events_timeline/desktop/timeline.dart'; -import 'package:bluecherry_client/utils/date.dart'; import 'package:bluecherry_client/utils/storage.dart'; import 'package:bluecherry_client/utils/video_player.dart'; import 'package:flutter/foundation.dart'; @@ -536,60 +535,6 @@ class SettingsProvider extends UnityProvider { save(); } - /// Formats the date according to the current [dateFormat]. - /// - /// [toLocal] defines if the date will be converted to local time. Defaults to `true` - String formatDate(DateTime date) { - if (kConvertTimeToLocalTimezone.value) date = date.toLocal(); - - return kDateFormat.value.format(date); - } - - String formatRawTime(String rawDate) { - return kTimeFormat.value.format( - kConvertTimeToLocalTimezone.value - ? DateTime.parse(rawDate).toLocal() - : timezoneAwareDate(rawDate), - ); - } - - /// Formats the date according to the current [dateFormat]. - /// - /// [toLocal] defines if the date will be converted to local time. Defaults to `true` - String formatTime( - DateTime time, { - DateFormat? pattern, - bool withSeconds = false, - bool? toLocal, - }) { - if (toLocal ?? kConvertTimeToLocalTimezone()) time = time.toLocal(); - - pattern ??= DateFormat(kTimeFormat.value.pattern); - - if (withSeconds) { - pattern = pattern.add_s(); - } - - return pattern.format(time); - } - - String formatTimeRaw( - String rawTime, { - DateFormat? pattern, - Duration offset = Duration.zero, - }) { - return formatTime( - timezoneAwareDate(rawTime).add(offset), - pattern: pattern, - ); - } - - String formatRawDateAndTime(String rawDateTime) { - final date = formatDate(DateTime.parse(rawDateTime)); - final time = formatRawTime(rawDateTime).toUpperCase(); - return '$date $time'; - } - void toggleCycling() { kLayoutCycleEnabled.value = !kLayoutCycleEnabled.value; save(); diff --git a/lib/screens/downloads/downloads_manager.dart b/lib/screens/downloads/downloads_manager.dart index 6cf335b3..ff84a582 100644 --- a/lib/screens/downloads/downloads_manager.dart +++ b/lib/screens/downloads/downloads_manager.dart @@ -24,6 +24,7 @@ import 'package:bluecherry_client/providers/downloads_provider.dart'; import 'package:bluecherry_client/providers/home_provider.dart'; import 'package:bluecherry_client/providers/settings_provider.dart'; import 'package:bluecherry_client/screens/downloads/indicators.dart'; +import 'package:bluecherry_client/utils/date.dart'; import 'package:bluecherry_client/utils/extensions.dart'; import 'package:bluecherry_client/utils/methods.dart'; import 'package:bluecherry_client/utils/theme.dart'; diff --git a/lib/screens/events_browser/events_screen.dart b/lib/screens/events_browser/events_screen.dart index 8b1e291c..9bfd7dc1 100644 --- a/lib/screens/events_browser/events_screen.dart +++ b/lib/screens/events_browser/events_screen.dart @@ -32,6 +32,7 @@ import 'package:bluecherry_client/screens/events_browser/filter.dart'; import 'package:bluecherry_client/screens/events_browser/sidebar.dart'; import 'package:bluecherry_client/screens/players/event_player_desktop.dart'; import 'package:bluecherry_client/utils/constants.dart'; +import 'package:bluecherry_client/utils/date.dart'; import 'package:bluecherry_client/utils/extensions.dart'; import 'package:bluecherry_client/utils/methods.dart'; import 'package:bluecherry_client/widgets/desktop_buttons.dart'; diff --git a/lib/screens/layouts/video_status_label.dart b/lib/screens/layouts/video_status_label.dart index 7f9cc39f..b4f46386 100644 --- a/lib/screens/layouts/video_status_label.dart +++ b/lib/screens/layouts/video_status_label.dart @@ -20,6 +20,7 @@ import 'package:bluecherry_client/models/device.dart'; import 'package:bluecherry_client/models/event.dart'; import 'package:bluecherry_client/providers/settings_provider.dart'; +import 'package:bluecherry_client/utils/date.dart'; import 'package:bluecherry_client/utils/extensions.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; diff --git a/lib/screens/players/event_player_desktop.dart b/lib/screens/players/event_player_desktop.dart index fe9e6da9..2997b512 100644 --- a/lib/screens/players/event_player_desktop.dart +++ b/lib/screens/players/event_player_desktop.dart @@ -27,6 +27,7 @@ import 'package:bluecherry_client/providers/downloads_provider.dart'; import 'package:bluecherry_client/providers/settings_provider.dart'; import 'package:bluecherry_client/screens/downloads/indicators.dart'; import 'package:bluecherry_client/screens/layouts/video_status_label.dart'; +import 'package:bluecherry_client/utils/date.dart'; import 'package:bluecherry_client/utils/extensions.dart'; import 'package:bluecherry_client/widgets/collapsable_sidebar.dart'; import 'package:bluecherry_client/widgets/desktop_buttons.dart'; diff --git a/lib/screens/settings/general.dart b/lib/screens/settings/general.dart index f243f15b..f9e919fc 100644 --- a/lib/screens/settings/general.dart +++ b/lib/screens/settings/general.dart @@ -20,6 +20,7 @@ import 'package:bluecherry_client/providers/settings_provider.dart'; import 'package:bluecherry_client/screens/settings/settings_desktop.dart'; import 'package:bluecherry_client/screens/settings/shared/options_chooser_tile.dart'; +import 'package:bluecherry_client/utils/date.dart'; import 'package:bluecherry_client/utils/extensions.dart'; import 'package:bluecherry_client/widgets/misc.dart'; import 'package:flutter/material.dart'; diff --git a/lib/utils/date.dart b/lib/utils/date.dart index 5130f973..068fe8a0 100644 --- a/lib/utils/date.dart +++ b/lib/utils/date.dart @@ -1,4 +1,6 @@ +import 'package:bluecherry_client/providers/settings_provider.dart'; import 'package:flutter/foundation.dart'; +import 'package:intl/intl.dart'; /// Convert a date string to a DateTime object, considering the timezone offset. DateTime timezoneAwareDate(String originalDateString) { @@ -21,3 +23,59 @@ DateTime timezoneAwareDate(String originalDateString) { return originalDateTime; } } + +extension DateSettingsExtension on SettingsProvider { + /// Formats the date according to the current [dateFormat]. + /// + /// [toLocal] defines if the date will be converted to local time. Defaults to `true` + String formatDate(DateTime date) { + if (kConvertTimeToLocalTimezone.value) date = date.toLocal(); + + return kDateFormat.value.format(date); + } + + String formatRawTime(String rawDate) { + return kTimeFormat.value.format( + kConvertTimeToLocalTimezone.value + ? DateTime.parse(rawDate).toLocal() + : timezoneAwareDate(rawDate), + ); + } + + /// Formats the date according to the current [dateFormat]. + /// + /// [toLocal] defines if the date will be converted to local time. Defaults to `true` + String formatTime( + DateTime time, { + DateFormat? pattern, + bool withSeconds = false, + bool? toLocal, + }) { + if (toLocal ?? kConvertTimeToLocalTimezone()) time = time.toLocal(); + + pattern ??= DateFormat(kTimeFormat.value.pattern); + + if (withSeconds) { + pattern = pattern.add_s(); + } + + return pattern.format(time); + } + + String formatTimeRaw( + String rawTime, { + DateFormat? pattern, + Duration offset = Duration.zero, + }) { + return formatTime( + timezoneAwareDate(rawTime).add(offset), + pattern: pattern, + ); + } + + String formatRawDateAndTime(String rawDateTime) { + final date = formatDate(DateTime.parse(rawDateTime)); + final time = formatRawTime(rawDateTime).toUpperCase(); + return '$date $time'; + } +}