Skip to content

Commit

Permalink
Bug fixes and UI tweaks (#189)
Browse files Browse the repository at this point in the history
  • Loading branch information
bdlukaa authored Dec 1, 2023
2 parents d2c9695 + 83c48d1 commit 7d8e5da
Show file tree
Hide file tree
Showing 34 changed files with 436 additions and 264 deletions.
3 changes: 2 additions & 1 deletion lib/models/device.dart
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,8 @@ class Device {
}

Future<String> getHLSUrl([Device? device]) async {
// return hlsURL;
if (url != null) return url!;

device ??= this;
var data = {
'id': device.id.toString(),
Expand Down
3 changes: 1 addition & 2 deletions lib/models/server.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import 'package:bluecherry_client/models/device.dart';
import 'package:bluecherry_client/providers/settings_provider.dart';
import 'package:bluecherry_client/utils/constants.dart';
import 'package:bluecherry_client/utils/extensions.dart';
import 'package:flutter/foundation.dart';
import 'package:unity_video_player/unity_video_player.dart';

class AdditionalServerOptions {
Expand Down Expand Up @@ -231,7 +230,7 @@ class Server {
other.login == login &&
other.password == password &&
other.additionalSettings == additionalSettings &&
listEquals(other.devices, devices) &&
other.devices == devices &&
other.serverUUID == serverUUID &&
other.cookie == cookie &&
other.online == online &&
Expand Down
87 changes: 47 additions & 40 deletions lib/providers/update_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import 'dart:io';

import 'package:bluecherry_client/utils/constants.dart';
import 'package:bluecherry_client/utils/logging.dart';
import 'package:bluecherry_client/utils/methods.dart';
import 'package:bluecherry_client/utils/storage.dart';
import 'package:dio/dio.dart';
Expand Down Expand Up @@ -377,51 +378,57 @@ class UpdateManager extends ChangeNotifier {
loading = true;
notifyListeners();

final response = await http.get(Uri.parse(appCastUrl));
try {
final response = await http.get(Uri.parse(appCastUrl));

if (response.statusCode != 200) {
debugPrint(
'Failed to check for updates (${response.statusCode}): ${response.body}',
);
loading = false;
notifyListeners();
return;
}
if (response.statusCode != 200) {
debugPrint(
'Failed to check for updates (${response.statusCode}): ${response.body}',
);
loading = false;
notifyListeners();
return;
}

var versions = <UpdateVersion>[];
final doc = XmlDocument.parse(response.body);
for (final item in doc.findAllElements('item')) {
late String version;
late String description;
late String publishedAt;
for (var child in item.children.whereType<XmlElement>()) {
switch (child.name.toString()) {
case UpdateVersion.titleField:
version = child.innerText.replaceAll('Version', '').trim();
break;
case UpdateVersion.descriptionField:
description = child.innerText.trim();
break;
case UpdateVersion.publishedAtField:
publishedAt = child.innerText.trim();
break;
default:
var versions = <UpdateVersion>[];
final doc = XmlDocument.parse(response.body);
for (final item in doc.findAllElements('item')) {
late String version;
late String description;
late String publishedAt;
for (var child in item.children.whereType<XmlElement>()) {
switch (child.name.toString()) {
case UpdateVersion.titleField:
version = child.innerText.replaceAll('Version', '').trim();
break;
case UpdateVersion.descriptionField:
description = child.innerText.trim();
break;
case UpdateVersion.publishedAtField:
publishedAt = child.innerText.trim();
break;
default:
}
}
versions.add(UpdateVersion(
version: version,
description: description,
publishedAt: publishedAt,
));
}
versions.add(UpdateVersion(
version: version,
description: description,
publishedAt: publishedAt,
));
}
// versions.sort(
// (a, b) => a.publishedAt.compareTo(b.publishedAt),
// );
versions = versions.reversed.toList();
// versions.sort(
// (a, b) => a.publishedAt.compareTo(b.publishedAt),
// );
versions = versions.reversed.toList();

if (versions != this.versions) this.versions = versions;
if (versions != this.versions) this.versions = versions;

loading = false;
lastCheck = DateTime.now(); // this updates the screen already
loading = false;
lastCheck = DateTime.now(); // this updates the screen already
} catch (error, stackTrace) {
debugPrint(error.toString());
debugPrint(stackTrace.toString());
writeErrorToFile(error, stackTrace);
}
}
}
88 changes: 67 additions & 21 deletions lib/utils/video_player.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,21 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

import 'dart:async';

import 'package:bluecherry_client/models/device.dart';
import 'package:bluecherry_client/providers/settings_provider.dart';
import 'package:bluecherry_client/utils/logging.dart';
import 'package:flutter/widgets.dart';
import 'package:unity_video_player/unity_video_player.dart';

class UnityPlayers with ChangeNotifier {
UnityPlayers._();
UnityPlayers._() {
UnityPlayers._reloadTimer = Timer.periodic(
reloadTime,
(_) => reloadDevices(),
);
}

static final instance = UnityPlayers._();

Expand All @@ -33,13 +40,62 @@ class UnityPlayers with ChangeNotifier {
/// This avoids redundantly creating new video player instance if a [Device]
/// is already present in the camera grid on the screen or allows to use
/// existing instance when switching tab (if common camera [Device] tile exists).
///
static final players = <String, UnityVideoPlayer>{};

/// Devices that should be reloaded at every [reloadTime] interval.
static final _reloadable = <String>[];
static const reloadTime = Duration(minutes: 5);

static late final Timer _reloadTimer;

/// Reloads all devices that are marked as reloadable.
static Future<void> reloadDevices() async {
debugPrint('Reloading devices $_reloadable');
for (final player
in players.entries.where((entry) => _reloadable.contains(entry.key))) {
// reload each device at once
await reloadDevice(Device.fromUUID(player.key)!);
}
}

/// Whether the given [Device] is reloadable.
static bool isReloadable(String deviceUUID) =>
_reloadable.contains(deviceUUID);

/// Helper method to create a video player with required configuration for a [Device].
static UnityVideoPlayer forDevice(Device device) {
final settings = SettingsProvider.instance;
final controller = UnityVideoPlayer.create(
late UnityVideoPlayer controller;

Future<void> setSource() async {
if (device.url != null) {
debugPrint(device.url);
controller.setDataSource(device.url!);
} else {
final (String source, Future<String> fallback) = switch (
device.preferredStreamingType ??
device.server.additionalSettings.preferredStreamingType ??
settings.streamingType) {
StreamingType.rtsp => (device.rtspURL, device.getHLSUrl()),
StreamingType.hls => (
await device.getHLSUrl(),
Future.value(device.rtspURL)
),
StreamingType.mjpeg => (device.mjpegURL, Future.value(device.hlsURL)),
};
debugPrint(source);
controller
..fallbackUrl = fallback
..setDataSource(source);

// TODO(bdlukaa): reevaluate if this system is still necessary.
// on the player, we now reload the device if the image
// is timed out. See [UnityVideoPlayer.create.onReload]
// _reloadable.add(source);
}
}

controller = UnityVideoPlayer.create(
quality: switch (device.server.additionalSettings.renderingQuality ??
settings.videoQuality) {
RenderingQuality.p4k => UnityVideoQuality.p4k,
Expand All @@ -51,28 +107,11 @@ class UnityPlayers with ChangeNotifier {
RenderingQuality.automatic =>
UnityVideoQuality.qualityForResolutionY(device.resolutionY),
},
onReload: setSource,
)
..setVolume(0.0)
..setSpeed(1.0);

Future<void> setSource() async {
final (String source, Future<String> fallback) = switch (
device.preferredStreamingType ??
device.server.additionalSettings.preferredStreamingType ??
settings.streamingType) {
StreamingType.rtsp => (device.rtspURL, device.getHLSUrl()),
StreamingType.hls => (
await device.getHLSUrl(),
Future.value(device.rtspURL)
),
StreamingType.mjpeg => (device.mjpegURL, Future.value(device.hlsURL)),
};
debugPrint(source);
controller
..fallbackUrl = fallback
..setDataSource(source);
}

setSource();

controller.onError.listen((event) {
Expand All @@ -87,6 +126,7 @@ class UnityPlayers with ChangeNotifier {
/// Release the video player for the given [Device].
static Future<void> releaseDevice(String deviceUUID) async {
debugPrint('Releasing device $deviceUUID. ${players[deviceUUID]}');
_reloadable.remove(deviceUUID);
await players[deviceUUID]?.dispose();
players.remove(deviceUUID);
}
Expand Down Expand Up @@ -136,4 +176,10 @@ class UnityPlayers with ChangeNotifier {
);
if (isLocalController) await player.dispose();
}

@override
void dispose() {
_reloadTimer.cancel();
super.dispose();
}
}
56 changes: 56 additions & 0 deletions lib/utils/widgets/squared_icon_button.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import 'package:flutter/material.dart';

class SquaredIconButton extends StatelessWidget {
/// Called when the button is pressed.
///
/// If null, the button is disabled.
final VoidCallback? onPressed;

/// The icon
final Widget icon;

/// The tooltip message.
///
/// See also
///
/// * [Tooltip.message]
final String? tooltip;

/// The padding around this button.
///
/// Defaults to 4.0 logical pixels on all sides
final EdgeInsetsDirectional padding;

const SquaredIconButton({
super.key,
required this.onPressed,
required this.icon,
this.tooltip,
this.padding = const EdgeInsetsDirectional.only(
top: 4.0,
bottom: 4.0,
end: 4.0,
),
});

@override
Widget build(BuildContext context) {
final widget = Padding(
padding: padding,
child: InkWell(
borderRadius: BorderRadius.circular(6.0),
onTap: onPressed,
child: Padding(
padding: const EdgeInsetsDirectional.all(2.5),
child: icon,
),
),
);

if (tooltip != null) {
return Tooltip(message: tooltip!, child: widget);
}

return widget;
}
}
3 changes: 2 additions & 1 deletion lib/widgets/collapsable_sidebar.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

import 'package:bluecherry_client/utils/widgets/squared_icon_button.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';

Expand Down Expand Up @@ -102,7 +103,7 @@ class _CollapsableSidebarState extends State<CollapsableSidebar>
: widget.left
? const EdgeInsetsDirectional.only(start: 5.0)
: const EdgeInsetsDirectional.only(end: 5.0),
child: IconButton(
child: SquaredIconButton(
key: collapseButtonKey,
tooltip: collapsed ? loc.expand : loc.collapse,
icon: RotationTransition(
Expand Down
10 changes: 5 additions & 5 deletions lib/widgets/desktop_buttons.dart
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,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/utils/methods.dart';
import 'package:bluecherry_client/utils/widgets/squared_icon_button.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';
Expand Down Expand Up @@ -215,13 +216,12 @@ class _WindowButtonsState extends State<WindowButtons> with WindowListener {
)
else if (home.tab == UnityTab.eventsScreen ||
home.tab == UnityTab.eventsPlayback && !canPop)
IconButton(
SquaredIconButton(
onPressed: () {
eventsScreenKey.currentState?.fetch();
eventsPlaybackScreenKey.currentState?.fetch();
},
icon: const Icon(Icons.refresh),
iconSize: 20.0,
icon: const Icon(Icons.refresh, size: 20.0),
tooltip: loc.refresh,
),
SizedBox(
Expand All @@ -244,7 +244,7 @@ class _WindowButtonsState extends State<WindowButtons> with WindowListener {
final icon = isSelected ? data.selectedIcon : data.icon;
final text = data.text;

return IconButton(
return SquaredIconButton(
icon: AnimatedSwitcher(
duration: const Duration(milliseconds: 200),
child: Icon(
Expand All @@ -254,9 +254,9 @@ class _WindowButtonsState extends State<WindowButtons> with WindowListener {
? theme.colorScheme.primary
: theme.hintColor,
fill: isSelected ? 1.0 : 0.0,
size: 22.0,
),
),
iconSize: 22.0,
tooltip: text,
onPressed: () => home.setTab(data.tab, context),
);
Expand Down
Loading

0 comments on commit 7d8e5da

Please sign in to comment.