Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Timeline Enhancements #262

Merged
merged 14 commits into from
Sep 7, 2024
Merged
2 changes: 2 additions & 0 deletions lib/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@
"noDevices": "No devices",
"noEventsLoaded": "NO EVENTS LOADED",
"noEventsLoadedTips": "• Select the cameras you want to see the events\n• Use the calendar to select a specific date or a date range \n• Use the \"Filter\" button to perform the search",
"timelineKeyboardShortcutsTips": "• Use the space bar to play/pause the timeline\n• Use the left and right arrow keys to move the timeline\n• Use the M key to mute/unmute the timeline\n• Use the mouse wheel to zoom in/out the timeline",
"invalidResponse": "Invalid response received from the server",
"cameraOptions": "Options",
"showFullscreenCamera": "Show in fullscreen",
Expand Down Expand Up @@ -525,6 +526,7 @@
"beginningInitialPoint": "Beginning",
"firstEventInitialPoint": "First event",
"hourAgoInitialPoint": "1 hour ago",
"automaticallySkipEmptyPeriods": "Automatically skip empty periods",
"@@APPLICATION": {},
"appearance": "Appearance",
"theme": "Theme",
Expand Down
4 changes: 3 additions & 1 deletion lib/l10n/app_fr.arb
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@
"noDevices": "Aucun appareil",
"noEventsLoaded": "AUCUN ÉVÈNEMENT CHARGÉ",
"noEventsLoadedTips": "• Sélectionnez la caméra dont vous voulez voir les evènements\n• Utilisez le calendrier pour sélectionner une date précise ou une période entre deux dates \n• Utilisez le bouton \"Filtre\" pour effectuer une recherche",
"timelineKeyboardShortcutsTips": "• Use the space bar to play/pause the timeline\n• Use the left and right arrow keys to move the timeline\n• Use the M key to mute/unmute the timeline\n • Use the mouse wheel to zoom in/out the timeline",
"invalidResponse": "Réponse invalide reçu du serveur",
"cameraOptions": "Options",
"showFullscreenCamera": "Montrer en plein écran",
Expand Down Expand Up @@ -501,6 +502,7 @@
"beginningInitialPoint": "Commencement",
"firstEventInitialPoint": "Premier évènement",
"hourAgoInitialPoint": "Il y a 1 heure",
"automaticallySkipEmptyPeriods": "Automatically skip empty periods",
"@@APPLICATION": {},
"appearance": "Apparence",
"theme": "Thème",
Expand Down Expand Up @@ -644,4 +646,4 @@
"@@@Updates and Help": {},
"help": "Aide",
"licenses": "Licenses"
}
}
2 changes: 2 additions & 0 deletions lib/l10n/app_pl.arb
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@
"noDevices": "Brak urządzeń",
"noEventsLoaded": "NIE ZAŁADOWANO ZDARZEŃ",
"noEventsLoadedTips": "• Wybież kamery do podglądu zdarzeń\n• Użyj kalnedarza żeby wybrać konkretną datę lub zakres \n• Użyj przycisku \"Filtr\" aby wyszukiwać",
"timelineKeyboardShortcutsTips": "• Use the space bar to play/pause the timeline\n• Use the left and right arrow keys to move the timeline\n• Use the M key to mute/unmute the timeline\n • Use the mouse wheel to zoom in/out the timeline",
"invalidResponse": "Odebrano nieprawidłową odpowiedź z serwera",
"cameraOptions": "Opcje",
"showFullscreenCamera": "Pokaż na pełnym ekranie",
Expand Down Expand Up @@ -525,6 +526,7 @@
"beginningInitialPoint": "Beginning",
"firstEventInitialPoint": "First event",
"hourAgoInitialPoint": "1 hour ago",
"automaticallySkipEmptyPeriods": "Automatically skip empty periods",
"@@APPLICATION": {},
"appearance": "Appearance",
"theme": "Motyw",
Expand Down
2 changes: 2 additions & 0 deletions lib/l10n/app_pt.arb
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@
"noDevices": "Nenhum dispositivo",
"noEventsLoaded": "NENHUM EVENTO CARREGADO",
"noEventsLoadedTips": "• Selecione as câmeras cujas você quer ver os eventos\n• Utilize o calendário para selecionar uma data específica ou intervalo de datas \n• Use o botão \"Filtrar\" para pesquisar",
"timelineKeyboardShortcutsTips": "• Use a barra de espaço para reproduzir/pausar a linha do tempo\n• Use as setas esquerda e direita para mover a linha do tempo\n• Use a tecla M para silenciar/dessilenciar a linha do tempo\n • Use o scroll do mouse para dar zoom na linha do tempo",
"invalidResponse": "Resposta inválida recebida do servidor",
"cameraOptions": "Opções",
"showFullscreenCamera": "Ver em tela cheia",
Expand Down Expand Up @@ -525,6 +526,7 @@
"beginningInitialPoint": "Início",
"firstEventInitialPoint": "Primeiro evento",
"hourAgoInitialPoint": "1 hora atrás",
"automaticallySkipEmptyPeriods": "Pular períodos vazios automaticamente",
"@@APPLICATION": {},
"appearance": "Visualização",
"theme": "Aparência",
Expand Down
13 changes: 11 additions & 2 deletions lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_localized_locales/flutter_localized_locales.dart';
import 'package:intl/date_symbol_data_local.dart';
import 'package:intl/intl.dart';
import 'package:path/path.dart' as path;
import 'package:permission_handler/permission_handler.dart';
import 'package:provider/provider.dart';
Expand All @@ -82,6 +84,8 @@ Future<void> main(List<String> args) async {
runApp(const SplashScreen());
}

await initializeDateFormatting();

DevHttpOverrides.configureCertificates();
API.initialize();
await UnityVideoPlayerInterface.instance.initialize();
Expand Down Expand Up @@ -365,8 +369,8 @@ class _UnityAppState extends State<UnityApp>
if (settings.name == '/events') {
final data = settings.arguments! as Map;
final event = data['event'] as Event;
final upcomingEvents =
(data['upcoming'] as Iterable<Event>?) ?? [];
final upcomingEvents = (data['upcoming'] as Iterable<Event>?) ??
List.empty(growable: true);
final videoPlayer = data['videoPlayer'] as UnityVideoPlayer?;

return MaterialPageRoute(
Expand Down Expand Up @@ -426,6 +430,11 @@ class _UnityAppState extends State<UnityApp>

return null;
},
builder: (context, child) {
Intl.defaultLocale = Localizations.localeOf(context).languageCode;

return child!;
},
);
}),
);
Expand Down
7 changes: 7 additions & 0 deletions lib/providers/events_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,13 @@ class EventsProvider extends UnityProvider {
notifyListeners();
}

int _eventTypeFilter = -1;
int get eventTypeFilter => _eventTypeFilter;
set eventTypeFilter(int value) {
_eventTypeFilter = value;
notifyListeners();
}

LoadedEvents? loadedEvents;

@override
Expand Down
54 changes: 44 additions & 10 deletions lib/providers/settings_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,11 @@ class _SettingsOption<T> {
late final String Function(T value) saveAs;
late final T Function(String value) loadFrom;
final ValueChanged<T>? onChanged;
final T Function()? valueOverrider;
final T Function(T value)? valueOverrider;

late T _value;

T get value => valueOverrider?.call() ?? _value;
T get value => valueOverrider?.call(_value) ?? _value;
set value(T newValue) {
SettingsProvider.instance.updateProperty(() {
_value = newValue;
Expand Down Expand Up @@ -302,20 +302,24 @@ class SettingsProvider extends UnityProvider {
);

// Timeline of Events
final kShowDifferentColorsForEvents = _SettingsOption(
final kShowDifferentColorsForEvents = _SettingsOption<bool>(
def: false,
key: 'timeline.show_different_colors_for_events',
);
final kPauseToBuffer = _SettingsOption(
final kPauseToBuffer = _SettingsOption<bool>(
def: false,
key: 'timeline.pause_to_buffer',
);
final kTimelineInitialPoint = _SettingsOption(
final kTimelineInitialPoint = _SettingsOption<TimelineInitialPoint>(
def: TimelineInitialPoint.beginning,
key: 'timeline.initial_point',
loadFrom: (value) => TimelineInitialPoint.values[int.parse(value)],
saveAs: (value) => value.index.toString(),
);
final kAutomaticallySkipEmptyPeriods = _SettingsOption<bool>(
def: false,
key: 'timeline.automatically_skip_empty_periods',
);

// Application
final kThemeMode = _SettingsOption(
Expand All @@ -328,15 +332,42 @@ class SettingsProvider extends UnityProvider {
def: Locale.fromSubtags(languageCode: Intl.getCurrentLocale()),
key: 'application.language_code',
);
final kDateFormat = _SettingsOption(
def: DateFormat('EEEE, dd MMMM yyyy'),

late final kDateFormat = _SettingsOption(
def: DateFormat(
'EEEE, dd MMMM yyyy',
kLanguageCode.value.toLanguageTag(),
),
key: 'application.date_format',
valueOverrider: (value) {
return DateFormat(value.pattern, kLanguageCode.value.toLanguageTag());
},
);
final kTimeFormat = _SettingsOption(
def: DateFormat('hh:mm a'),

static const availableTimeFormats = ['HH:mm', 'hh:mm a'];
late final kTimeFormat = _SettingsOption(
def: DateFormat('hh:mm a', kLanguageCode.value.toLanguageTag()),
key: 'application.time_format',
valueOverrider: (value) {
return DateFormat(value.pattern, kLanguageCode.value.toLanguageTag());
},
);

/// The extended time format adds the second to the time format.
DateFormat get extendedTimeFormat {
return switch (kTimeFormat.value.pattern!) {
'HH:mm' => DateFormat('HH:mm:ss', kLanguageCode.value.toLanguageTag()),
'hh:mm a' => DateFormat(
'hh:mm:ss a',
kLanguageCode.value.toLanguageTag(),
),
_ => DateFormat(
kTimeFormat.value.pattern,
kLanguageCode.value.toLanguageTag(),
),
};
}

// TODO(bdlukaa): remove this in future releases
var _hasMigratedTimezone = false;
late final kConvertTimeToLocalTimezone = _SettingsOption<bool>(
Expand Down Expand Up @@ -433,7 +464,7 @@ class SettingsProvider extends UnityProvider {
..zoom.softwareZoom = value;
}
},
valueOverrider: isHardwareZoomSupported ? () => true : null,
valueOverrider: isHardwareZoomSupported ? (_) => true : null,
);
final kEventsMatrixedZoom = _SettingsOption(
def: true,
Expand Down Expand Up @@ -493,6 +524,7 @@ class SettingsProvider extends UnityProvider {
kShowDifferentColorsForEvents.loadData(data),
kPauseToBuffer.loadData(data),
kTimelineInitialPoint.loadData(data),
kAutomaticallySkipEmptyPeriods.loadData(data),
kThemeMode.loadData(data),
kLanguageCode.loadData(data),
kDateFormat.loadData(data),
Expand Down Expand Up @@ -570,6 +602,8 @@ class SettingsProvider extends UnityProvider {
kPauseToBuffer.key: kPauseToBuffer.saveAs(kPauseToBuffer.value),
kTimelineInitialPoint.key:
kTimelineInitialPoint.saveAs(kTimelineInitialPoint.value),
kAutomaticallySkipEmptyPeriods.key: kAutomaticallySkipEmptyPeriods
.saveAs(kAutomaticallySkipEmptyPeriods.value),
kThemeMode.key: kThemeMode.saveAs(kThemeMode.value),
kLanguageCode.key: kLanguageCode.saveAs(kLanguageCode.value),
kDateFormat.key: kDateFormat.saveAs(kDateFormat.value),
Expand Down
134 changes: 134 additions & 0 deletions lib/screens/events_browser/event_type_filter.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
/*
* This file is a part of Bluecherry Client (https://github.com/bluecherrydvr/unity).
*
* Copyright 2022 Bluecherry, LLC
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation; either version 3 of
* the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

import 'package:auto_size_text/auto_size_text.dart';
import 'package:bluecherry_client/models/event.dart';
import 'package:bluecherry_client/providers/events_provider.dart';
import 'package:bluecherry_client/widgets/misc.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:provider/provider.dart';

class EventTypeFilterTile extends StatefulWidget {
const EventTypeFilterTile({super.key});

@override
State<EventTypeFilterTile> createState() => _EventTypeFilterTileState();
}

class _EventTypeFilterTileState extends State<EventTypeFilterTile> {
final _eventTypeFilterTileKey = GlobalKey();

@override
Widget build(BuildContext context) {
final loc = AppLocalizations.of(context);
final eventsProvider = context.watch<EventsProvider>();
final theme = Theme.of(context);

return ListTile(
key: _eventTypeFilterTileKey,
dense: true,
title: Text(
loc.eventType,
style: const TextStyle(fontWeight: FontWeight.bold),
),
trailing: AutoSizeText(
() {
final type = eventsProvider.eventTypeFilter;
// For some reason I can not use a switch here
if (type == EventType.motion.index) {
return loc.motion;
} else if (type == EventType.continuous.index) {
return loc.continuous;
} else {
return 'All';
}
}(),
maxLines: 1,
),
onTap: () async {
final box = _eventTypeFilterTileKey.currentContext!.findRenderObject()
as RenderBox;

showMenu(
context: context,
position: RelativeRect.fromRect(
box.localToGlobal(
Offset.zero,
ancestor: Navigator.of(context).context.findRenderObject(),
) &
box.size,
Offset.zero & MediaQuery.of(context).size,
),
constraints: BoxConstraints(
minWidth: box.size.width - 8,
maxWidth: box.size.width - 8,
),
items: <PopupMenuEntry>[
PopupMenuLabel(
label: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 6.0,
),
child: Text(
loc.eventType,
maxLines: 1,
style: theme.textTheme.labelSmall,
),
),
),
const PopupMenuDivider(),
_buildMenuItem(
value: -1,
child: const Text('All'),
),
_buildMenuItem(
value: EventType.motion.index,
child: Text(loc.motion),
),
_buildMenuItem(
value: EventType.continuous.index,
child: Text(loc.continuous),
),
],
);
},
);
}

PopupMenuItem _buildMenuItem({required Widget child, required int value}) {
final eventsProvider = context.read<EventsProvider>();
final selected = eventsProvider.eventTypeFilter == value;

return CheckedPopupMenuItem(
value: value,
padding: const EdgeInsets.symmetric(horizontal: 20.0),
checked: selected,
// enabled: !selected,
onTap: () {
eventsProvider.eventTypeFilter = value;
},
child: Align(
alignment: AlignmentDirectional.centerEnd,
child: child,
),
);
}
}
Loading
Loading