mirror of
https://github.com/Dr-Blank/Vaani.git
synced 2025-08-14 22:52:35 +02:00
use logging package
This commit is contained in:
parent
99fb8264f1
commit
f24e63d852
14 changed files with 135 additions and 86 deletions
|
@ -20,7 +20,7 @@ pluginManagement {
|
||||||
plugins {
|
plugins {
|
||||||
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
||||||
id "com.android.application" version "7.3.0" apply false
|
id "com.android.application" version "7.3.0" apply false
|
||||||
id "org.jetbrains.kotlin.android" version "1.7.10" apply false
|
id "org.jetbrains.kotlin.android" version "2.0.0" apply false
|
||||||
}
|
}
|
||||||
|
|
||||||
include ":app"
|
include ":app"
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
import 'package:shelfsdk/audiobookshelf_api.dart';
|
import 'package:shelfsdk/audiobookshelf_api.dart';
|
||||||
import 'package:whispering_pages/db/cache_manager.dart';
|
import 'package:whispering_pages/db/cache_manager.dart';
|
||||||
|
@ -10,6 +10,8 @@ import 'package:whispering_pages/settings/api_settings_provider.dart';
|
||||||
|
|
||||||
part 'api_provider.g.dart';
|
part 'api_provider.g.dart';
|
||||||
|
|
||||||
|
final _logger = Logger('api_provider');
|
||||||
|
|
||||||
Uri makeBaseUrl(String address) {
|
Uri makeBaseUrl(String address) {
|
||||||
if (!address.startsWith('http') && !address.startsWith('https')) {
|
if (!address.startsWith('http') && !address.startsWith('https')) {
|
||||||
address = 'https://$address';
|
address = 'https://$address';
|
||||||
|
@ -103,7 +105,7 @@ class PersonalizedView extends _$PersonalizedView {
|
||||||
for (final item in resJson)
|
for (final item in resJson)
|
||||||
Shelf.fromJson(item as Map<String, dynamic>),
|
Shelf.fromJson(item as Map<String, dynamic>),
|
||||||
];
|
];
|
||||||
debugPrint('reading from cache: $cachedRes');
|
_logger.fine('reading from cache: $cachedRes');
|
||||||
yield res;
|
yield res;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -119,7 +121,7 @@ class PersonalizedView extends _$PersonalizedView {
|
||||||
fileExtension: 'json',
|
fileExtension: 'json',
|
||||||
key: key,
|
key: key,
|
||||||
);
|
);
|
||||||
debugPrint('writing to cache: $newFile');
|
_logger.fine('writing to cache: $newFile');
|
||||||
yield res!;
|
yield res!;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,16 +1,19 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
import 'package:whispering_pages/api/server_provider.dart'
|
import 'package:whispering_pages/api/server_provider.dart'
|
||||||
show audiobookShelfServerProvider;
|
show audiobookShelfServerProvider;
|
||||||
import 'package:whispering_pages/settings/models/audiobookshelf_server.dart';
|
|
||||||
import 'package:whispering_pages/settings/models/authenticated_user.dart' as model;
|
|
||||||
import 'package:whispering_pages/settings/api_settings_provider.dart';
|
|
||||||
import 'package:whispering_pages/db/storage.dart';
|
import 'package:whispering_pages/db/storage.dart';
|
||||||
|
import 'package:whispering_pages/settings/api_settings_provider.dart';
|
||||||
|
import 'package:whispering_pages/settings/models/audiobookshelf_server.dart';
|
||||||
|
import 'package:whispering_pages/settings/models/authenticated_user.dart'
|
||||||
|
as model;
|
||||||
|
|
||||||
part 'authenticated_user_provider.g.dart';
|
part 'authenticated_user_provider.g.dart';
|
||||||
|
|
||||||
final _box = AvailableHiveBoxes.authenticatedUserBox;
|
final _box = AvailableHiveBoxes.authenticatedUserBox;
|
||||||
|
|
||||||
|
final _logger = Logger('authenticated_user_provider');
|
||||||
|
|
||||||
/// provides with a set of authenticated users
|
/// provides with a set of authenticated users
|
||||||
@riverpod
|
@riverpod
|
||||||
class AuthenticatedUser extends _$AuthenticatedUser {
|
class AuthenticatedUser extends _$AuthenticatedUser {
|
||||||
|
@ -32,10 +35,10 @@ class AuthenticatedUser extends _$AuthenticatedUser {
|
||||||
Set<model.AuthenticatedUser> readFromBoxOrCreate() {
|
Set<model.AuthenticatedUser> readFromBoxOrCreate() {
|
||||||
if (_box.isNotEmpty) {
|
if (_box.isNotEmpty) {
|
||||||
final foundData = _box.getRange(0, _box.length);
|
final foundData = _box.getRange(0, _box.length);
|
||||||
debugPrint('found users in box: $foundData');
|
_logger.fine('found users in box: $foundData');
|
||||||
return foundData.toSet();
|
return foundData.toSet();
|
||||||
} else {
|
} else {
|
||||||
debugPrint('no settings found in box');
|
_logger.fine('no settings found in box');
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -46,7 +49,7 @@ class AuthenticatedUser extends _$AuthenticatedUser {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_box.addAll(state);
|
_box.addAll(state);
|
||||||
debugPrint('writing state to box: $state');
|
_logger.fine('writing state to box: $state');
|
||||||
}
|
}
|
||||||
|
|
||||||
void addUser(model.AuthenticatedUser user) {
|
void addUser(model.AuthenticatedUser user) {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
import 'package:shelfsdk/audiobookshelf_api.dart';
|
import 'package:shelfsdk/audiobookshelf_api.dart';
|
||||||
import 'package:whispering_pages/api/api_provider.dart';
|
import 'package:whispering_pages/api/api_provider.dart';
|
||||||
|
@ -14,6 +14,8 @@ import 'package:whispering_pages/db/cache_manager.dart';
|
||||||
|
|
||||||
part 'image_provider.g.dart';
|
part 'image_provider.g.dart';
|
||||||
|
|
||||||
|
final _logger = Logger('cover_image_provider');
|
||||||
|
|
||||||
@Riverpod(keepAlive: true)
|
@Riverpod(keepAlive: true)
|
||||||
class CoverImage extends _$CoverImage {
|
class CoverImage extends _$CoverImage {
|
||||||
@override
|
@override
|
||||||
|
@ -29,23 +31,23 @@ class CoverImage extends _$CoverImage {
|
||||||
|
|
||||||
if (file != null) {
|
if (file != null) {
|
||||||
// if the image is in the cache, yield it
|
// if the image is in the cache, yield it
|
||||||
debugPrint(
|
_logger.fine(
|
||||||
'cover image found in cache for ${libraryItem.id} at ${file.file.path}',
|
'cover image found in cache for ${libraryItem.id} at ${file.file.path}',
|
||||||
);
|
);
|
||||||
yield await file.file.readAsBytes();
|
yield await file.file.readAsBytes();
|
||||||
// return if no need to fetch from the server
|
// return if no need to fetch from the server
|
||||||
if (libraryItem.updatedAt.isBefore(await file.file.lastModified())) {
|
if (libraryItem.updatedAt.isBefore(await file.file.lastModified())) {
|
||||||
debugPrint(
|
_logger.fine(
|
||||||
'cover image is up to date for ${libraryItem.id}, no need to fetch from the server',
|
'cover image is up to date for ${libraryItem.id}, no need to fetch from the server',
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
debugPrint(
|
_logger.fine(
|
||||||
'cover image stale for ${libraryItem.id}, fetching from the server',
|
'cover image stale for ${libraryItem.id}, fetching from the server',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
debugPrint('cover image not found in cache for ${libraryItem.id}');
|
_logger.fine('cover image not found in cache for ${libraryItem.id}');
|
||||||
}
|
}
|
||||||
|
|
||||||
// check if the image is in the cache
|
// check if the image is in the cache
|
||||||
|
@ -61,7 +63,7 @@ class CoverImage extends _$CoverImage {
|
||||||
key: libraryItem.id,
|
key: libraryItem.id,
|
||||||
fileExtension: 'jpg',
|
fileExtension: 'jpg',
|
||||||
);
|
);
|
||||||
debugPrint(
|
_logger.fine(
|
||||||
'cover image fetched for for ${libraryItem.id}, file time: ${await newFile.lastModified()}',
|
'cover image fetched for for ${libraryItem.id}, file time: ${await newFile.lastModified()}',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
import 'package:shelfsdk/audiobookshelf_api.dart' as shelfsdk;
|
import 'package:shelfsdk/audiobookshelf_api.dart' as shelfsdk;
|
||||||
import 'package:whispering_pages/api/api_provider.dart';
|
import 'package:whispering_pages/api/api_provider.dart';
|
||||||
|
@ -10,6 +10,8 @@ import 'package:whispering_pages/shared/extensions/model_conversions.dart';
|
||||||
|
|
||||||
part 'library_item_provider.g.dart';
|
part 'library_item_provider.g.dart';
|
||||||
|
|
||||||
|
final _logger = Logger('LibraryItemProvider');
|
||||||
|
|
||||||
/// provides the library item for the given id
|
/// provides the library item for the given id
|
||||||
@riverpod
|
@riverpod
|
||||||
class LibraryItem extends _$LibraryItem {
|
class LibraryItem extends _$LibraryItem {
|
||||||
|
@ -17,7 +19,7 @@ class LibraryItem extends _$LibraryItem {
|
||||||
Stream<shelfsdk.LibraryItemExpanded> build(String id) async* {
|
Stream<shelfsdk.LibraryItemExpanded> build(String id) async* {
|
||||||
final api = ref.watch(authenticatedApiProvider);
|
final api = ref.watch(authenticatedApiProvider);
|
||||||
|
|
||||||
debugPrint('LibraryItemProvider fetching library item: $id');
|
_logger.fine('LibraryItemProvider fetching library item: $id');
|
||||||
|
|
||||||
// ! this is a mock delay
|
// ! this is a mock delay
|
||||||
// await Future.delayed(const Duration(seconds: 10));
|
// await Future.delayed(const Duration(seconds: 10));
|
||||||
|
@ -27,14 +29,15 @@ class LibraryItem extends _$LibraryItem {
|
||||||
final cachedFile = await apiResponseCacheManager.getFileFromMemory(key) ??
|
final cachedFile = await apiResponseCacheManager.getFileFromMemory(key) ??
|
||||||
await apiResponseCacheManager.getFileFromCache(key);
|
await apiResponseCacheManager.getFileFromCache(key);
|
||||||
if (cachedFile != null) {
|
if (cachedFile != null) {
|
||||||
debugPrint('LibraryItemProvider reading from cache for $id from ${cachedFile.file}');
|
_logger.fine(
|
||||||
|
'LibraryItemProvider reading from cache for $id from ${cachedFile.file}');
|
||||||
// read file as json
|
// read file as json
|
||||||
final cachedItem = shelfsdk.LibraryItemExpanded.fromJson(
|
final cachedItem = shelfsdk.LibraryItemExpanded.fromJson(
|
||||||
jsonDecode(await cachedFile.file.readAsString()),
|
jsonDecode(await cachedFile.file.readAsString()),
|
||||||
);
|
);
|
||||||
yield cachedItem;
|
yield cachedItem;
|
||||||
} else {
|
} else {
|
||||||
debugPrint('LibraryItemProvider cache miss for $id');
|
_logger.fine('LibraryItemProvider cache miss for $id');
|
||||||
}
|
}
|
||||||
|
|
||||||
// ! this is a mock delay
|
// ! this is a mock delay
|
||||||
|
@ -60,7 +63,7 @@ class LibraryItem extends _$LibraryItem {
|
||||||
fileExtension: 'json',
|
fileExtension: 'json',
|
||||||
key: key,
|
key: key,
|
||||||
);
|
);
|
||||||
debugPrint('writing to cache: $newFile');
|
_logger.fine('writing to cache: $newFile');
|
||||||
yield item.asExpanded;
|
yield item.asExpanded;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:shelfsdk/audiobookshelf_api.dart';
|
import 'package:shelfsdk/audiobookshelf_api.dart';
|
||||||
import 'package:whispering_pages/features/player/core/audiobook_player.dart';
|
import 'package:whispering_pages/features/player/core/audiobook_player.dart';
|
||||||
|
|
||||||
|
final _logger = Logger('PlaybackReporter');
|
||||||
|
|
||||||
/// this playback reporter will watch the player and report to the server
|
/// this playback reporter will watch the player and report to the server
|
||||||
///
|
///
|
||||||
/// it will by default report every 10 seconds
|
/// it will by default report every 10 seconds
|
||||||
|
@ -29,7 +31,7 @@ class PlaybackReporter {
|
||||||
_reportingInterval = value;
|
_reportingInterval = value;
|
||||||
_cancelReportTimer();
|
_cancelReportTimer();
|
||||||
_setReportTimer();
|
_setReportTimer();
|
||||||
debugPrint('PlaybackReporter set interval: $value');
|
_logger.info('set interval: $value');
|
||||||
}
|
}
|
||||||
|
|
||||||
/// the minimum duration to report
|
/// the minimum duration to report
|
||||||
|
@ -62,9 +64,9 @@ class PlaybackReporter {
|
||||||
if (player.playing) {
|
if (player.playing) {
|
||||||
_stopwatch.start();
|
_stopwatch.start();
|
||||||
_setReportTimer();
|
_setReportTimer();
|
||||||
debugPrint('PlaybackReporter starting stopwatch');
|
_logger.fine('starting stopwatch');
|
||||||
} else {
|
} else {
|
||||||
debugPrint('PlaybackReporter not starting stopwatch');
|
_logger.fine('not starting stopwatch');
|
||||||
}
|
}
|
||||||
|
|
||||||
_subscriptions.add(
|
_subscriptions.add(
|
||||||
|
@ -73,7 +75,7 @@ class PlaybackReporter {
|
||||||
if (player.book != null && _reportTimer == null) {
|
if (player.book != null && _reportTimer == null) {
|
||||||
_setReportTimer();
|
_setReportTimer();
|
||||||
} else if (player.book == null && _reportTimer != null) {
|
} else if (player.book == null && _reportTimer != null) {
|
||||||
debugPrint('PlaybackReporter book is null, closing session');
|
_logger.info('book is null, closing session');
|
||||||
await closeSession();
|
await closeSession();
|
||||||
_cancelReportTimer();
|
_cancelReportTimer();
|
||||||
}
|
}
|
||||||
|
@ -81,34 +83,34 @@ class PlaybackReporter {
|
||||||
// start or stop the stopwatch based on the playing state
|
// start or stop the stopwatch based on the playing state
|
||||||
if (state.playing) {
|
if (state.playing) {
|
||||||
_stopwatch.start();
|
_stopwatch.start();
|
||||||
debugPrint(
|
_logger.fine(
|
||||||
'PlaybackReporter player state observed, starting stopwatch at ${_stopwatch.elapsed}',
|
'player state observed, starting stopwatch at ${_stopwatch.elapsed}',
|
||||||
);
|
);
|
||||||
} else if (!state.playing) {
|
} else if (!state.playing) {
|
||||||
_stopwatch.stop();
|
_stopwatch.stop();
|
||||||
debugPrint(
|
_logger.fine(
|
||||||
'PlaybackReporter player state observed, stopping stopwatch at ${_stopwatch.elapsed}',
|
'player state observed, stopping stopwatch at ${_stopwatch.elapsed}',
|
||||||
);
|
);
|
||||||
await syncCurrentPosition();
|
await syncCurrentPosition();
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
debugPrint(
|
_logger.fine(
|
||||||
'PlaybackReporter initialized with interval: $reportingInterval, threshold: $reportingDurationThreshold',
|
'initialized with interval: $reportingInterval, threshold: $reportingDurationThreshold',
|
||||||
);
|
);
|
||||||
debugPrint(
|
_logger.fine(
|
||||||
'PlaybackReporter initialized with deviceModel: $deviceModel, deviceSdkVersion: $deviceSdkVersion, deviceClientName: $deviceClientName, deviceClientVersion: $deviceClientVersion, deviceManufacturer: $deviceManufacturer',
|
'initialized with deviceModel: $deviceModel, deviceSdkVersion: $deviceSdkVersion, deviceClientName: $deviceClientName, deviceClientVersion: $deviceClientVersion, deviceManufacturer: $deviceManufacturer',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void tryReportPlayback(_) async {
|
void tryReportPlayback(_) async {
|
||||||
debugPrint(
|
_logger.fine(
|
||||||
'PlaybackReporter callback called when elapsed ${_stopwatch.elapsed}',
|
'callback called when elapsed ${_stopwatch.elapsed}',
|
||||||
);
|
);
|
||||||
if (_stopwatch.elapsed > reportingDurationThreshold) {
|
if (_stopwatch.elapsed > reportingDurationThreshold) {
|
||||||
debugPrint(
|
_logger.fine(
|
||||||
'PlaybackReporter reporting now with elapsed ${_stopwatch.elapsed} > threshold $reportingDurationThreshold',
|
'reporting now with elapsed ${_stopwatch.elapsed} > threshold $reportingDurationThreshold',
|
||||||
);
|
);
|
||||||
await syncCurrentPosition();
|
await syncCurrentPosition();
|
||||||
}
|
}
|
||||||
|
@ -123,7 +125,7 @@ class PlaybackReporter {
|
||||||
_stopwatch.stop();
|
_stopwatch.stop();
|
||||||
_reportTimer?.cancel();
|
_reportTimer?.cancel();
|
||||||
|
|
||||||
debugPrint('PlaybackReporter disposed');
|
_logger.fine('disposed');
|
||||||
}
|
}
|
||||||
|
|
||||||
/// current sessionId
|
/// current sessionId
|
||||||
|
@ -147,7 +149,7 @@ class PlaybackReporter {
|
||||||
),
|
),
|
||||||
responseErrorHandler: _responseErrorHandler,
|
responseErrorHandler: _responseErrorHandler,
|
||||||
);
|
);
|
||||||
debugPrint('PlaybackReporter Started session: $sessionId');
|
_logger.info('Started session: $sessionId');
|
||||||
return _session;
|
return _session;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -159,7 +161,7 @@ class PlaybackReporter {
|
||||||
try {
|
try {
|
||||||
_session ??= await startSession();
|
_session ??= await startSession();
|
||||||
} on NoAudiobookPlayingError {
|
} on NoAudiobookPlayingError {
|
||||||
debugPrint('PlaybackReporter No audiobook playing to sync position');
|
_logger.warning('No audiobook playing to sync position');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final currentPosition = player.positionInBook;
|
final currentPosition = player.positionInBook;
|
||||||
|
@ -170,8 +172,8 @@ class PlaybackReporter {
|
||||||
responseErrorHandler: _responseErrorHandler,
|
responseErrorHandler: _responseErrorHandler,
|
||||||
);
|
);
|
||||||
|
|
||||||
debugPrint(
|
_logger.fine(
|
||||||
'PlaybackReporter Synced position: $currentPosition with timeListened: ${_stopwatch.elapsed} for session: $sessionId',
|
'Synced position: $currentPosition with timeListened: ${_stopwatch.elapsed} for session: $sessionId',
|
||||||
);
|
);
|
||||||
|
|
||||||
// reset the stopwatch
|
// reset the stopwatch
|
||||||
|
@ -180,7 +182,7 @@ class PlaybackReporter {
|
||||||
|
|
||||||
Future<void> closeSession() async {
|
Future<void> closeSession() async {
|
||||||
if (sessionId == null) {
|
if (sessionId == null) {
|
||||||
debugPrint('PlaybackReporter No session to close');
|
_logger.warning('No session to close');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -190,23 +192,23 @@ class PlaybackReporter {
|
||||||
responseErrorHandler: _responseErrorHandler,
|
responseErrorHandler: _responseErrorHandler,
|
||||||
);
|
);
|
||||||
_session = null;
|
_session = null;
|
||||||
debugPrint('PlaybackReporter Closed session');
|
_logger.info('Closed session');
|
||||||
}
|
}
|
||||||
|
|
||||||
void _setReportTimer() {
|
void _setReportTimer() {
|
||||||
_reportTimer = Timer.periodic(_reportingInterval, tryReportPlayback);
|
_reportTimer = Timer.periodic(_reportingInterval, tryReportPlayback);
|
||||||
debugPrint('PlaybackReporter set timer with interval: $_reportingInterval');
|
_logger.fine('set timer with interval: $_reportingInterval');
|
||||||
}
|
}
|
||||||
|
|
||||||
void _cancelReportTimer() {
|
void _cancelReportTimer() {
|
||||||
_reportTimer?.cancel();
|
_reportTimer?.cancel();
|
||||||
_reportTimer = null;
|
_reportTimer = null;
|
||||||
debugPrint('PlaybackReporter cancelled timer');
|
_logger.fine('cancelled timer');
|
||||||
}
|
}
|
||||||
|
|
||||||
void _responseErrorHandler(response, [error]) {
|
void _responseErrorHandler(response, [error]) {
|
||||||
if (response.statusCode != 200) {
|
if (response.statusCode != 200) {
|
||||||
debugPrint('PlaybackReporter Error with api: $response, $error');
|
_logger.shout('Error with api: $response, $error');
|
||||||
throw PlaybackSyncError(
|
throw PlaybackSyncError(
|
||||||
'Error syncing position: ${response.body}, $error',
|
'Error syncing position: ${response.body}, $error',
|
||||||
);
|
);
|
||||||
|
@ -215,8 +217,8 @@ class PlaybackReporter {
|
||||||
|
|
||||||
SyncSessionReqParams? _getSyncData() {
|
SyncSessionReqParams? _getSyncData() {
|
||||||
if (player.book?.libraryItemId != _session?.libraryItemId) {
|
if (player.book?.libraryItemId != _session?.libraryItemId) {
|
||||||
debugPrint(
|
_logger.info(
|
||||||
'PlaybackReporter Book changed, not syncing position for session: $sessionId',
|
'Book changed, not syncing position for session: $sessionId',
|
||||||
);
|
);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_animate/flutter_animate.dart';
|
import 'package:flutter_animate/flutter_animate.dart';
|
||||||
import 'package:just_audio/just_audio.dart';
|
import 'package:just_audio/just_audio.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
import 'package:whispering_pages/features/player/core/audiobook_player.dart';
|
import 'package:whispering_pages/features/player/core/audiobook_player.dart';
|
||||||
|
|
||||||
/// this timer pauses the music player after a certain duration
|
/// this timer pauses the music player after a certain duration
|
||||||
|
@ -10,6 +10,8 @@ import 'package:whispering_pages/features/player/core/audiobook_player.dart';
|
||||||
/// watches the state of the music player and pauses it when the timer is up
|
/// watches the state of the music player and pauses it when the timer is up
|
||||||
/// timer is cancelled when the music player is paused or stopped
|
/// timer is cancelled when the music player is paused or stopped
|
||||||
class SleepTimer {
|
class SleepTimer {
|
||||||
|
final _logger = Logger('SleepTimer');
|
||||||
|
|
||||||
/// The duration after which the music player will be paused
|
/// The duration after which the music player will be paused
|
||||||
Duration _duration;
|
Duration _duration;
|
||||||
|
|
||||||
|
@ -54,15 +56,15 @@ class SleepTimer {
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
debugPrint('SleepTimer created with duration: $duration');
|
_logger.fine('created with duration: $duration');
|
||||||
}
|
}
|
||||||
|
|
||||||
/// resets the timer
|
/// resets the timer
|
||||||
void reset() {
|
void reset() {
|
||||||
if (timer != null) {
|
if (timer != null) {
|
||||||
timer!.cancel();
|
timer!.cancel();
|
||||||
debugPrint(
|
_logger.fine(
|
||||||
'SleepTimer cancelled timer, remaining time: $remainingTime, duration: $duration',
|
'cancelled timer, remaining time: $remainingTime, duration: $duration',
|
||||||
);
|
);
|
||||||
timer = null;
|
timer = null;
|
||||||
}
|
}
|
||||||
|
@ -77,10 +79,10 @@ class SleepTimer {
|
||||||
timer = Timer(duration, () {
|
timer = Timer(duration, () {
|
||||||
player.pause();
|
player.pause();
|
||||||
reset();
|
reset();
|
||||||
debugPrint('SleepTimer paused player after $duration');
|
_logger.fine('paused player after $duration');
|
||||||
});
|
});
|
||||||
startedAt = DateTime.now();
|
startedAt = DateTime.now();
|
||||||
debugPrint('SleepTimer started for $duration at $startedAt');
|
_logger.fine('started for $duration at $startedAt');
|
||||||
}
|
}
|
||||||
|
|
||||||
Duration get remainingTime {
|
Duration get remainingTime {
|
||||||
|
@ -105,6 +107,6 @@ class SleepTimer {
|
||||||
for (var sub in _subscriptions) {
|
for (var sub in _subscriptions) {
|
||||||
sub.cancel();
|
sub.cancel();
|
||||||
}
|
}
|
||||||
debugPrint('SleepTimer disposed');
|
_logger.fine('disposed');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ import 'package:just_audio_background/just_audio_background.dart'
|
||||||
show JustAudioBackground;
|
show JustAudioBackground;
|
||||||
import 'package:just_audio_media_kit/just_audio_media_kit.dart'
|
import 'package:just_audio_media_kit/just_audio_media_kit.dart'
|
||||||
show JustAudioMediaKit;
|
show JustAudioMediaKit;
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
import 'package:whispering_pages/api/server_provider.dart';
|
import 'package:whispering_pages/api/server_provider.dart';
|
||||||
import 'package:whispering_pages/db/storage.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/playback_reporting/providers/playback_reporter_provider.dart';
|
||||||
|
@ -13,10 +14,20 @@ import 'package:whispering_pages/features/sleep_timer/providers/sleep_timer_prov
|
||||||
import 'package:whispering_pages/router/router.dart';
|
import 'package:whispering_pages/router/router.dart';
|
||||||
import 'package:whispering_pages/settings/api_settings_provider.dart';
|
import 'package:whispering_pages/settings/api_settings_provider.dart';
|
||||||
import 'package:whispering_pages/settings/app_settings_provider.dart';
|
import 'package:whispering_pages/settings/app_settings_provider.dart';
|
||||||
|
import 'package:whispering_pages/shared/extensions/duration_format.dart';
|
||||||
import 'package:whispering_pages/theme/theme.dart';
|
import 'package:whispering_pages/theme/theme.dart';
|
||||||
|
|
||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
// Configure the root Logger
|
||||||
|
Logger.root.level = Level.ALL; // Capture all logs
|
||||||
|
Logger.root.onRecord.listen((record) {
|
||||||
|
// Print log records to the console
|
||||||
|
debugPrint(
|
||||||
|
'${record.loggerName}: ${record.level.name}: ${record.time.time}: ${record.message}',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
// for playing audio on windows, linux
|
// for playing audio on windows, linux
|
||||||
JustAudioMediaKit.ensureInitialized();
|
JustAudioMediaKit.ensureInitialized();
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
// this provider is used to provide the Api settings to the app
|
// this provider is used to provide the Api settings to the app
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
import 'package:whispering_pages/db/available_boxes.dart';
|
import 'package:whispering_pages/db/available_boxes.dart';
|
||||||
import 'package:whispering_pages/settings/models/api_settings.dart' as model;
|
import 'package:whispering_pages/settings/models/api_settings.dart' as model;
|
||||||
|
@ -9,6 +9,8 @@ part 'api_settings_provider.g.dart';
|
||||||
|
|
||||||
final _box = AvailableHiveBoxes.apiSettingsBox;
|
final _box = AvailableHiveBoxes.apiSettingsBox;
|
||||||
|
|
||||||
|
final _logger = Logger('ApiSettingsProvider');
|
||||||
|
|
||||||
@Riverpod(keepAlive: true)
|
@Riverpod(keepAlive: true)
|
||||||
class ApiSettings extends _$ApiSettings {
|
class ApiSettings extends _$ApiSettings {
|
||||||
@override
|
@override
|
||||||
|
@ -31,12 +33,12 @@ class ApiSettings extends _$ApiSettings {
|
||||||
activeServer: foundSettings.activeUser?.server,
|
activeServer: foundSettings.activeUser?.server,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
debugPrint('found api settings in box: $foundSettings');
|
_logger.fine('found api settings in box: $foundSettings');
|
||||||
return foundSettings;
|
return foundSettings;
|
||||||
} else {
|
} else {
|
||||||
// create a new settings object
|
// create a new settings object
|
||||||
const settings = model.ApiSettings();
|
const settings = model.ApiSettings();
|
||||||
debugPrint('created new api settings: $settings');
|
_logger.fine('created new api settings: $settings');
|
||||||
return settings;
|
return settings;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -45,7 +47,7 @@ class ApiSettings extends _$ApiSettings {
|
||||||
void writeToBox() {
|
void writeToBox() {
|
||||||
_box.clear();
|
_box.clear();
|
||||||
_box.add(state);
|
_box.add(state);
|
||||||
debugPrint('wrote api settings to box: $state');
|
_logger.fine('wrote api settings to box: $state');
|
||||||
}
|
}
|
||||||
|
|
||||||
void updateState(model.ApiSettings newSettings, {bool force = false}) {
|
void updateState(model.ApiSettings newSettings, {bool force = false}) {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
// this provider is used to provide the app settings to the app
|
// this provider is used to provide the app settings to the app
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
import 'package:whispering_pages/db/available_boxes.dart';
|
import 'package:whispering_pages/db/available_boxes.dart';
|
||||||
import 'package:whispering_pages/settings/models/app_settings.dart' as model;
|
import 'package:whispering_pages/settings/models/app_settings.dart' as model;
|
||||||
|
@ -9,6 +9,8 @@ part 'app_settings_provider.g.dart';
|
||||||
|
|
||||||
final _box = AvailableHiveBoxes.userPrefsBox;
|
final _box = AvailableHiveBoxes.userPrefsBox;
|
||||||
|
|
||||||
|
final _logger = Logger('AppSettingsProvider');
|
||||||
|
|
||||||
@Riverpod(keepAlive: true)
|
@Riverpod(keepAlive: true)
|
||||||
class AppSettings extends _$AppSettings {
|
class AppSettings extends _$AppSettings {
|
||||||
@override
|
@override
|
||||||
|
@ -24,12 +26,12 @@ class AppSettings extends _$AppSettings {
|
||||||
// see if the settings are already in the box
|
// see if the settings are already in the box
|
||||||
if (_box.isNotEmpty) {
|
if (_box.isNotEmpty) {
|
||||||
final foundSettings = _box.getAt(0);
|
final foundSettings = _box.getAt(0);
|
||||||
debugPrint('found settings in box: $foundSettings');
|
_logger.fine('found settings in box: $foundSettings');
|
||||||
return foundSettings;
|
return foundSettings;
|
||||||
} else {
|
} else {
|
||||||
// create a new settings object
|
// create a new settings object
|
||||||
const settings = model.AppSettings();
|
const settings = model.AppSettings();
|
||||||
debugPrint('created new settings: $settings');
|
_logger.fine('created new settings: $settings');
|
||||||
return settings;
|
return settings;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -38,7 +40,7 @@ class AppSettings extends _$AppSettings {
|
||||||
void writeToBox() {
|
void writeToBox() {
|
||||||
_box.clear();
|
_box.clear();
|
||||||
_box.add(state);
|
_box.add(state);
|
||||||
debugPrint('wrote settings to box: $state');
|
_logger.fine('wrote settings to box: $state');
|
||||||
}
|
}
|
||||||
|
|
||||||
void toggleDarkMode() {
|
void toggleDarkMode() {
|
||||||
|
|
|
@ -9,3 +9,10 @@ extension DurationFormat on Duration {
|
||||||
return '${hours}h ${minutes}m';
|
return '${hours}h ${minutes}m';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension OnlyTime on DateTime {
|
||||||
|
// in format HH:MM:ss
|
||||||
|
// padding with 0
|
||||||
|
String get time =>
|
||||||
|
'${hour.toString().padLeft(2, '0')}:${minute.toString().padLeft(2, '0')}:${second.toString().padLeft(2, '0')}';
|
||||||
|
}
|
||||||
|
|
|
@ -1,11 +1,14 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_animate/flutter_animate.dart';
|
import 'package:flutter_animate/flutter_animate.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
import 'package:shelfsdk/audiobookshelf_api.dart';
|
import 'package:shelfsdk/audiobookshelf_api.dart';
|
||||||
import 'package:whispering_pages/api/image_provider.dart';
|
import 'package:whispering_pages/api/image_provider.dart';
|
||||||
|
|
||||||
part 'theme_from_cover_provider.g.dart';
|
part 'theme_from_cover_provider.g.dart';
|
||||||
|
|
||||||
|
final _logger = Logger('ThemeFromCoverProvider');
|
||||||
|
|
||||||
@Riverpod(keepAlive: true)
|
@Riverpod(keepAlive: true)
|
||||||
Future<FutureOr<ColorScheme?>> themeFromCover(
|
Future<FutureOr<ColorScheme?>> themeFromCover(
|
||||||
ThemeFromCoverRef ref,
|
ThemeFromCoverRef ref,
|
||||||
|
@ -15,7 +18,7 @@ Future<FutureOr<ColorScheme?>> themeFromCover(
|
||||||
// ! add deliberate delay to simulate a long running task as it interferes with other animations
|
// ! add deliberate delay to simulate a long running task as it interferes with other animations
|
||||||
await Future.delayed(500.ms);
|
await Future.delayed(500.ms);
|
||||||
|
|
||||||
debugPrint('Generating color scheme from cover image');
|
_logger.fine('Generating color scheme from cover image');
|
||||||
return ColorScheme.fromImageProvider(
|
return ColorScheme.fromImageProvider(
|
||||||
provider: img,
|
provider: img,
|
||||||
brightness: brightness,
|
brightness: brightness,
|
||||||
|
@ -26,7 +29,7 @@ Future<FutureOr<ColorScheme?>> themeFromCover(
|
||||||
// RootIsolateToken? token = RootIsolateToken.instance;
|
// RootIsolateToken? token = RootIsolateToken.instance;
|
||||||
// final scheme = await Isolate.run(
|
// final scheme = await Isolate.run(
|
||||||
// () async {
|
// () async {
|
||||||
// debugPrint('Isolate running ${Isolate.current.debugName}');
|
// _logger.fine('Isolate running ${Isolate.current.debugName}');
|
||||||
// try {
|
// try {
|
||||||
// BackgroundIsolateBinaryMessenger.ensureInitialized(token!);
|
// BackgroundIsolateBinaryMessenger.ensureInitialized(token!);
|
||||||
// WidgetsFlutterBinding.ensureInitialized();
|
// WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
@ -35,7 +38,7 @@ Future<FutureOr<ColorScheme?>> themeFromCover(
|
||||||
// brightness: brightness,
|
// brightness: brightness,
|
||||||
// );
|
// );
|
||||||
// } catch (e) {
|
// } catch (e) {
|
||||||
// debugPrint('Error in isolate: $e');
|
// _logger.fine('Error in isolate: $e');
|
||||||
// return null;
|
// return null;
|
||||||
// }
|
// }
|
||||||
// },
|
// },
|
||||||
|
@ -60,9 +63,9 @@ FutureOr<ColorScheme?> themeOfLibraryItem(
|
||||||
return val;
|
return val;
|
||||||
// coverImage.when(
|
// coverImage.when(
|
||||||
// data: (value) async {
|
// data: (value) async {
|
||||||
// debugPrint('CoverImage: $value');
|
// _logger.fine('CoverImage: $value');
|
||||||
// final val = ref.watch(themeFromCoverProvider(MemoryImage(value)));
|
// final val = ref.watch(themeFromCoverProvider(MemoryImage(value)));
|
||||||
// debugPrint('ColorScheme generated: $val');
|
// _logger.fine('ColorScheme generated: $val');
|
||||||
// ref.invalidateSelf();
|
// ref.invalidateSelf();
|
||||||
// return val;
|
// return val;
|
||||||
// },
|
// },
|
||||||
|
|
38
pubspec.lock
38
pubspec.lock
|
@ -113,6 +113,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.0.7"
|
version: "0.0.7"
|
||||||
|
background_downloader:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: background_downloader
|
||||||
|
sha256: "9504093db43da6095c44dd14fc816f3ee8961633ace12340f5d3c4fbfd346e2d"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "8.5.2"
|
||||||
boolean_selector:
|
boolean_selector:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -157,18 +165,18 @@ packages:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
name: build_runner
|
name: build_runner
|
||||||
sha256: "1414d6d733a85d8ad2f1dfcb3ea7945759e35a123cb99ccfac75d0758f75edfa"
|
sha256: "644dc98a0f179b872f612d3eb627924b578897c629788e858157fa5e704ca0c7"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.4.10"
|
version: "2.4.11"
|
||||||
build_runner_core:
|
build_runner_core:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: build_runner_core
|
name: build_runner_core
|
||||||
sha256: "4ae8ffe5ac758da294ecf1802f2aff01558d8b1b00616aa7538ea9a8a5d50799"
|
sha256: e3c79f69a64bdfcd8a776a3c28db4eb6e3fb5356d013ae5eb2e52007706d5dbe
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "7.3.0"
|
version: "7.3.1"
|
||||||
built_collection:
|
built_collection:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -397,10 +405,10 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: file_picker
|
name: file_picker
|
||||||
sha256: "29c90806ac5f5fb896547720b73b17ee9aed9bba540dc5d91fe29f8c5745b10a"
|
sha256: "2ca051989f69d1b2ca012b2cf3ccf78c70d40144f0861ff2c063493f7c8c3d45"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "8.0.3"
|
version: "8.0.5"
|
||||||
fixnum:
|
fixnum:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -548,10 +556,10 @@ packages:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: go_router
|
name: go_router
|
||||||
sha256: abec47eb8c8c36ebf41d0a4c64dbbe7f956e39a012b3aafc530e951bdc12fe3f
|
sha256: cdae1b9c8bd7efadcef6112e81c903662ef2ce105cbd220a04bbb7c3425b5554
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "14.1.4"
|
version: "14.2.0"
|
||||||
graphs:
|
graphs:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -761,7 +769,7 @@ packages:
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.0.3"
|
version: "0.0.3"
|
||||||
logging:
|
logging:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: logging
|
name: logging
|
||||||
sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340"
|
sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340"
|
||||||
|
@ -899,10 +907,10 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: path_provider_android
|
name: path_provider_android
|
||||||
sha256: "9c96da072b421e98183f9ea7464898428e764bc0ce5567f27ec8693442e72514"
|
sha256: bca87b0165ffd7cdb9cad8edd22d18d2201e886d9a9f19b4fb3452ea7df3a72a
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.5"
|
version: "2.2.6"
|
||||||
path_provider_foundation:
|
path_provider_foundation:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -947,10 +955,10 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: platform
|
name: platform
|
||||||
sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec"
|
sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.4"
|
version: "3.1.5"
|
||||||
plugin_platform_interface:
|
plugin_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -1263,10 +1271,10 @@ packages:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: url_launcher
|
name: url_launcher
|
||||||
sha256: "6ce1e04375be4eed30548f10a315826fd933c1e493206eab82eed01f438c8d2e"
|
sha256: "21b704ce5fa560ea9f3b525b43601c678728ba46725bab9b01187b4831377ed3"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.2.6"
|
version: "6.3.0"
|
||||||
url_launcher_android:
|
url_launcher_android:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
|
@ -35,6 +35,7 @@ dependencies:
|
||||||
audio_session: ^0.1.19
|
audio_session: ^0.1.19
|
||||||
audio_video_progress_bar: ^2.0.2
|
audio_video_progress_bar: ^2.0.2
|
||||||
auto_scroll_text: ^0.0.7
|
auto_scroll_text: ^0.0.7
|
||||||
|
background_downloader: ^8.5.2
|
||||||
cached_network_image: ^3.3.1
|
cached_network_image: ^3.3.1
|
||||||
coast: ^2.0.2
|
coast: ^2.0.2
|
||||||
collection: ^1.18.0
|
collection: ^1.18.0
|
||||||
|
@ -61,6 +62,7 @@ dependencies:
|
||||||
just_audio_background: ^0.0.1-beta.11
|
just_audio_background: ^0.0.1-beta.11
|
||||||
just_audio_media_kit: ^2.0.4
|
just_audio_media_kit: ^2.0.4
|
||||||
list_wheel_scroll_view_nls: ^0.0.3
|
list_wheel_scroll_view_nls: ^0.0.3
|
||||||
|
logging: ^1.2.0
|
||||||
lottie: ^3.1.0
|
lottie: ^3.1.0
|
||||||
media_kit_libs_linux: any
|
media_kit_libs_linux: any
|
||||||
media_kit_libs_windows_audio: any
|
media_kit_libs_windows_audio: any
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue