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

Rework timeline view #120

Merged
merged 45 commits into from
Jul 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
4243646
Render the timeline view split by times
bdlukaa Jul 10, 2023
70ed47d
Add events for device
bdlukaa Jul 10, 2023
98bd9e2
Implement precision thumb
bdlukaa Jul 10, 2023
5781f1b
Create a ticker that runs the timeline
bdlukaa Jul 10, 2023
0260984
Add volume and speed
bdlukaa Jul 11, 2023
8ab80bf
Fetch events from server
bdlukaa Jul 11, 2023
411f1e6
Events must be greater than 1 minute
bdlukaa Jul 11, 2023
d6d91a6
Show loading indicator when fetching events
bdlukaa Jul 11, 2023
3d6e2bd
Improve fake data for testing
bdlukaa Jul 11, 2023
0a3d1d1
Implement video view
bdlukaa Jul 12, 2023
f0bce6d
Add TimelineCaption and make text more visible on the grid
bdlukaa Jul 12, 2023
8153cec
It is possible to display multiple events in the same hour
bdlukaa Jul 12, 2023
232427a
Add support to show multiple events in the same hour
bdlukaa Jul 12, 2023
27a99d7
Add debug info and better styling
bdlukaa Jul 13, 2023
e0a99c5
Call setVolume and setSpeed
bdlukaa Jul 13, 2023
be4963e
Add keyboard support
bdlukaa Jul 13, 2023
763a494
Date is initialized even if there are no available servers
bdlukaa Jul 13, 2023
c953f20
fix: add repeat support to keyboard; events are not triggered twice
bdlukaa Jul 13, 2023
3380681
Merge remote-tracking branch 'upstream/main'
bdlukaa Jul 15, 2023
2c25e8e
Remove old timeline implementation
bdlukaa Jul 17, 2023
bc9d375
Add refresh button; Correctly infer date and do not let events in the…
bdlukaa Jul 17, 2023
b0a92c4
fix congruent events; split timeline card widget; add timeline sidebar
bdlukaa Jul 17, 2023
551cf5c
Map events for device object, not device name
bdlukaa Jul 18, 2023
a5f85c2
Add device selecton on TimelineSidebar and update UI
bdlukaa Jul 18, 2023
ac61290
resolve issues
bdlukaa Jul 18, 2023
c4fb56f
Add DateFilter field
bdlukaa Jul 18, 2023
3f23f95
Split mobile and desktop
bdlukaa Jul 18, 2023
98f1c9e
Initial layout for mobile view
bdlukaa Jul 18, 2023
ffe28b5
Track the video position with the scroll controller
bdlukaa Jul 19, 2023
83789a8
Add ensureScrollPosition
bdlukaa Jul 19, 2023
74dfdb2
Add seek on scroll
bdlukaa Jul 19, 2023
688a7b5
Switch cameras and download event on mobile
bdlukaa Jul 19, 2023
986a07a
Enhance mobile UI edges, drawer button
bdlukaa Jul 19, 2023
eded60a
Enhance UI
bdlukaa Jul 20, 2023
e16fc2a
Scroll is done dynamically and "next" and "previous" button implemented
bdlukaa Jul 20, 2023
7fb19ab
performance
bdlukaa Jul 20, 2023
71461b6
Improve caching strategy
bdlukaa Jul 20, 2023
b0c31bb
Add refresh button and update buffer indicator
bdlukaa Jul 20, 2023
23e2069
Implement pauseToBuffer , enhance play-pause icon and enhance streams…
bdlukaa Jul 21, 2023
2b8c78a
Fix play and pause
bdlukaa Jul 21, 2023
8c461de
Add hero transitions and use same video controller between screens
bdlukaa Jul 21, 2023
e11d5f6
Update UI and buffer factor
bdlukaa Jul 22, 2023
b2f4d21
Correctly play the timeline on mobile and add a time filter
bdlukaa Jul 25, 2023
c1bf320
Add amount of events per camera on mobile camera selector
bdlukaa Jul 26, 2023
55e92f0
Improve performance of the device selector screen
bdlukaa Jul 26, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 30 additions & 27 deletions lib/api/api.dart
Original file line number Diff line number Diff line change
Expand Up @@ -176,33 +176,36 @@ class API {
// debugPrint(response.body);

final parser = Xml2Json()..parse(response.body);
final events = jsonDecode(parser.toGData())['feed']['entry'].map((e) {
if (!e.containsKey('content')) debugPrint(e.toString());
return Event(
server,
int.parse(e['id']['raw']),
int.parse((e['category']['term'] as String).split('/').first),
e['title']['\$t'],
e['published'] == null || e['published']['\$t'] == null
? DateTime.now()
: DateTime.parse(e['published']['\$t']),
e['updated'] == null || e['updated']['\$t'] == null
? DateTime.now()
: DateTime.parse(e['updated']['\$t']),
e['category']['term'],
!e.containsKey('content')
? null
: int.parse(e['content']['media_id']),
!e.containsKey('content')
? null
: Uri.parse(
e['content'][r'$t'].replaceAll(
'https://',
'https://${Uri.encodeComponent(server.login)}:${Uri.encodeComponent(server.password)}@',
),
),
);
}).cast<Event>();
final events = (jsonDecode(parser.toGData())['feed']['entry'] as List)
.map((e) {
if (!e.containsKey('content')) debugPrint(e.toString());
return Event(
server,
int.parse(e['id']['raw']),
int.parse((e['category']['term'] as String).split('/').first),
e['title']['\$t'],
e['published'] == null || e['published']['\$t'] == null
? DateTime.now()
: DateTime.parse(e['published']['\$t']).toLocal(),
e['updated'] == null || e['updated']['\$t'] == null
? DateTime.now()
: DateTime.parse(e['updated']['\$t']).toLocal(),
e['category']['term'],
!e.containsKey('content')
? null
: int.parse(e['content']['media_id']),
!e.containsKey('content')
? null
: Uri.parse(
e['content'][r'$t'].replaceAll(
'https://',
'https://${Uri.encodeComponent(server.login)}:${Uri.encodeComponent(server.password)}@',
),
),
);
})
.where((e) => e.duration > const Duration(minutes: 1))
.cast<Event>();

debugPrint('Loaded ${events.length} events for server ${server.name}');
return events;
Expand Down
10 changes: 10 additions & 0 deletions lib/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
"replaceCamera": "Replace Camera",
"reloadCamera": "Reload Camera",
"selectACamera": "Select a camera",
"switchCamera": "Switch camera",
"online": "Online",
"offline": "Offline",
"removeFromView": "Remove from view",
Expand All @@ -56,6 +57,8 @@
"event": "Event",
"duration": "Duration",
"priority": "Priority",
"next": "Next",
"previous": "Previous",
"date": "Date",
"lastUpdate": "Last Update",
"theme": "Theme",
Expand Down Expand Up @@ -146,6 +149,7 @@
"downloads": "Downloads",
"download": "Download",
"downloaded": "Downloaded",
"downloading": "Downloading",
"seeInDownloads": "See in Downloads",
"delete": "Delete",
"showInFiles": "Show in Files",
Expand Down Expand Up @@ -181,6 +185,12 @@
"toDate": "To",
"allowAlarms": "Allow alarms",
"nextEvents": "Next events",
"nEvents": "{n} events",
"@nEvents": {
"placeholders": {
"n": {}
}
},
"@Event Priorities": {},
"info": "Info",
"warn": "Warning",
Expand Down
5 changes: 4 additions & 1 deletion lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,9 @@ class UnityApp extends StatelessWidget {
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>?) ?? [];
final videoPlayer = data['videoPlayer'] as UnityVideoPlayer?;

return MaterialPageRoute(
settings: RouteSettings(
Expand All @@ -234,6 +236,7 @@ class UnityApp extends StatelessWidget {
return EventPlayerScreen(
event: event,
upcomingEvents: upcomingEvents,
player: videoPlayer,
);
},
);
Expand Down
9 changes: 9 additions & 0 deletions lib/models/device.dart
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,15 @@ class Device {
this.hasPTZ = false,
});

Device.dump({
this.name = 'device',
this.id = 0,
this.status = true,
this.resolutionX = 640,
this.resolutionY = 480,
this.hasPTZ = false,
}) : server = Server.dump();

String get uri => 'live/$id';

factory Device.fromServerJson(Map map, Server server) {
Expand Down
7 changes: 3 additions & 4 deletions lib/models/event.dart
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

import 'package:bluecherry_client/models/server.dart';
import 'package:bluecherry_client/providers/server_provider.dart';
import 'package:bluecherry_client/utils/extensions.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';

Expand Down Expand Up @@ -66,14 +67,12 @@ class Event {
.last
.trim()
.split(' ')
.map((e) => e.isEmpty ? '' : e[0].toUpperCase() + e.substring(1))
.map((e) => e.uppercaseFirst())
.join(' ');
}

Duration get duration {
// return mediaDuration ?? updated.difference(published);
// TODO(bdlukaa): for some reason, the diff is off by a few seconds. use this to counterpart the issue
final dur = updated.difference(published) - const Duration(seconds: 3);
final dur = updated.difference(published);
if (dur < Duration.zero) return updated.difference(published);
return dur;
}
Expand Down
16 changes: 16 additions & 0 deletions lib/models/server.dart
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,22 @@ class Server {
this.passedCertificates = true,
});

Server.dump({
this.name = 'server',
this.ip = 'server:ip',
this.port = 7001,
this.login = 'admin',
this.password = 'admin',
this.devices = const [],
this.rtspPort = 7002,
this.serverUUID,
this.cookie,
this.savePassword = false,
this.connectAutomaticallyAtStartup = true,
this.online = true,
this.passedCertificates = true,
});

String get id {
return '$name;$ip;$port';
}
Expand Down
15 changes: 8 additions & 7 deletions lib/providers/home_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -109,19 +109,20 @@ class HomeProvider extends ChangeNotifier {
final home = context.read<HomeProvider>();
final tab = home.tab;

/// On device grid or in eventsPlayback, use landscape
if ([
UnityTab.deviceGrid.index,
UnityTab.eventsPlayback.index,
].contains(tab)) {
/// On device grid, use landscape
if ([UnityTab.deviceGrid.index].contains(tab)) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Events Screen is portrait-mode only.

setDefaultStatusBarStyle();
DeviceOrientations.instance.set([
DeviceOrientation.landscapeLeft,
DeviceOrientation.landscapeRight,
]);
} else if ([UnityTab.directCameraScreen.index].contains(tab)) {
setDefaultStatusBarStyle();
DeviceOrientations.instance.set([
DeviceOrientation.portraitUp,
DeviceOrientation.portraitDown,
]);
} else if ([UnityTab.addServer.index].contains(tab)) {
// Use portrait orientation in "Add Server" tab.
// See #14.
setDefaultStatusBarStyle(isLight: true);
DeviceOrientations.instance.set([
DeviceOrientation.portraitUp,
Expand Down
4 changes: 2 additions & 2 deletions lib/providers/settings_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ class SettingsProvider extends ChangeNotifier {
/// 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 = true}) {
String formatDate(DateTime date, {bool toLocal = false}) {
if (toLocal) date = date.toLocal();

return dateFormat.format(date);
Expand All @@ -237,7 +237,7 @@ class SettingsProvider extends ChangeNotifier {
/// 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 = true}) {
String formatTime(DateTime time, {bool toLocal = false}) {
if (toLocal) time = time.toLocal();

return timeFormat.format(time);
Expand Down
20 changes: 17 additions & 3 deletions lib/utils/extensions.dart
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,20 @@ extension DurationExtension on Duration {

return this;
}

double get inDoubleSeconds {
return inMilliseconds / 1000;
}
}

extension IterableExtension<T> on Iterable<T> {
T? firstWhereOrNull(bool Function(T element) test) {
try {
return firstWhere(test);
} catch (_) {
return null;
}
}
}

extension NotificationExtensions on NotificationClickAction {
Expand Down Expand Up @@ -152,9 +166,9 @@ extension DateTimeExtension on DateTime {
}

bool isInBetween(DateTime first, DateTime second) {
return isAfter(first) && isBefore(second) ||
this == first ||
this == second;
return (isAfter(first) && isBefore(second)) ||
isAtSameMomentAs(first) ||
isAtSameMomentAs(second);
}
}

Expand Down
23 changes: 23 additions & 0 deletions lib/utils/tree_view/tree_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,29 @@ import 'package:flutter_simple_treeview/flutter_simple_treeview.dart'
export 'package:flutter_simple_treeview/flutter_simple_treeview.dart'
show TreeNode, TreeController;

Widget buildCheckbox({
required bool? value,
required ValueChanged<bool?> onChanged,
required bool isError,
double checkboxScale = 0.8,
}) {
return Transform.scale(
scale: checkboxScale,
child: Checkbox(
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
visualDensity: const VisualDensity(
horizontal: -4,
vertical: -4,
),
splashRadius: 0.0,
tristate: true,
value: value,
isError: isError,
onChanged: onChanged,
),
);
}

/// Tree view with collapsible and expandable nodes.
class TreeView extends StatefulWidget {
/// List of root level tree nodes.
Expand Down
5 changes: 4 additions & 1 deletion lib/widgets/desktop_buttons.dart
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import 'package:bluecherry_client/models/device.dart';
import 'package:bluecherry_client/models/event.dart';
import 'package:bluecherry_client/providers/home_provider.dart';
import 'package:bluecherry_client/widgets/events/events_screen.dart';
import 'package:bluecherry_client/widgets/events_timeline/events_playback.dart';
import 'package:bluecherry_client/widgets/home.dart';
import 'package:bluecherry_client/widgets/misc.dart';
import 'package:flutter/material.dart';
Expand Down Expand Up @@ -200,10 +201,12 @@ class _WindowButtonsState extends State<WindowButtons> with WindowListener {
padding: EdgeInsets.symmetric(horizontal: 8.0),
child: UnityLoadingIndicator(),
)
else if (home.tab == UnityTab.eventsScreen.index && !canPop)
else if (home.tab == UnityTab.eventsScreen.index ||
home.tab == UnityTab.eventsPlayback.index && !canPop)
IconButton(
onPressed: () {
eventsScreenKey.currentState?.fetch();
eventsPlaybackScreenKey.currentState?.fetch();
},
icon: const Icon(Icons.refresh),
iconSize: 20.0,
Expand Down
21 changes: 20 additions & 1 deletion lib/widgets/device_grid/desktop/desktop_device_grid.dart
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,7 @@ class DesktopDeviceTile extends StatelessWidget {

return UnityVideoView(
key: ValueKey(device.fullName),
heroTag: device.streamURL,
player: videoPlayer,
paneBuilder: (context, controller) {
return DesktopTileViewport(controller: controller, device: device);
Expand Down Expand Up @@ -322,6 +323,25 @@ class DesktopTileViewport extends StatefulWidget {
State<DesktopTileViewport> createState() => _DesktopTileViewportState();
}

const shadows = [
Shadow(
blurRadius: 10,
offset: Offset(-4, -4),
),
Shadow(
blurRadius: 10,
offset: Offset(4, 4),
),
Shadow(
blurRadius: 10,
offset: Offset(-4, 4),
),
Shadow(
blurRadius: 10,
offset: Offset(4, -4),
),
];

class _DesktopTileViewportState extends State<DesktopTileViewport> {
bool ptzEnabled = false;

Expand Down Expand Up @@ -359,7 +379,6 @@ class _DesktopTileViewportState extends State<DesktopTileViewport> {

final theme = Theme.of(context);
final view = context.watch<DesktopViewProvider>();

final isSubView = AlternativeWindow.maybeOf(context) != null;

Widget foreground = PTZController(
Expand Down
3 changes: 2 additions & 1 deletion lib/widgets/device_grid/desktop/layout_manager.dart
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@ class _LayoutManagerState extends State<LayoutManager> {
final settings = context.watch<SettingsProvider>();
timer?.cancel();
timer = Timer.periodic(settings.layoutCyclingTogglePeriod, (timer) {
final settings = SettingsProvider.instance;
if (!mounted) return;

final view = DesktopViewProvider.instance;

if (settings.layoutCyclingEnabled) {
Expand Down
10 changes: 3 additions & 7 deletions lib/widgets/device_grid/mobile/device_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -149,13 +149,8 @@ class _MobileDeviceViewState extends State<MobileDeviceView> {
break;
case 1:
if (mounted) {
final result = await Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => const DeviceSelectorScreen(),
),
);

if (result is Device) {
final result = await showDeviceSelectorScreen(context);
if (result != null) {
view.replace(widget.tab, widget.index, result);
if (mounted) setState(() {});
}
Expand Down Expand Up @@ -251,6 +246,7 @@ class DeviceTileState extends State<DeviceTile> {
if (videoPlayer == null) return const SizedBox.shrink();

return UnityVideoView(
heroTag: widget.device.streamURL,
player: videoPlayer!,
paneBuilder: (context, controller) {
final error = UnityVideoView.of(context).error;
Expand Down
Loading