From be7f5daa883d2131ad7183d23e227b1632c048cc Mon Sep 17 00:00:00 2001 From: Dr-Blank <64108942+Dr-Blank@users.noreply.github.com> Date: Sat, 15 Jun 2024 23:43:08 -0400 Subject: [PATCH] playback reporting --- .../core/playback_reporter.dart | 252 +++++++++++++++++ .../providers/playback_reporter_provider.dart | 40 +++ .../playback_reporter_provider.g.dart | 26 ++ .../player/providers/audiobook_player.dart | 2 + .../player/providers/audiobook_player.g.dart | 5 +- lib/main.dart | 3 + lib/settings/metadata/metadata_provider.dart | 259 ++++++++++++++++++ .../metadata/metadata_provider.g.dart | 69 +++++ lib/settings/models/app_settings.dart | 1 + lib/settings/models/app_settings.freezed.dart | 37 ++- lib/settings/models/app_settings.g.dart | 5 + lib/theme/theme_from_cover_provider.dart | 2 +- pubspec.lock | 56 ++++ pubspec.yaml | 4 +- 14 files changed, 751 insertions(+), 10 deletions(-) create mode 100644 lib/features/playback_reporting/core/playback_reporter.dart create mode 100644 lib/features/playback_reporting/providers/playback_reporter_provider.dart create mode 100644 lib/features/playback_reporting/providers/playback_reporter_provider.g.dart create mode 100644 lib/settings/metadata/metadata_provider.dart create mode 100644 lib/settings/metadata/metadata_provider.g.dart diff --git a/lib/features/playback_reporting/core/playback_reporter.dart b/lib/features/playback_reporting/core/playback_reporter.dart new file mode 100644 index 0000000..5685d21 --- /dev/null +++ b/lib/features/playback_reporting/core/playback_reporter.dart @@ -0,0 +1,252 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:shelfsdk/audiobookshelf_api.dart'; +import 'package:whispering_pages/features/player/core/audiobook_player.dart'; + +/// this playback reporter will watch the player and report to the server +/// +/// it will by default report every 10 seconds +/// and also report when the player is paused/stopped/finished/playing +class PlaybackReporter { + /// The player to watch + final AudiobookPlayer player; + + /// the api to report to + final AudiobookshelfApi authenticatedApi; + + /// The stopwatch to keep track of the time since the last report + final _stopwatch = Stopwatch(); + + /// subscriptions + final List _subscriptions = []; + + Duration _reportingInterval; + + /// the duration to wait before reporting + Duration get reportingInterval => _reportingInterval; + set reportingInterval(Duration value) { + _reportingInterval = value; + _cancelReportTimer(); + _setReportTimer(); + debugPrint('PlaybackReporter set interval: $value'); + } + + /// the minimum duration to report + final Duration reportingDurationThreshold; + + /// timer to report every 10 seconds + Timer? _reportTimer; + + /// metadata to report + String? deviceName; + String? deviceModel; + String? deviceSdkVersion; + String? deviceClientName; + String? deviceClientVersion; + String? deviceManufacturer; + + PlaybackReporter( + this.player, + this.authenticatedApi, { + this.deviceName, + this.deviceModel, + this.deviceSdkVersion, + this.deviceClientName, + this.deviceClientVersion, + this.deviceManufacturer, + this.reportingDurationThreshold = const Duration(seconds: 1), + Duration reportingInterval = const Duration(seconds: 10), + }) : _reportingInterval = reportingInterval { + // initial conditions + if (player.playing) { + _stopwatch.start(); + _setReportTimer(); + debugPrint('PlaybackReporter starting stopwatch'); + } else { + debugPrint('PlaybackReporter not starting stopwatch'); + } + + _subscriptions.add( + player.playerStateStream.listen((state) async { + // set timer if any book is playing and cancel if not + if (player.book != null && _reportTimer == null) { + _setReportTimer(); + } else if (player.book == null && _reportTimer != null) { + debugPrint('PlaybackReporter book is null, closing session'); + await closeSession(); + _cancelReportTimer(); + } + + // start or stop the stopwatch based on the playing state + if (state.playing) { + _stopwatch.start(); + debugPrint( + 'PlaybackReporter player state observed, starting stopwatch at ${_stopwatch.elapsed}', + ); + } else if (!state.playing) { + _stopwatch.stop(); + debugPrint( + 'PlaybackReporter player state observed, stopping stopwatch at ${_stopwatch.elapsed}', + ); + await syncCurrentPosition(); + } + }), + ); + + debugPrint( + 'PlaybackReporter initialized with interval: $reportingInterval, threshold: $reportingDurationThreshold', + ); + debugPrint( + 'PlaybackReporter initialized with deviceModel: $deviceModel, deviceSdkVersion: $deviceSdkVersion, deviceClientName: $deviceClientName, deviceClientVersion: $deviceClientVersion, deviceManufacturer: $deviceManufacturer', + ); + } + + void tryReportPlayback(_) async { + debugPrint( + 'PlaybackReporter callback called when elapsed ${_stopwatch.elapsed}', + ); + if (_stopwatch.elapsed > reportingDurationThreshold) { + debugPrint( + 'PlaybackReporter reporting now with elapsed ${_stopwatch.elapsed} > threshold $reportingDurationThreshold', + ); + await syncCurrentPosition(); + } + } + + /// dispose the timer + Future dispose() async { + for (var sub in _subscriptions) { + sub.cancel(); + } + await closeSession(); + _stopwatch.stop(); + _reportTimer?.cancel(); + + debugPrint('PlaybackReporter disposed'); + } + + /// current sessionId + /// this is used to report the playback + String? sessionId; + + Future startSession() async { + if (sessionId != null) { + return sessionId!; + } + if (player.book == null) { + throw NoAudiobookPlayingError(); + } + final session = await authenticatedApi.items.play( + libraryItemId: player.book!.libraryItemId, + parameters: PlayItemReqParams( + deviceInfo: await _getDeviceInfo(), + forceDirectPlay: false, + forceTranscode: false, + ), + responseErrorHandler: _responseErrorHandler, + ); + sessionId = session!.id; + debugPrint('PlaybackReporter Started session: $sessionId'); + return sessionId; + } + + Future syncCurrentPosition() async { + try { + sessionId ??= await startSession(); + } on NoAudiobookPlayingError { + debugPrint('PlaybackReporter No audiobook playing to sync position'); + return; + } + final currentPosition = player.position; + + await authenticatedApi.sessions.syncOpen( + sessionId: sessionId!, + parameters: _getSyncData(), + responseErrorHandler: _responseErrorHandler, + ); + + debugPrint( + 'PlaybackReporter Synced position: $currentPosition with timeListened: ${_stopwatch.elapsed} for session: $sessionId', + ); + + // reset the stopwatch + _stopwatch.reset(); + } + + Future closeSession() async { + if (sessionId == null) { + debugPrint('PlaybackReporter No session to close'); + return; + } + + await authenticatedApi.sessions.closeOpen( + sessionId: sessionId!, + parameters: _getSyncData(), + responseErrorHandler: _responseErrorHandler, + ); + sessionId = null; + debugPrint('PlaybackReporter Closed session'); + } + + void _setReportTimer() { + _reportTimer = Timer.periodic(_reportingInterval, tryReportPlayback); + debugPrint('PlaybackReporter set timer with interval: $_reportingInterval'); + } + + void _cancelReportTimer() { + _reportTimer?.cancel(); + _reportTimer = null; + debugPrint('PlaybackReporter cancelled timer'); + } + + void _responseErrorHandler(response, [error]) { + if (response.statusCode != 200) { + debugPrint('PlaybackReporter Error with api: $response, $error'); + throw PlaybackSyncError( + 'Error syncing position: ${response.body}, $error', + ); + } + } + + SyncSessionReqParams _getSyncData() { + return SyncSessionReqParams( + currentTime: player.position, + timeListened: _stopwatch.elapsed, + duration: player.book?.duration ?? Duration.zero, + ); + } + + Future _getDeviceInfo() async { + return DeviceInfoReqParams( + clientVersion: deviceClientVersion, + manufacturer: deviceManufacturer, + model: deviceModel, + sdkVersion: deviceSdkVersion, + clientName: deviceClientName, + deviceName: deviceName, + ); + } +} + +class PlaybackSyncError implements Exception { + String message; + + PlaybackSyncError([this.message = 'Error syncing playback']); + + @override + String toString() { + return 'PlaybackSyncError: $message'; + } +} + +class NoAudiobookPlayingError implements Exception { + String message; + + NoAudiobookPlayingError([this.message = 'No audiobook is playing']); + + @override + String toString() { + return 'NoAudiobookPlayingError: $message'; + } +} diff --git a/lib/features/playback_reporting/providers/playback_reporter_provider.dart b/lib/features/playback_reporting/providers/playback_reporter_provider.dart new file mode 100644 index 0000000..2616696 --- /dev/null +++ b/lib/features/playback_reporting/providers/playback_reporter_provider.dart @@ -0,0 +1,40 @@ +import 'package:package_info_plus/package_info_plus.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:whispering_pages/api/api_provider.dart'; +import 'package:whispering_pages/features/playback_reporting/core/playback_reporter.dart' + as core; +import 'package:whispering_pages/features/player/providers/audiobook_player.dart'; +import 'package:whispering_pages/settings/app_settings_provider.dart'; +import 'package:whispering_pages/settings/metadata/metadata_provider.dart'; + +part 'playback_reporter_provider.g.dart'; + +@Riverpod(keepAlive: true) +class PlaybackReporter extends _$PlaybackReporter { + @override + Future build() async { + final appSettings = ref.watch(appSettingsProvider); + final player = ref.watch(simpleAudiobookPlayerProvider); + final packageInfo = await PackageInfo.fromPlatform(); + final api = ref.watch(authenticatedApiProvider); + final deviceName = await ref.watch(deviceNameProvider.future); + final deviceModel = await ref.watch(deviceModelProvider.future); + final deviceSdkVersion = await ref.watch(deviceSdkVersionProvider.future); + final deviceManufacturer = + await ref.watch(deviceManufacturerProvider.future); + + final reporter = core.PlaybackReporter( + player, + api, + reportingInterval: appSettings.playerSettings.playbackReportInterval, + deviceName: deviceName, + deviceModel: deviceModel, + deviceSdkVersion: deviceSdkVersion, + deviceClientName: packageInfo.appName, + deviceClientVersion: packageInfo.version, + deviceManufacturer: deviceManufacturer, + ); + ref.onDispose(reporter.dispose); + return reporter; + } +} diff --git a/lib/features/playback_reporting/providers/playback_reporter_provider.g.dart b/lib/features/playback_reporting/providers/playback_reporter_provider.g.dart new file mode 100644 index 0000000..2a76328 --- /dev/null +++ b/lib/features/playback_reporting/providers/playback_reporter_provider.g.dart @@ -0,0 +1,26 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'playback_reporter_provider.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$playbackReporterHash() => r'c210b7286d9c151fd59a9ead9eb4a28d1cffdc7c'; + +/// See also [PlaybackReporter]. +@ProviderFor(PlaybackReporter) +final playbackReporterProvider = + AsyncNotifierProvider.internal( + PlaybackReporter.new, + name: r'playbackReporterProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$playbackReporterHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef _$PlaybackReporter = AsyncNotifier; +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/lib/features/player/providers/audiobook_player.dart b/lib/features/player/providers/audiobook_player.dart index e4de66d..593854b 100644 --- a/lib/features/player/providers/audiobook_player.dart +++ b/lib/features/player/providers/audiobook_player.dart @@ -19,6 +19,8 @@ part 'audiobook_player.g.dart'; const playerId = 'audiobook_player'; +/// Simple because it doesn't rebuild when the player state changes +/// it only rebuilds when the token changes @Riverpod(keepAlive: true) class SimpleAudiobookPlayer extends _$SimpleAudiobookPlayer { @override diff --git a/lib/features/player/providers/audiobook_player.g.dart b/lib/features/player/providers/audiobook_player.g.dart index 9787ee3..1ff198c 100644 --- a/lib/features/player/providers/audiobook_player.g.dart +++ b/lib/features/player/providers/audiobook_player.g.dart @@ -9,7 +9,10 @@ part of 'audiobook_player.dart'; String _$simpleAudiobookPlayerHash() => r'b65e6d779476a2c1fa38f617771bf997acb4f5b8'; -/// See also [SimpleAudiobookPlayer]. +/// Simple because it doesn't rebuild when the player state changes +/// it only rebuilds when the token changes +/// +/// Copied from [SimpleAudiobookPlayer]. @ProviderFor(SimpleAudiobookPlayer) final simpleAudiobookPlayerProvider = NotifierProvider.internal( diff --git a/lib/main.dart b/lib/main.dart index 444d17d..9badc47 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -7,6 +7,7 @@ import 'package:just_audio_media_kit/just_audio_media_kit.dart' show JustAudioMediaKit; import 'package:whispering_pages/api/server_provider.dart'; import 'package:whispering_pages/db/storage.dart'; +import 'package:whispering_pages/features/playback_reporting/providers/playback_reporter_provider.dart'; import 'package:whispering_pages/features/player/providers/audiobook_player.dart'; import 'package:whispering_pages/features/sleep_timer/providers/sleep_timer_provider.dart'; import 'package:whispering_pages/router/router.dart'; @@ -85,9 +86,11 @@ class _EagerInitialization extends ConsumerWidget { try { ref.watch(simpleAudiobookPlayerProvider); ref.watch(sleepTimerProvider); + ref.watch(playbackReporterProvider); } catch (e) { debugPrintStack(stackTrace: StackTrace.current, label: e.toString()); } + return child; } } diff --git a/lib/settings/metadata/metadata_provider.dart b/lib/settings/metadata/metadata_provider.dart new file mode 100644 index 0000000..a28766a --- /dev/null +++ b/lib/settings/metadata/metadata_provider.dart @@ -0,0 +1,259 @@ +import 'package:device_info_plus/device_info_plus.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'metadata_provider.g.dart'; + +@Riverpod(keepAlive: true) +Future deviceName(DeviceNameRef ref) async { + final data = await _getDeviceData(DeviceInfoPlugin()); + + // try different keys to get the device name + return + // android + data['product'] ?? + // ios + data['name'] ?? + // linux + data['name'] ?? + // windows + data['computerName'] ?? + // macos + data['model'] ?? + // web + data['browserName'] ?? + 'Unknown name'; +} + +@Riverpod(keepAlive: true) +Future deviceModel(DeviceModelRef ref) async { + final data = await _getDeviceData(DeviceInfoPlugin()); + + // try different keys to get the device model + return + // android, eg: Google Pixel 4 + data['model'] ?? + // ios, eg: iPhone 12 Pro + data['name'] ?? + // linux, eg: Linux Mint 20.1 + data['name'] ?? + // windows, eg: Surface Pro 7 + data['productId'] ?? + // macos, eg: MacBook Pro (13-inch, M1, 2020) + data['model'] ?? + // web, eg: Chrome 87.0.4280.88 + data['browserName'] ?? + 'Unknown model'; +} + +@Riverpod(keepAlive: true) +Future deviceSdkVersion(DeviceSdkVersionRef ref) async { + final data = await _getDeviceData(DeviceInfoPlugin()); + + // try different keys to get the device sdk version + return + // android, eg: 30 + data['version.sdkInt'] ?? + // ios, eg: 14.4 + data['systemVersion'] ?? + // linux, eg: 5.4.0-66-generic + data['version'] ?? + // windows, eg: 10.0.19042 + data['displayVersion'] ?? + // macos, eg: 11.2.1 + data['osRelease'] ?? + // web, eg: 87.0.4280.88 + data['appVersion'] ?? + 'Unknown sdk version'; +} + +@Riverpod(keepAlive: true) +Future deviceManufacturer(DeviceManufacturerRef ref) async { + final data = await _getDeviceData(DeviceInfoPlugin()); + + // try different keys to get the device manufacturer + return + // android, eg: Google + data['manufacturer'] ?? + // ios, eg: Apple + data['manufacturer'] ?? + // linux, eg: Linux + data['idLike'] ?? + // windows, eg: Microsoft + data['productName'] ?? + // macos, eg: Apple + data['manufacturer'] ?? + // web, eg: Google Inc. + data['vendor'] ?? + 'Unknown manufacturer'; +} + +// copied from https://pub.dev/packages/device_info_plus/example +Map _readAndroidBuildData(AndroidDeviceInfo build) { + return { + 'version.securityPatch': build.version.securityPatch, + 'version.sdkInt': build.version.sdkInt, + 'version.release': build.version.release, + 'version.previewSdkInt': build.version.previewSdkInt, + 'version.incremental': build.version.incremental, + 'version.codename': build.version.codename, + 'version.baseOS': build.version.baseOS, + 'board': build.board, + 'bootloader': build.bootloader, + 'brand': build.brand, + 'device': build.device, + 'display': build.display, + 'fingerprint': build.fingerprint, + 'hardware': build.hardware, + 'host': build.host, + 'id': build.id, + 'manufacturer': build.manufacturer, + 'model': build.model, + 'product': build.product, + 'supported32BitAbis': build.supported32BitAbis, + 'supported64BitAbis': build.supported64BitAbis, + 'supportedAbis': build.supportedAbis, + 'tags': build.tags, + 'type': build.type, + 'isPhysicalDevice': build.isPhysicalDevice, + 'systemFeatures': build.systemFeatures, + 'serialNumber': build.serialNumber, + 'isLowRamDevice': build.isLowRamDevice, + }; +} + +Map _readIosDeviceInfo(IosDeviceInfo data) { + return { + 'name': data.name, + 'systemName': data.systemName, + 'systemVersion': data.systemVersion, + 'model': data.model, + 'localizedModel': data.localizedModel, + 'identifierForVendor': data.identifierForVendor, + 'isPhysicalDevice': data.isPhysicalDevice, + 'utsname.sysname:': data.utsname.sysname, + 'utsname.nodename:': data.utsname.nodename, + 'utsname.release:': data.utsname.release, + 'utsname.version:': data.utsname.version, + 'utsname.machine:': data.utsname.machine, + }; +} + +Map _readLinuxDeviceInfo(LinuxDeviceInfo data) { + return { + 'name': data.name, + 'version': data.version, + 'id': data.id, + 'idLike': data.idLike, + 'versionCodename': data.versionCodename, + 'versionId': data.versionId, + 'prettyName': data.prettyName, + 'buildId': data.buildId, + 'variant': data.variant, + 'variantId': data.variantId, + 'machineId': data.machineId, + }; +} + +Map _readWebBrowserInfo(WebBrowserInfo data) { + return { + 'browserName': data.browserName.name, + 'appCodeName': data.appCodeName, + 'appName': data.appName, + 'appVersion': data.appVersion, + 'deviceMemory': data.deviceMemory, + 'language': data.language, + 'languages': data.languages, + 'platform': data.platform, + 'product': data.product, + 'productSub': data.productSub, + 'userAgent': data.userAgent, + 'vendor': data.vendor, + 'vendorSub': data.vendorSub, + 'hardwareConcurrency': data.hardwareConcurrency, + 'maxTouchPoints': data.maxTouchPoints, + }; +} + +Map _readMacOsDeviceInfo(MacOsDeviceInfo data) { + return { + 'computerName': data.computerName, + 'hostName': data.hostName, + 'arch': data.arch, + 'model': data.model, + 'kernelVersion': data.kernelVersion, + 'majorVersion': data.majorVersion, + 'minorVersion': data.minorVersion, + 'patchVersion': data.patchVersion, + 'osRelease': data.osRelease, + 'activeCPUs': data.activeCPUs, + 'memorySize': data.memorySize, + 'cpuFrequency': data.cpuFrequency, + 'systemGUID': data.systemGUID, + }; +} + +Map _readWindowsDeviceInfo(WindowsDeviceInfo data) { + return { + 'numberOfCores': data.numberOfCores, + 'computerName': data.computerName, + 'systemMemoryInMegabytes': data.systemMemoryInMegabytes, + 'userName': data.userName, + 'majorVersion': data.majorVersion, + 'minorVersion': data.minorVersion, + 'buildNumber': data.buildNumber, + 'platformId': data.platformId, + 'csdVersion': data.csdVersion, + 'servicePackMajor': data.servicePackMajor, + 'servicePackMinor': data.servicePackMinor, + 'suitMask': data.suitMask, + 'productType': data.productType, + 'reserved': data.reserved, + 'buildLab': data.buildLab, + 'buildLabEx': data.buildLabEx, + 'digitalProductId': data.digitalProductId, + 'displayVersion': data.displayVersion, + 'editionId': data.editionId, + 'installDate': data.installDate, + 'productId': data.productId, + 'productName': data.productName, + 'registeredOwner': data.registeredOwner, + 'releaseId': data.releaseId, + 'deviceId': data.deviceId, + }; +} + +Future> _getDeviceData( + DeviceInfoPlugin deviceInfoPlugin, +) async { + Map deviceData; + try { + if (kIsWeb) { + deviceData = _readWebBrowserInfo(await deviceInfoPlugin.webBrowserInfo); + } else { + deviceData = switch (defaultTargetPlatform) { + TargetPlatform.android => + _readAndroidBuildData(await deviceInfoPlugin.androidInfo), + TargetPlatform.iOS => + _readIosDeviceInfo(await deviceInfoPlugin.iosInfo), + TargetPlatform.linux => + _readLinuxDeviceInfo(await deviceInfoPlugin.linuxInfo), + TargetPlatform.windows => + _readWindowsDeviceInfo(await deviceInfoPlugin.windowsInfo), + TargetPlatform.macOS => + _readMacOsDeviceInfo(await deviceInfoPlugin.macOsInfo), + TargetPlatform.fuchsia => { + errorKey: 'Fuchsia platform isn\'t supported', + }, + }; + } + } on PlatformException { + deviceData = { + errorKey: 'Failed to get platform version.', + }; + } + return deviceData; +} + +const errorKey = 'Error:'; diff --git a/lib/settings/metadata/metadata_provider.g.dart b/lib/settings/metadata/metadata_provider.g.dart new file mode 100644 index 0000000..6be8d78 --- /dev/null +++ b/lib/settings/metadata/metadata_provider.g.dart @@ -0,0 +1,69 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'metadata_provider.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$deviceNameHash() => r'bc206a3a8c14f3da6e257e92e1ccdc79364f4e28'; + +/// See also [deviceName]. +@ProviderFor(deviceName) +final deviceNameProvider = FutureProvider.internal( + deviceName, + name: r'deviceNameProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') ? null : _$deviceNameHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef DeviceNameRef = FutureProviderRef; +String _$deviceModelHash() => r'3d7e8ef4a37b90f98e38dc8d5f16ca30f71e15b2'; + +/// See also [deviceModel]. +@ProviderFor(deviceModel) +final deviceModelProvider = FutureProvider.internal( + deviceModel, + name: r'deviceModelProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') ? null : _$deviceModelHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef DeviceModelRef = FutureProviderRef; +String _$deviceSdkVersionHash() => r'0553db1a6c90a4db2841761ac2765eb1ba86a714'; + +/// See also [deviceSdkVersion]. +@ProviderFor(deviceSdkVersion) +final deviceSdkVersionProvider = FutureProvider.internal( + deviceSdkVersion, + name: r'deviceSdkVersionProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$deviceSdkVersionHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef DeviceSdkVersionRef = FutureProviderRef; +String _$deviceManufacturerHash() => + r'f0a57e6a92b551fbe266d0a6a29d35dc497882a9'; + +/// See also [deviceManufacturer]. +@ProviderFor(deviceManufacturer) +final deviceManufacturerProvider = FutureProvider.internal( + deviceManufacturer, + name: r'deviceManufacturerProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$deviceManufacturerHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef DeviceManufacturerRef = FutureProviderRef; +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/lib/settings/models/app_settings.dart b/lib/settings/models/app_settings.dart index 48464e4..b57b9f7 100644 --- a/lib/settings/models/app_settings.dart +++ b/lib/settings/models/app_settings.dart @@ -31,6 +31,7 @@ class PlayerSettings with _$PlayerSettings { @Default(1) double preferredDefaultSpeed, @Default([0.75, 1, 1.25, 1.5, 1.75, 2]) List speedOptions, @Default(SleepTimerSettings()) SleepTimerSettings sleepTimerSettings, + @Default(Duration(seconds: 10)) Duration playbackReportInterval, }) = _PlayerSettings; factory PlayerSettings.fromJson(Map json) => diff --git a/lib/settings/models/app_settings.freezed.dart b/lib/settings/models/app_settings.freezed.dart index 6f352f8..e48139b 100644 --- a/lib/settings/models/app_settings.freezed.dart +++ b/lib/settings/models/app_settings.freezed.dart @@ -231,6 +231,7 @@ mixin _$PlayerSettings { List get speedOptions => throw _privateConstructorUsedError; SleepTimerSettings get sleepTimerSettings => throw _privateConstructorUsedError; + Duration get playbackReportInterval => throw _privateConstructorUsedError; Map toJson() => throw _privateConstructorUsedError; @JsonKey(ignore: true) @@ -250,7 +251,8 @@ abstract class $PlayerSettingsCopyWith<$Res> { double preferredDefaultVolume, double preferredDefaultSpeed, List speedOptions, - SleepTimerSettings sleepTimerSettings}); + SleepTimerSettings sleepTimerSettings, + Duration playbackReportInterval}); $MinimizedPlayerSettingsCopyWith<$Res> get miniPlayerSettings; $ExpandedPlayerSettingsCopyWith<$Res> get expandedPlayerSettings; @@ -276,6 +278,7 @@ class _$PlayerSettingsCopyWithImpl<$Res, $Val extends PlayerSettings> Object? preferredDefaultSpeed = null, Object? speedOptions = null, Object? sleepTimerSettings = null, + Object? playbackReportInterval = null, }) { return _then(_value.copyWith( miniPlayerSettings: null == miniPlayerSettings @@ -302,6 +305,10 @@ class _$PlayerSettingsCopyWithImpl<$Res, $Val extends PlayerSettings> ? _value.sleepTimerSettings : sleepTimerSettings // ignore: cast_nullable_to_non_nullable as SleepTimerSettings, + playbackReportInterval: null == playbackReportInterval + ? _value.playbackReportInterval + : playbackReportInterval // ignore: cast_nullable_to_non_nullable + as Duration, ) as $Val); } @@ -347,7 +354,8 @@ abstract class _$$PlayerSettingsImplCopyWith<$Res> double preferredDefaultVolume, double preferredDefaultSpeed, List speedOptions, - SleepTimerSettings sleepTimerSettings}); + SleepTimerSettings sleepTimerSettings, + Duration playbackReportInterval}); @override $MinimizedPlayerSettingsCopyWith<$Res> get miniPlayerSettings; @@ -374,6 +382,7 @@ class __$$PlayerSettingsImplCopyWithImpl<$Res> Object? preferredDefaultSpeed = null, Object? speedOptions = null, Object? sleepTimerSettings = null, + Object? playbackReportInterval = null, }) { return _then(_$PlayerSettingsImpl( miniPlayerSettings: null == miniPlayerSettings @@ -400,6 +409,10 @@ class __$$PlayerSettingsImplCopyWithImpl<$Res> ? _value.sleepTimerSettings : sleepTimerSettings // ignore: cast_nullable_to_non_nullable as SleepTimerSettings, + playbackReportInterval: null == playbackReportInterval + ? _value.playbackReportInterval + : playbackReportInterval // ignore: cast_nullable_to_non_nullable + as Duration, )); } } @@ -413,7 +426,8 @@ class _$PlayerSettingsImpl implements _PlayerSettings { this.preferredDefaultVolume = 1, this.preferredDefaultSpeed = 1, final List speedOptions = const [0.75, 1, 1.25, 1.5, 1.75, 2], - this.sleepTimerSettings = const SleepTimerSettings()}) + this.sleepTimerSettings = const SleepTimerSettings(), + this.playbackReportInterval = const Duration(seconds: 10)}) : _speedOptions = speedOptions; factory _$PlayerSettingsImpl.fromJson(Map json) => @@ -443,10 +457,13 @@ class _$PlayerSettingsImpl implements _PlayerSettings { @override @JsonKey() final SleepTimerSettings sleepTimerSettings; + @override + @JsonKey() + final Duration playbackReportInterval; @override String toString() { - return 'PlayerSettings(miniPlayerSettings: $miniPlayerSettings, expandedPlayerSettings: $expandedPlayerSettings, preferredDefaultVolume: $preferredDefaultVolume, preferredDefaultSpeed: $preferredDefaultSpeed, speedOptions: $speedOptions, sleepTimerSettings: $sleepTimerSettings)'; + return 'PlayerSettings(miniPlayerSettings: $miniPlayerSettings, expandedPlayerSettings: $expandedPlayerSettings, preferredDefaultVolume: $preferredDefaultVolume, preferredDefaultSpeed: $preferredDefaultSpeed, speedOptions: $speedOptions, sleepTimerSettings: $sleepTimerSettings, playbackReportInterval: $playbackReportInterval)'; } @override @@ -465,7 +482,9 @@ class _$PlayerSettingsImpl implements _PlayerSettings { const DeepCollectionEquality() .equals(other._speedOptions, _speedOptions) && (identical(other.sleepTimerSettings, sleepTimerSettings) || - other.sleepTimerSettings == sleepTimerSettings)); + other.sleepTimerSettings == sleepTimerSettings) && + (identical(other.playbackReportInterval, playbackReportInterval) || + other.playbackReportInterval == playbackReportInterval)); } @JsonKey(ignore: true) @@ -477,7 +496,8 @@ class _$PlayerSettingsImpl implements _PlayerSettings { preferredDefaultVolume, preferredDefaultSpeed, const DeepCollectionEquality().hash(_speedOptions), - sleepTimerSettings); + sleepTimerSettings, + playbackReportInterval); @JsonKey(ignore: true) @override @@ -501,7 +521,8 @@ abstract class _PlayerSettings implements PlayerSettings { final double preferredDefaultVolume, final double preferredDefaultSpeed, final List speedOptions, - final SleepTimerSettings sleepTimerSettings}) = _$PlayerSettingsImpl; + final SleepTimerSettings sleepTimerSettings, + final Duration playbackReportInterval}) = _$PlayerSettingsImpl; factory _PlayerSettings.fromJson(Map json) = _$PlayerSettingsImpl.fromJson; @@ -519,6 +540,8 @@ abstract class _PlayerSettings implements PlayerSettings { @override SleepTimerSettings get sleepTimerSettings; @override + Duration get playbackReportInterval; + @override @JsonKey(ignore: true) _$$PlayerSettingsImplCopyWith<_$PlayerSettingsImpl> get copyWith => throw _privateConstructorUsedError; diff --git a/lib/settings/models/app_settings.g.dart b/lib/settings/models/app_settings.g.dart index 55cf0e2..434f306 100644 --- a/lib/settings/models/app_settings.g.dart +++ b/lib/settings/models/app_settings.g.dart @@ -46,6 +46,10 @@ _$PlayerSettingsImpl _$$PlayerSettingsImplFromJson(Map json) => ? const SleepTimerSettings() : SleepTimerSettings.fromJson( json['sleepTimerSettings'] as Map), + playbackReportInterval: json['playbackReportInterval'] == null + ? const Duration(seconds: 10) + : Duration( + microseconds: (json['playbackReportInterval'] as num).toInt()), ); Map _$$PlayerSettingsImplToJson( @@ -57,6 +61,7 @@ Map _$$PlayerSettingsImplToJson( 'preferredDefaultSpeed': instance.preferredDefaultSpeed, 'speedOptions': instance.speedOptions, 'sleepTimerSettings': instance.sleepTimerSettings, + 'playbackReportInterval': instance.playbackReportInterval.inMicroseconds, }; _$ExpandedPlayerSettingsImpl _$$ExpandedPlayerSettingsImplFromJson( diff --git a/lib/theme/theme_from_cover_provider.dart b/lib/theme/theme_from_cover_provider.dart index ea50661..49310bd 100644 --- a/lib/theme/theme_from_cover_provider.dart +++ b/lib/theme/theme_from_cover_provider.dart @@ -12,7 +12,7 @@ Future> themeFromCover( ImageProvider img, { Brightness brightness = Brightness.dark, }) async { - // ! add deliberate delay to simulate a long running task + // ! add deliberate delay to simulate a long running task as it interferes with other animations await Future.delayed(500.ms); debugPrint('Generating color scheme from cover image'); diff --git a/pubspec.lock b/pubspec.lock index b198092..a83d68a 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -337,6 +337,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.6" + device_info_plus: + dependency: "direct main" + description: + name: device_info_plus + sha256: eead12d1a1ed83d8283ab4c2f3fca23ac4082f29f25f29dff0f758f57d06ec91 + url: "https://pub.dev" + source: hosted + version: "10.1.0" + device_info_plus_platform_interface: + dependency: transitive + description: + name: device_info_plus_platform_interface + sha256: d3b01d5868b50ae571cd1dc6e502fc94d956b665756180f7b16ead09e836fd64 + url: "https://pub.dev" + source: hosted + version: "7.0.0" duration_picker: dependency: "direct main" description: @@ -847,6 +863,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.0" + package_info_plus: + dependency: "direct main" + description: + name: package_info_plus + sha256: b93d8b4d624b4ea19b0a5a208b2d6eff06004bc3ce74c06040b120eeadd00ce0 + url: "https://pub.dev" + source: hosted + version: "8.0.0" + package_info_plus_platform_interface: + dependency: transitive + description: + name: package_info_plus_platform_interface + sha256: f49918f3433a3146047372f9d4f1f847511f2acd5cd030e1f44fe5a50036b70e + url: "https://pub.dev" + source: hosted + version: "3.0.0" path: dependency: "direct main" description: @@ -1015,6 +1047,22 @@ packages: url: "https://pub.dev" source: hosted version: "0.0.5" + sensors_plus: + dependency: "direct main" + description: + name: sensors_plus + sha256: "6898cd4490ffc27fea4de5976585e92fae55355175d46c6c3b3d719d42f9e230" + url: "https://pub.dev" + source: hosted + version: "5.0.1" + sensors_plus_platform_interface: + dependency: transitive + description: + name: sensors_plus_platform_interface + sha256: bc472d6cfd622acb4f020e726433ee31788b038056691ba433fec80e448a094f + url: "https://pub.dev" + source: hosted + version: "1.2.0" shelf: dependency: transitive description: @@ -1339,6 +1387,14 @@ packages: url: "https://pub.dev" source: hosted version: "5.5.1" + win32_registry: + dependency: transitive + description: + name: win32_registry + sha256: "10589e0d7f4e053f2c61023a31c9ce01146656a70b7b7f0828c0b46d7da2a9bb" + url: "https://pub.dev" + source: hosted + version: "1.1.3" xdg_directories: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 8935e34..418468f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -39,6 +39,7 @@ dependencies: coast: ^2.0.2 collection: ^1.18.0 cupertino_icons: ^1.0.6 + device_info_plus: ^10.1.0 duration_picker: ^1.2.0 easy_stepper: ^0.8.4 flutter: @@ -66,12 +67,13 @@ dependencies: miniplayer: path: ../miniplayer numberpicker: ^2.1.2 + package_info_plus: ^8.0.0 path: ^1.9.0 path_provider: ^2.1.0 riverpod_annotation: ^2.3.5 rxdart: ^0.27.7 scroll_loop_auto_scroll: ^0.0.5 - # sensors_plus: ^5.0.1 + sensors_plus: ^5.0.1 shelfsdk: path: ../../_dart/shelfsdk shimmer: ^3.0.0