mirror of
https://github.com/Dr-Blank/Vaani.git
synced 2025-12-06 02:59:28 +00:00
feat: error reporting with logs (#45)
* feat: add ability to get logs file from ui * test: add unit test for log line parsing in logs_provider * refactor: update all logs to obfuscate sensitive information * feat: generate dynamic zip file name for logs export * feat: enhance logging in audiobook player and provider for better debugging * refactor: extract user display logic into UserBar widget for offline access of settings and logs * feat: add About section with app metadata and source code link in YouPage
This commit is contained in:
parent
7b0c2c4b88
commit
35a2d7cfce
44 changed files with 861 additions and 176 deletions
BIN
assets/images/vaani_logo_foreground.png
Normal file
BIN
assets/images/vaani_logo_foreground.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.8 KiB |
|
|
@ -8,6 +8,7 @@ import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
import 'package:shelfsdk/audiobookshelf_api.dart';
|
import 'package:shelfsdk/audiobookshelf_api.dart';
|
||||||
import 'package:vaani/db/cache_manager.dart';
|
import 'package:vaani/db/cache_manager.dart';
|
||||||
import 'package:vaani/settings/api_settings_provider.dart';
|
import 'package:vaani/settings/api_settings_provider.dart';
|
||||||
|
import 'package:vaani/shared/extensions/obfuscation.dart';
|
||||||
|
|
||||||
part 'api_provider.g.dart';
|
part 'api_provider.g.dart';
|
||||||
|
|
||||||
|
|
@ -80,7 +81,7 @@ FutureOr<ServerStatusResponse?> serverStatus(
|
||||||
Uri baseUrl, [
|
Uri baseUrl, [
|
||||||
ResponseErrorHandler? responseErrorHandler,
|
ResponseErrorHandler? responseErrorHandler,
|
||||||
]) async {
|
]) async {
|
||||||
_logger.fine('fetching server status: $baseUrl');
|
_logger.fine('fetching server status: ${baseUrl.obfuscate()}');
|
||||||
final api = ref.watch(audiobookshelfApiProvider(baseUrl));
|
final api = ref.watch(audiobookshelfApiProvider(baseUrl));
|
||||||
final res =
|
final res =
|
||||||
await api.server.status(responseErrorHandler: responseErrorHandler);
|
await api.server.status(responseErrorHandler: responseErrorHandler);
|
||||||
|
|
@ -145,7 +146,6 @@ class PersonalizedView extends _$PersonalizedView {
|
||||||
_logger.warning('failed to fetch personalized view');
|
_logger.warning('failed to fetch personalized view');
|
||||||
yield [];
|
yield [];
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// method to force refresh the view and ignore the cache
|
// method to force refresh the view and ignore the cache
|
||||||
|
|
|
||||||
|
|
@ -327,7 +327,7 @@ class _IsServerAliveProviderElement
|
||||||
String get address => (origin as IsServerAliveProvider).address;
|
String get address => (origin as IsServerAliveProvider).address;
|
||||||
}
|
}
|
||||||
|
|
||||||
String _$serverStatusHash() => r'2739906a1862d09b098588ebd16749a09032ee99';
|
String _$serverStatusHash() => r'd7079e19e68f5f61b0afa0f73a2af8807c4b3cf6';
|
||||||
|
|
||||||
/// fetch status of server
|
/// fetch status of server
|
||||||
///
|
///
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,8 @@ import 'package:vaani/api/server_provider.dart'
|
||||||
import 'package:vaani/db/storage.dart';
|
import 'package:vaani/db/storage.dart';
|
||||||
import 'package:vaani/settings/api_settings_provider.dart';
|
import 'package:vaani/settings/api_settings_provider.dart';
|
||||||
import 'package:vaani/settings/models/audiobookshelf_server.dart';
|
import 'package:vaani/settings/models/audiobookshelf_server.dart';
|
||||||
import 'package:vaani/settings/models/authenticated_user.dart'
|
import 'package:vaani/settings/models/authenticated_user.dart' as model;
|
||||||
as model;
|
import 'package:vaani/shared/extensions/obfuscation.dart';
|
||||||
|
|
||||||
part 'authenticated_user_provider.g.dart';
|
part 'authenticated_user_provider.g.dart';
|
||||||
|
|
||||||
|
|
@ -35,7 +35,9 @@ 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);
|
||||||
_logger.fine('found users in box: $foundData');
|
_logger.fine(
|
||||||
|
'found users in box: ${foundData.obfuscate()}',
|
||||||
|
);
|
||||||
return foundData.toSet();
|
return foundData.toSet();
|
||||||
} else {
|
} else {
|
||||||
_logger.fine('no settings found in box');
|
_logger.fine('no settings found in box');
|
||||||
|
|
@ -49,7 +51,7 @@ class AuthenticatedUser extends _$AuthenticatedUser {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_box.addAll(state);
|
_box.addAll(state);
|
||||||
_logger.fine('writing state to box: $state');
|
_logger.fine('writing state to box: ${state.obfuscate()}');
|
||||||
}
|
}
|
||||||
|
|
||||||
void addUser(model.AuthenticatedUser user, {bool setActive = false}) {
|
void addUser(model.AuthenticatedUser user, {bool setActive = false}) {
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ part of 'authenticated_user_provider.dart';
|
||||||
// RiverpodGenerator
|
// RiverpodGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$authenticatedUserHash() => r'308f19b33ae04af6340fb83167fa64aa23400a09';
|
String _$authenticatedUserHash() => r'1983527595207c63a12bc84cf0bf1a3c1d729506';
|
||||||
|
|
||||||
/// provides with a set of authenticated users
|
/// provides with a set of authenticated users
|
||||||
///
|
///
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,18 @@
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
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:vaani/api/authenticated_user_provider.dart';
|
import 'package:vaani/api/authenticated_user_provider.dart';
|
||||||
import 'package:vaani/db/storage.dart';
|
import 'package:vaani/db/storage.dart';
|
||||||
import 'package:vaani/settings/api_settings_provider.dart';
|
import 'package:vaani/settings/api_settings_provider.dart';
|
||||||
import 'package:vaani/settings/models/audiobookshelf_server.dart'
|
import 'package:vaani/settings/models/audiobookshelf_server.dart' as model;
|
||||||
as model;
|
import 'package:vaani/shared/extensions/obfuscation.dart';
|
||||||
|
|
||||||
part 'server_provider.g.dart';
|
part 'server_provider.g.dart';
|
||||||
|
|
||||||
final _box = AvailableHiveBoxes.serverBox;
|
final _box = AvailableHiveBoxes.serverBox;
|
||||||
|
|
||||||
|
final _logger = Logger('AudiobookShelfServerProvider');
|
||||||
|
|
||||||
class ServerAlreadyExistsException implements Exception {
|
class ServerAlreadyExistsException implements Exception {
|
||||||
final model.AudiobookShelfServer server;
|
final model.AudiobookShelfServer server;
|
||||||
|
|
||||||
|
|
@ -47,10 +49,10 @@ class AudiobookShelfServer extends _$AudiobookShelfServer {
|
||||||
Set<model.AudiobookShelfServer> readFromBoxOrCreate() {
|
Set<model.AudiobookShelfServer> readFromBoxOrCreate() {
|
||||||
if (_box.isNotEmpty) {
|
if (_box.isNotEmpty) {
|
||||||
final foundServers = _box.getRange(0, _box.length);
|
final foundServers = _box.getRange(0, _box.length);
|
||||||
debugPrint('found servers in box: $foundServers');
|
_logger.info('found servers in box: ${foundServers.obfuscate()}');
|
||||||
return foundServers.whereNotNull().toSet();
|
return foundServers.whereNotNull().toSet();
|
||||||
} else {
|
} else {
|
||||||
debugPrint('no settings found in box');
|
_logger.info('no settings found in box');
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -61,7 +63,7 @@ class AudiobookShelfServer extends _$AudiobookShelfServer {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_box.addAll(state);
|
_box.addAll(state);
|
||||||
debugPrint('writing state to box: $state');
|
_logger.info('writing state to box: ${state.obfuscate()}');
|
||||||
}
|
}
|
||||||
|
|
||||||
void addServer(model.AudiobookShelfServer server) {
|
void addServer(model.AudiobookShelfServer server) {
|
||||||
|
|
@ -71,8 +73,8 @@ class AudiobookShelfServer extends _$AudiobookShelfServer {
|
||||||
state = {...state, server};
|
state = {...state, server};
|
||||||
}
|
}
|
||||||
|
|
||||||
void removeServer(model.AudiobookShelfServer server,
|
void removeServer(
|
||||||
{
|
model.AudiobookShelfServer server, {
|
||||||
bool removeUsers = false,
|
bool removeUsers = false,
|
||||||
}) {
|
}) {
|
||||||
state = state.where((s) => s != server).toSet();
|
state = state.where((s) => s != server).toSet();
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ part of 'server_provider.dart';
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$audiobookShelfServerHash() =>
|
String _$audiobookShelfServerHash() =>
|
||||||
r'f0d645bb42233c59886bc43fdc473897484ceca1';
|
r'0084fb72c4c54323207928b95716cfd9ca496c11';
|
||||||
|
|
||||||
/// provides with a set of servers added by the user
|
/// provides with a set of servers added by the user
|
||||||
///
|
///
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,20 @@
|
||||||
// does the initial setup of the storage
|
|
||||||
|
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:hive/hive.dart';
|
import 'package:hive/hive.dart';
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
import 'package:vaani/main.dart';
|
||||||
import 'package:vaani/settings/constants.dart';
|
import 'package:vaani/settings/constants.dart';
|
||||||
|
|
||||||
import 'register_models.dart';
|
import 'register_models.dart';
|
||||||
|
|
||||||
|
// does the initial setup of the storage
|
||||||
Future initStorage() async {
|
Future initStorage() async {
|
||||||
final dir = await getApplicationDocumentsDirectory();
|
final dir = await getApplicationDocumentsDirectory();
|
||||||
|
|
||||||
// use vaani as the directory for hive
|
// use vaani as the directory for hive
|
||||||
final storageDir = Directory(p.join(
|
final storageDir = Directory(
|
||||||
|
p.join(
|
||||||
dir.path,
|
dir.path,
|
||||||
AppMetadata.appNameLowerCase,
|
AppMetadata.appNameLowerCase,
|
||||||
),
|
),
|
||||||
|
|
@ -22,7 +22,7 @@ Future initStorage() async {
|
||||||
await storageDir.create(recursive: true);
|
await storageDir.create(recursive: true);
|
||||||
|
|
||||||
Hive.defaultDirectory = storageDir.path;
|
Hive.defaultDirectory = storageDir.path;
|
||||||
debugPrint('Hive storage directory init: ${Hive.defaultDirectory}');
|
appLogger.config('Hive storage directory init: ${Hive.defaultDirectory}');
|
||||||
|
|
||||||
await registerModels();
|
await registerModels();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import 'package:logging/logging.dart';
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
import 'package:shelfsdk/audiobookshelf_api.dart';
|
import 'package:shelfsdk/audiobookshelf_api.dart';
|
||||||
import 'package:vaani/shared/extensions/model_conversions.dart';
|
import 'package:vaani/shared/extensions/model_conversions.dart';
|
||||||
|
import 'package:vaani/shared/extensions/obfuscation.dart';
|
||||||
|
|
||||||
final _logger = Logger('AudiobookDownloadManager');
|
final _logger = Logger('AudiobookDownloadManager');
|
||||||
final tq = MemoryTaskQueue();
|
final tq = MemoryTaskQueue();
|
||||||
|
|
@ -35,7 +36,9 @@ class AudiobookDownloadManager {
|
||||||
|
|
||||||
FileDownloader().addTaskQueue(tq);
|
FileDownloader().addTaskQueue(tq);
|
||||||
|
|
||||||
_logger.fine('initialized with baseUrl: $baseUrl, token: $token');
|
_logger.fine(
|
||||||
|
'initialized with baseUrl: ${Uri.parse(baseUrl).obfuscate()} and token: ${token.obfuscate()}',
|
||||||
|
);
|
||||||
_logger.fine(
|
_logger.fine(
|
||||||
'requiresWiFi: $requiresWiFi, retries: $retries, allowPause: $allowPause',
|
'requiresWiFi: $requiresWiFi, retries: $retries, allowPause: $allowPause',
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -31,9 +31,11 @@ class SimpleDownloadManager extends _$SimpleDownloadManager {
|
||||||
core.tq.maxConcurrentByGroup = downloadSettings.maxConcurrentByGroup;
|
core.tq.maxConcurrentByGroup = downloadSettings.maxConcurrentByGroup;
|
||||||
|
|
||||||
ref.onDispose(() {
|
ref.onDispose(() {
|
||||||
|
_logger.info('disposing download manager');
|
||||||
manager.dispose();
|
manager.dispose();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
_logger.config('initialized download manager');
|
||||||
return manager;
|
return manager;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -52,12 +54,14 @@ class DownloadManager extends _$DownloadManager {
|
||||||
Future<void> queueAudioBookDownload(
|
Future<void> queueAudioBookDownload(
|
||||||
LibraryItemExpanded item,
|
LibraryItemExpanded item,
|
||||||
) async {
|
) async {
|
||||||
|
_logger.fine('queueing download for ${item.id}');
|
||||||
await state.queueAudioBookDownload(
|
await state.queueAudioBookDownload(
|
||||||
item,
|
item,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> deleteDownloadedItem(LibraryItemExpanded item) async {
|
Future<void> deleteDownloadedItem(LibraryItemExpanded item) async {
|
||||||
|
_logger.fine('deleting downloaded item ${item.id}');
|
||||||
await state.deleteDownloadedItem(item);
|
await state.deleteDownloadedItem(item);
|
||||||
ref.notifyListeners();
|
ref.notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -158,7 +158,7 @@ class _DownloadHistoryProviderElement
|
||||||
}
|
}
|
||||||
|
|
||||||
String _$simpleDownloadManagerHash() =>
|
String _$simpleDownloadManagerHash() =>
|
||||||
r'cec95717c86e422f88f78aa014d29e800e5a2089';
|
r'8ab13f06ec5f2f73b73064bd285813dc890b7f36';
|
||||||
|
|
||||||
/// See also [SimpleDownloadManager].
|
/// See also [SimpleDownloadManager].
|
||||||
@ProviderFor(SimpleDownloadManager)
|
@ProviderFor(SimpleDownloadManager)
|
||||||
|
|
@ -174,7 +174,7 @@ final simpleDownloadManagerProvider = NotifierProvider<SimpleDownloadManager,
|
||||||
);
|
);
|
||||||
|
|
||||||
typedef _$SimpleDownloadManager = Notifier<core.AudiobookDownloadManager>;
|
typedef _$SimpleDownloadManager = Notifier<core.AudiobookDownloadManager>;
|
||||||
String _$downloadManagerHash() => r'7296a39439230f77abbe7d3231dae748f09c7ecf';
|
String _$downloadManagerHash() => r'852012e32e613f86445afc7f7e4e85bec808e982';
|
||||||
|
|
||||||
/// See also [DownloadManager].
|
/// See also [DownloadManager].
|
||||||
@ProviderFor(DownloadManager)
|
@ProviderFor(DownloadManager)
|
||||||
|
|
|
||||||
|
|
@ -518,7 +518,7 @@ Future<void> libraryItemPlayButtonOnPressed({
|
||||||
required shelfsdk.BookExpanded book,
|
required shelfsdk.BookExpanded book,
|
||||||
shelfsdk.MediaProgress? userMediaProgress,
|
shelfsdk.MediaProgress? userMediaProgress,
|
||||||
}) async {
|
}) async {
|
||||||
debugPrint('Pressed play/resume button');
|
appLogger.info('Pressed play/resume button');
|
||||||
final player = ref.watch(audiobookPlayerProvider);
|
final player = ref.watch(audiobookPlayerProvider);
|
||||||
|
|
||||||
final isCurrentBookSetInPlayer = player.book == book;
|
final isCurrentBookSetInPlayer = player.book == book;
|
||||||
|
|
@ -527,8 +527,8 @@ Future<void> libraryItemPlayButtonOnPressed({
|
||||||
Future<void>? setSourceFuture;
|
Future<void>? setSourceFuture;
|
||||||
// set the book to the player if not already set
|
// set the book to the player if not already set
|
||||||
if (!isCurrentBookSetInPlayer) {
|
if (!isCurrentBookSetInPlayer) {
|
||||||
debugPrint('Setting the book ${book.libraryItemId}');
|
appLogger.info('Setting the book ${book.libraryItemId}');
|
||||||
debugPrint('Initial position: ${userMediaProgress?.currentTime}');
|
appLogger.info('Initial position: ${userMediaProgress?.currentTime}');
|
||||||
final downloadManager = ref.watch(simpleDownloadManagerProvider);
|
final downloadManager = ref.watch(simpleDownloadManagerProvider);
|
||||||
final libItem =
|
final libItem =
|
||||||
await ref.read(libraryItemProvider(book.libraryItemId).future);
|
await ref.read(libraryItemProvider(book.libraryItemId).future);
|
||||||
|
|
@ -539,9 +539,9 @@ Future<void> libraryItemPlayButtonOnPressed({
|
||||||
downloadedUris: downloadedUris,
|
downloadedUris: downloadedUris,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
debugPrint('Book was already set');
|
appLogger.info('Book was already set');
|
||||||
if (isPlayingThisBook) {
|
if (isPlayingThisBook) {
|
||||||
debugPrint('Pausing the book');
|
appLogger.info('Pausing the book');
|
||||||
await player.pause();
|
await player.pause();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -383,7 +383,7 @@ class _BookCover extends HookConsumerWidget {
|
||||||
: themeData,
|
: themeData,
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
appLogger.shout('Error changing theme: $e');
|
appLogger.severe('Error changing theme: $e');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
36
lib/features/logging/core/logger.dart
Normal file
36
lib/features/logging/core/logger.dart
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
import 'package:logging_appenders/logging_appenders.dart';
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
import 'package:vaani/shared/extensions/duration_format.dart';
|
||||||
|
|
||||||
|
Future<String> getLoggingFilePath() async {
|
||||||
|
final Directory directory = await getApplicationDocumentsDirectory();
|
||||||
|
return '${directory.path}/vaani.log';
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> initLogging() async {
|
||||||
|
final formatter = const DefaultLogRecordFormatter();
|
||||||
|
if (kReleaseMode) {
|
||||||
|
Logger.root.level = Level.INFO; // is also the default
|
||||||
|
// Write to a file
|
||||||
|
RotatingFileAppender(
|
||||||
|
baseFilePath: await getLoggingFilePath(),
|
||||||
|
formatter: formatter,
|
||||||
|
).attachToLogger(Logger.root);
|
||||||
|
} else {
|
||||||
|
Logger.root.level = Level.FINE; // Capture all logs
|
||||||
|
RotatingFileAppender(
|
||||||
|
baseFilePath: await getLoggingFilePath(),
|
||||||
|
formatter: formatter,
|
||||||
|
).attachToLogger(Logger.root);
|
||||||
|
Logger.root.onRecord.listen((record) {
|
||||||
|
// Print log records to the console
|
||||||
|
debugPrint(
|
||||||
|
'${record.loggerName}: ${record.level.name}: ${record.time.time}: ${record.message}',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
76
lib/features/logging/providers/logs_provider.dart
Normal file
76
lib/features/logging/providers/logs_provider.dart
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:archive/archive_io.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
import 'package:vaani/features/logging/core/logger.dart';
|
||||||
|
|
||||||
|
part 'logs_provider.g.dart';
|
||||||
|
|
||||||
|
@riverpod
|
||||||
|
class Logs extends _$Logs {
|
||||||
|
@override
|
||||||
|
Future<List<LogRecord>> build() async {
|
||||||
|
final path = await getLoggingFilePath();
|
||||||
|
final file = File(path);
|
||||||
|
if (!file.existsSync()) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
final lines = await file.readAsLines();
|
||||||
|
return lines.map(parseLogLine).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> clear() async {
|
||||||
|
final path = await getLoggingFilePath();
|
||||||
|
final file = File(path);
|
||||||
|
await file.writeAsString('');
|
||||||
|
state = AsyncData([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String> getZipFilePath() async {
|
||||||
|
var encoder = ZipFileEncoder();
|
||||||
|
encoder.create(await generateZipFilePath());
|
||||||
|
encoder.addFile(File(await getLoggingFilePath()));
|
||||||
|
encoder.close();
|
||||||
|
return encoder.zipPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String> generateZipFilePath() async {
|
||||||
|
Directory appDocDirectory = await getTemporaryDirectory();
|
||||||
|
return '${appDocDirectory.path}/${generateZipFileName()}';
|
||||||
|
}
|
||||||
|
|
||||||
|
String generateZipFileName() {
|
||||||
|
return 'vaani-${DateTime.now().toIso8601String()}.zip';
|
||||||
|
}
|
||||||
|
|
||||||
|
Level parseLevel(String level) {
|
||||||
|
return Level.LEVELS
|
||||||
|
.firstWhere((l) => l.name == level, orElse: () => Level.ALL);
|
||||||
|
}
|
||||||
|
|
||||||
|
LogRecord parseLogLine(String line) {
|
||||||
|
// 2024-10-03 00:48:58.012400 INFO GoRouter - getting location for name: "logs"
|
||||||
|
|
||||||
|
final RegExp logLineRegExp = RegExp(
|
||||||
|
r'(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{6}) (\w+) (\w+) - (.+)',
|
||||||
|
);
|
||||||
|
|
||||||
|
final match = logLineRegExp.firstMatch(line);
|
||||||
|
if (match == null) {
|
||||||
|
// return as is
|
||||||
|
return LogRecord(Level.ALL, line, 'Unknown');
|
||||||
|
}
|
||||||
|
|
||||||
|
final timeString = match.group(1)!;
|
||||||
|
final levelString = match.group(2)!;
|
||||||
|
final loggerName = match.group(3)!;
|
||||||
|
final message = match.group(4)!;
|
||||||
|
|
||||||
|
final time = DateTime.parse(timeString);
|
||||||
|
final level = parseLevel(levelString);
|
||||||
|
|
||||||
|
return LogRecord(level, message, loggerName, time);
|
||||||
|
}
|
||||||
25
lib/features/logging/providers/logs_provider.g.dart
Normal file
25
lib/features/logging/providers/logs_provider.g.dart
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'logs_provider.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// RiverpodGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
String _$logsHash() => r'901376741d17ddbb889d1b7b96bc2882289720a0';
|
||||||
|
|
||||||
|
/// See also [Logs].
|
||||||
|
@ProviderFor(Logs)
|
||||||
|
final logsProvider =
|
||||||
|
AutoDisposeAsyncNotifierProvider<Logs, List<LogRecord>>.internal(
|
||||||
|
Logs.new,
|
||||||
|
name: r'logsProvider',
|
||||||
|
debugGetCreateSourceHash:
|
||||||
|
const bool.fromEnvironment('dart.vm.product') ? null : _$logsHash,
|
||||||
|
dependencies: null,
|
||||||
|
allTransitiveDependencies: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
typedef _$Logs = AutoDisposeAsyncNotifier<List<LogRecord>>;
|
||||||
|
// ignore_for_file: type=lint
|
||||||
|
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
|
||||||
266
lib/features/logging/view/logs_page.dart
Normal file
266
lib/features/logging/view/logs_page.dart
Normal file
|
|
@ -0,0 +1,266 @@
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:file_picker/file_picker.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
import 'package:share_plus/share_plus.dart';
|
||||||
|
import 'package:vaani/features/logging/providers/logs_provider.dart';
|
||||||
|
import 'package:vaani/main.dart';
|
||||||
|
|
||||||
|
class LogsPage extends HookConsumerWidget {
|
||||||
|
const LogsPage({super.key});
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final logs = ref.watch(logsProvider);
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final scrollController = useScrollController();
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text('Logs'),
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
tooltip: 'Clear logs',
|
||||||
|
icon: const Icon(Icons.delete_forever),
|
||||||
|
onPressed: () async {
|
||||||
|
// ask for confirmation
|
||||||
|
final shouldClear = await showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: const Text('Clear logs?'),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop(false);
|
||||||
|
},
|
||||||
|
child: const Text('Cancel'),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop(true);
|
||||||
|
},
|
||||||
|
child: const Text('Clear'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (shouldClear == true) {
|
||||||
|
ref.read(logsProvider.notifier).clear();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
tooltip: 'Share logs',
|
||||||
|
icon: const Icon(Icons.share),
|
||||||
|
onPressed: () async {
|
||||||
|
appLogger.info('Preparing logs for sharing');
|
||||||
|
final zipLogFilePath =
|
||||||
|
await ref.read(logsProvider.notifier).getZipFilePath();
|
||||||
|
|
||||||
|
// submit logs
|
||||||
|
final result = await Share.shareXFiles([XFile(zipLogFilePath)]);
|
||||||
|
|
||||||
|
switch (result.status) {
|
||||||
|
case ShareResultStatus.success:
|
||||||
|
appLogger.info('Share success');
|
||||||
|
break;
|
||||||
|
case ShareResultStatus.dismissed:
|
||||||
|
appLogger.info('Share dismissed');
|
||||||
|
break;
|
||||||
|
case ShareResultStatus.unavailable:
|
||||||
|
appLogger.severe('Share unavailable');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
tooltip: 'Download logs',
|
||||||
|
icon: const Icon(Icons.download),
|
||||||
|
onPressed: () async {
|
||||||
|
appLogger.info('Preparing logs for download');
|
||||||
|
final zipLogFilePath =
|
||||||
|
await ref.read(logsProvider.notifier).getZipFilePath();
|
||||||
|
|
||||||
|
// save to folder
|
||||||
|
String? outputFile = await FilePicker.platform.saveFile(
|
||||||
|
dialogTitle: 'Please select an output file:',
|
||||||
|
fileName: zipLogFilePath.split('/').last,
|
||||||
|
);
|
||||||
|
if (outputFile != null) {
|
||||||
|
try {
|
||||||
|
final file = File(outputFile);
|
||||||
|
final zipFile = File(zipLogFilePath);
|
||||||
|
await zipFile.copy(file.path);
|
||||||
|
} catch (e) {
|
||||||
|
appLogger.severe('Error saving file: $e');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
appLogger.info('Download cancelled');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
tooltip: 'Refresh logs',
|
||||||
|
icon: const Icon(Icons.refresh),
|
||||||
|
onPressed: () {
|
||||||
|
ref.invalidate(logsProvider);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
tooltip: 'Scroll to top',
|
||||||
|
icon: const Icon(Icons.arrow_upward),
|
||||||
|
onPressed: () {
|
||||||
|
scrollController.animateTo(
|
||||||
|
0,
|
||||||
|
duration: const Duration(milliseconds: 500),
|
||||||
|
curve: Curves.easeInOut,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
// a column with listview.builder and a scrollable list of logs
|
||||||
|
body: Column(
|
||||||
|
children: [
|
||||||
|
// a filter for log levels, loggers, and search
|
||||||
|
// TODO: implement filters and search
|
||||||
|
|
||||||
|
Expanded(
|
||||||
|
child: logs.when(
|
||||||
|
data: (logRecords) {
|
||||||
|
return Scrollbar(
|
||||||
|
controller: scrollController,
|
||||||
|
child: ListView.builder(
|
||||||
|
controller: scrollController,
|
||||||
|
itemCount: logRecords.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final logRecord = logRecords[index];
|
||||||
|
return LogRecordTile(logRecord: logRecord, theme: theme);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
loading: () => const Center(child: CircularProgressIndicator()),
|
||||||
|
error: (error, stackTrace) => Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const Text('Error loading logs'),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () {
|
||||||
|
ref.invalidate(logsProvider);
|
||||||
|
},
|
||||||
|
child: const Text('Retry'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class LogRecordTile extends StatelessWidget {
|
||||||
|
final LogRecord logRecord;
|
||||||
|
final ThemeData theme;
|
||||||
|
const LogRecordTile({
|
||||||
|
required this.logRecord,
|
||||||
|
required this.theme,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ListTile(
|
||||||
|
leading: CircleAvatar(
|
||||||
|
backgroundColor: getLogLevelColor(logRecord.level, theme),
|
||||||
|
child: Icon(
|
||||||
|
getLogLevelIcon(logRecord.level),
|
||||||
|
color: getLogLevelTextColor(logRecord.level, theme),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
title: Text(logRecord.loggerName),
|
||||||
|
subtitle: Text(logRecord.message),
|
||||||
|
onTap: () {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) {
|
||||||
|
return AlertDialog(
|
||||||
|
icon: Icon(getLogLevelIcon(logRecord.level)),
|
||||||
|
title: Text(logRecord.loggerName),
|
||||||
|
content: Text.rich(
|
||||||
|
TextSpan(
|
||||||
|
children: [
|
||||||
|
TextSpan(
|
||||||
|
text: logRecord.time.toIso8601String(),
|
||||||
|
style: const TextStyle(fontStyle: FontStyle.italic),
|
||||||
|
),
|
||||||
|
const TextSpan(text: '\n\n'),
|
||||||
|
TextSpan(
|
||||||
|
text: logRecord.message,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
child: const Text('Close'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
IconData getLogLevelIcon(Level level) {
|
||||||
|
switch (level) {
|
||||||
|
case Level.INFO:
|
||||||
|
return (Icons.info);
|
||||||
|
case Level.WARNING:
|
||||||
|
return (Icons.warning);
|
||||||
|
case Level.SEVERE:
|
||||||
|
case Level.SHOUT:
|
||||||
|
return (Icons.error);
|
||||||
|
default:
|
||||||
|
return (Icons.bug_report);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Color? getLogLevelColor(Level level, ThemeData theme) {
|
||||||
|
switch (level) {
|
||||||
|
case Level.INFO:
|
||||||
|
return theme.colorScheme.surfaceContainerLow;
|
||||||
|
case Level.WARNING:
|
||||||
|
return theme.colorScheme.surfaceBright;
|
||||||
|
case Level.SEVERE:
|
||||||
|
case Level.SHOUT:
|
||||||
|
return theme.colorScheme.errorContainer;
|
||||||
|
default:
|
||||||
|
return theme.colorScheme.primaryContainer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Color? getLogLevelTextColor(Level level, ThemeData theme) {
|
||||||
|
switch (level) {
|
||||||
|
case Level.INFO:
|
||||||
|
return theme.colorScheme.onSurface;
|
||||||
|
case Level.WARNING:
|
||||||
|
return theme.colorScheme.onSurface;
|
||||||
|
case Level.SEVERE:
|
||||||
|
case Level.SHOUT:
|
||||||
|
return theme.colorScheme.onErrorContainer;
|
||||||
|
default:
|
||||||
|
return theme.colorScheme.onPrimaryContainer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -11,6 +11,7 @@ import 'package:vaani/models/error_response.dart';
|
||||||
import 'package:vaani/router/router.dart';
|
import 'package:vaani/router/router.dart';
|
||||||
import 'package:vaani/settings/constants.dart';
|
import 'package:vaani/settings/constants.dart';
|
||||||
import 'package:vaani/settings/models/models.dart' as model;
|
import 'package:vaani/settings/models/models.dart' as model;
|
||||||
|
import 'package:vaani/shared/extensions/obfuscation.dart';
|
||||||
import 'package:vaani/shared/utils.dart';
|
import 'package:vaani/shared/utils.dart';
|
||||||
|
|
||||||
class UserLoginWithOpenID extends HookConsumerWidget {
|
class UserLoginWithOpenID extends HookConsumerWidget {
|
||||||
|
|
@ -89,7 +90,9 @@ class UserLoginWithOpenID extends HookConsumerWidget {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
appLogger.fine('Got OpenID login endpoint: $openIDLoginEndpoint');
|
appLogger.fine(
|
||||||
|
'Got OpenID login endpoint: ${openIDLoginEndpoint.obfuscate()}',
|
||||||
|
);
|
||||||
|
|
||||||
// add the flow to the provider
|
// add the flow to the provider
|
||||||
ref.read(oauthFlowsProvider.notifier).addFlow(
|
ref.read(oauthFlowsProvider.notifier).addFlow(
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:http/http.dart' as http;
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:shelfsdk/audiobookshelf_api.dart';
|
import 'package:shelfsdk/audiobookshelf_api.dart';
|
||||||
import 'package:vaani/features/player/core/audiobook_player.dart';
|
import 'package:vaani/features/player/core/audiobook_player.dart';
|
||||||
|
import 'package:vaani/shared/extensions/obfuscation.dart';
|
||||||
|
|
||||||
final _logger = Logger('PlaybackReporter');
|
final _logger = Logger('PlaybackReporter');
|
||||||
|
|
||||||
|
|
@ -255,9 +257,9 @@ class PlaybackReporter {
|
||||||
_logger.fine('cancelled timer');
|
_logger.fine('cancelled timer');
|
||||||
}
|
}
|
||||||
|
|
||||||
void _responseErrorHandler(response, [error]) {
|
void _responseErrorHandler(http.Response response, [error]) {
|
||||||
if (response.statusCode != 200) {
|
if (response.statusCode != 200) {
|
||||||
_logger.shout('Error with api: $response, $error');
|
_logger.severe('Error with api: ${response.obfuscate()}, $error');
|
||||||
throw PlaybackSyncError(
|
throw PlaybackSyncError(
|
||||||
'Error syncing position: ${response.body}, $error',
|
'Error syncing position: ${response.body}, $error',
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -11,38 +11,38 @@ import 'package:shelfsdk/audiobookshelf_api.dart';
|
||||||
import 'package:vaani/settings/app_settings_provider.dart';
|
import 'package:vaani/settings/app_settings_provider.dart';
|
||||||
import 'package:vaani/settings/models/app_settings.dart';
|
import 'package:vaani/settings/models/app_settings.dart';
|
||||||
import 'package:vaani/shared/extensions/model_conversions.dart';
|
import 'package:vaani/shared/extensions/model_conversions.dart';
|
||||||
|
import 'package:vaani/shared/extensions/obfuscation.dart';
|
||||||
|
|
||||||
final _logger = Logger('AudiobookPlayer');
|
final _logger = Logger('AudiobookPlayer');
|
||||||
|
|
||||||
/// returns the sum of the duration of all the previous tracks before the [index]
|
/// returns the sum of the duration of all the previous tracks before the [index]
|
||||||
Duration sumOfTracks(BookExpanded book, int? index) {
|
Duration sumOfTracks(BookExpanded book, int? index) {
|
||||||
|
_logger.fine('Calculating sum of tracks for index: $index');
|
||||||
// return 0 if index is less than 0
|
// return 0 if index is less than 0
|
||||||
if (index == null || index < 0) {
|
if (index == null || index < 0) {
|
||||||
|
_logger.warning('Index is null or less than 0, returning 0');
|
||||||
return Duration.zero;
|
return Duration.zero;
|
||||||
}
|
}
|
||||||
return book.tracks.sublist(0, index).fold<Duration>(Duration.zero,
|
final total = book.tracks.sublist(0, index).fold<Duration>(
|
||||||
(previousValue, element) {
|
Duration.zero,
|
||||||
return previousValue + element.duration;
|
(previousValue, element) => previousValue + element.duration,
|
||||||
});
|
);
|
||||||
|
_logger.fine('Sum of tracks for index: $index is $total');
|
||||||
|
return total;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// returns the [AudioTrack] to play based on the [position] in the [book]
|
/// returns the [AudioTrack] to play based on the [position] in the [book]
|
||||||
AudioTrack getTrackToPlay(BookExpanded book, Duration position) {
|
AudioTrack getTrackToPlay(BookExpanded book, Duration position) {
|
||||||
// var totalDuration = Duration.zero;
|
_logger.fine('Getting track to play for position: $position');
|
||||||
// for (var track in book.tracks) {
|
final track = book.tracks.firstWhere(
|
||||||
// totalDuration += track.duration;
|
|
||||||
// if (totalDuration >= position) {
|
|
||||||
// return track;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// return book.tracks.last;
|
|
||||||
return book.tracks.firstWhere(
|
|
||||||
(element) {
|
(element) {
|
||||||
return element.startOffset <= position &&
|
return element.startOffset <= position &&
|
||||||
(element.startOffset + element.duration) >= position;
|
(element.startOffset + element.duration) >= position;
|
||||||
},
|
},
|
||||||
orElse: () => book.tracks.last,
|
orElse: () => book.tracks.last,
|
||||||
);
|
);
|
||||||
|
_logger.fine('Track to play for position: $position is $track');
|
||||||
|
return track;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// will manage the audio player instance
|
/// will manage the audio player instance
|
||||||
|
|
@ -50,6 +50,7 @@ class AudiobookPlayer extends AudioPlayer {
|
||||||
// constructor which takes in the BookExpanded object
|
// constructor which takes in the BookExpanded object
|
||||||
AudiobookPlayer(this.token, this.baseUrl) : super() {
|
AudiobookPlayer(this.token, this.baseUrl) : super() {
|
||||||
// set the source of the player to the first track in the book
|
// set the source of the player to the first track in the book
|
||||||
|
_logger.config('Setting up audiobook player');
|
||||||
}
|
}
|
||||||
|
|
||||||
/// the [BookExpanded] being played
|
/// the [BookExpanded] being played
|
||||||
|
|
@ -84,20 +85,23 @@ class AudiobookPlayer extends AudioPlayer {
|
||||||
List<Uri>? downloadedUris,
|
List<Uri>? downloadedUris,
|
||||||
Uri? artworkUri,
|
Uri? artworkUri,
|
||||||
}) async {
|
}) async {
|
||||||
|
_logger.finer(
|
||||||
|
'Initial position: $initialPosition, Downloaded URIs: $downloadedUris',
|
||||||
|
);
|
||||||
final appSettings = loadOrCreateAppSettings();
|
final appSettings = loadOrCreateAppSettings();
|
||||||
// if the book is null, stop the player
|
|
||||||
if (book == null) {
|
if (book == null) {
|
||||||
_book = null;
|
_book = null;
|
||||||
_logger.info('Book is null, stopping player');
|
_logger.info('Book is null, stopping player');
|
||||||
return stop();
|
return stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
// see if the book is the same as the current book
|
|
||||||
if (_book == book) {
|
if (_book == book) {
|
||||||
_logger.info('Book is the same, doing nothing');
|
_logger.info('Book is the same, doing nothing');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// first stop the player and clear the source
|
_logger.info('Setting source for book: $book');
|
||||||
|
|
||||||
|
_logger.fine('Stopping player');
|
||||||
await stop();
|
await stop();
|
||||||
|
|
||||||
_book = book;
|
_book = book;
|
||||||
|
|
@ -114,6 +118,7 @@ class AudiobookPlayer extends AudioPlayer {
|
||||||
? initialPosition - trackToPlay.startOffset
|
? initialPosition - trackToPlay.startOffset
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
|
_logger.finer('Setting audioSource');
|
||||||
await setAudioSource(
|
await setAudioSource(
|
||||||
preload: preload,
|
preload: preload,
|
||||||
initialIndex: initialIndex,
|
initialIndex: initialIndex,
|
||||||
|
|
@ -124,7 +129,7 @@ class AudiobookPlayer extends AudioPlayer {
|
||||||
final retrievedUri =
|
final retrievedUri =
|
||||||
_getUri(track, downloadedUris, baseUrl: baseUrl, token: token);
|
_getUri(track, downloadedUris, baseUrl: baseUrl, token: token);
|
||||||
_logger.fine(
|
_logger.fine(
|
||||||
'Setting source for track: ${track.title}, URI: $retrievedUri',
|
'Setting source for track: ${track.title}, URI: ${retrievedUri.obfuscate()}',
|
||||||
);
|
);
|
||||||
return AudioSource.uri(
|
return AudioSource.uri(
|
||||||
retrievedUri,
|
retrievedUri,
|
||||||
|
|
@ -145,7 +150,7 @@ class AudiobookPlayer extends AudioPlayer {
|
||||||
}).toList(),
|
}).toList(),
|
||||||
),
|
),
|
||||||
).catchError((error) {
|
).catchError((error) {
|
||||||
_logger.shout('Error: $error');
|
_logger.shout('Error in setting audio source: $error');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -153,7 +158,7 @@ class AudiobookPlayer extends AudioPlayer {
|
||||||
Future<void> togglePlayPause() {
|
Future<void> togglePlayPause() {
|
||||||
// check if book is set
|
// check if book is set
|
||||||
if (_book == null) {
|
if (_book == null) {
|
||||||
throw StateError('No book is set');
|
_logger.warning('No book is set, not toggling play/pause');
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO refactor this to cover all the states
|
// TODO refactor this to cover all the states
|
||||||
|
|
@ -169,9 +174,11 @@ class AudiobookPlayer extends AudioPlayer {
|
||||||
@override
|
@override
|
||||||
Future<void> seek(Duration? positionInBook, {int? index}) async {
|
Future<void> seek(Duration? positionInBook, {int? index}) async {
|
||||||
if (_book == null) {
|
if (_book == null) {
|
||||||
|
_logger.warning('No book is set, not seeking');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (positionInBook == null) {
|
if (positionInBook == null) {
|
||||||
|
_logger.warning('Position given is null, not seeking');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final tracks = _book!.tracks;
|
final tracks = _book!.tracks;
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,11 @@
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
import 'package:vaani/api/api_provider.dart';
|
import 'package:vaani/api/api_provider.dart';
|
||||||
import 'package:vaani/features/player/core/audiobook_player.dart'
|
import 'package:vaani/features/player/core/audiobook_player.dart' as core;
|
||||||
as core;
|
|
||||||
|
|
||||||
part 'audiobook_player.g.dart';
|
part 'audiobook_player.g.dart';
|
||||||
|
|
||||||
// @Riverpod(keepAlive: true)
|
final _logger = Logger('AudiobookPlayerProvider');
|
||||||
// core.AudiobookPlayer audiobookPlayer(
|
|
||||||
// AudiobookPlayerRef ref,
|
|
||||||
// ) {
|
|
||||||
// final api = ref.watch(authenticatedApiProvider);
|
|
||||||
// final player = core.AudiobookPlayer(api.token!, api.baseUrl);
|
|
||||||
|
|
||||||
// ref.onDispose(player.dispose);
|
|
||||||
|
|
||||||
// return player;
|
|
||||||
// }
|
|
||||||
|
|
||||||
const playerId = 'audiobook_player';
|
const playerId = 'audiobook_player';
|
||||||
|
|
||||||
|
|
@ -32,6 +22,7 @@ class SimpleAudiobookPlayer extends _$SimpleAudiobookPlayer {
|
||||||
);
|
);
|
||||||
|
|
||||||
ref.onDispose(player.dispose);
|
ref.onDispose(player.dispose);
|
||||||
|
_logger.finer('created simple player');
|
||||||
|
|
||||||
return player;
|
return player;
|
||||||
}
|
}
|
||||||
|
|
@ -47,18 +38,16 @@ class AudiobookPlayer extends _$AudiobookPlayer {
|
||||||
|
|
||||||
// bind notify listeners to the player
|
// bind notify listeners to the player
|
||||||
player.playerStateStream.listen((_) {
|
player.playerStateStream.listen((_) {
|
||||||
notifyListeners();
|
ref.notifyListeners();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
_logger.finer('created player');
|
||||||
|
|
||||||
return player;
|
return player;
|
||||||
}
|
}
|
||||||
|
|
||||||
void notifyListeners() {
|
|
||||||
ref.notifyListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> setSpeed(double speed) async {
|
Future<void> setSpeed(double speed) async {
|
||||||
await state.setSpeed(speed);
|
await state.setSpeed(speed);
|
||||||
notifyListeners();
|
ref.notifyListeners();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ part of 'audiobook_player.dart';
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$simpleAudiobookPlayerHash() =>
|
String _$simpleAudiobookPlayerHash() =>
|
||||||
r'9e11ed2791d35e308f8cbe61a79a45cf51466ebb';
|
r'5e94bbff4314adceb5affa704fc4d079d4016afa';
|
||||||
|
|
||||||
/// Simple because it doesn't rebuild when the player state changes
|
/// Simple because it doesn't rebuild when the player state changes
|
||||||
/// it only rebuilds when the token changes
|
/// it only rebuilds when the token changes
|
||||||
|
|
@ -26,7 +26,7 @@ final simpleAudiobookPlayerProvider =
|
||||||
);
|
);
|
||||||
|
|
||||||
typedef _$SimpleAudiobookPlayer = Notifier<core.AudiobookPlayer>;
|
typedef _$SimpleAudiobookPlayer = Notifier<core.AudiobookPlayer>;
|
||||||
String _$audiobookPlayerHash() => r'44394b1dbbf85eb19ef1f693717e8cbc15b768e5';
|
String _$audiobookPlayerHash() => r'0f180308067486896fec6a65a6afb0e6686ac4a0';
|
||||||
|
|
||||||
/// See also [AudiobookPlayer].
|
/// See also [AudiobookPlayer].
|
||||||
@ProviderFor(AudiobookPlayer)
|
@ProviderFor(AudiobookPlayer)
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
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:vaani/features/player/providers/audiobook_player.dart';
|
import 'package:vaani/features/player/providers/audiobook_player.dart';
|
||||||
|
|
@ -5,12 +6,15 @@ import 'package:vaani/shared/extensions/model_conversions.dart';
|
||||||
|
|
||||||
part 'currently_playing_provider.g.dart';
|
part 'currently_playing_provider.g.dart';
|
||||||
|
|
||||||
|
final _logger = Logger('CurrentlyPlayingProvider');
|
||||||
|
|
||||||
@riverpod
|
@riverpod
|
||||||
BookExpanded? currentlyPlayingBook(CurrentlyPlayingBookRef ref) {
|
BookExpanded? currentlyPlayingBook(CurrentlyPlayingBookRef ref) {
|
||||||
try {
|
try {
|
||||||
final player = ref.watch(audiobookPlayerProvider);
|
final player = ref.watch(audiobookPlayerProvider);
|
||||||
return player.book;
|
return player.book;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
_logger.warning('Error getting currently playing book: $e');
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ part of 'currently_playing_provider.dart';
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$currentlyPlayingBookHash() =>
|
String _$currentlyPlayingBookHash() =>
|
||||||
r'52334c7b4d68fd498a2a00208d8d7f1ba0085237';
|
r'7440b0d54cb364f66e704783652e8f1490ae90e0';
|
||||||
|
|
||||||
/// See also [currentlyPlayingBook].
|
/// See also [currentlyPlayingBook].
|
||||||
@ProviderFor(currentlyPlayingBook)
|
@ProviderFor(currentlyPlayingBook)
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:vaani/features/player/providers/audiobook_player.dart';
|
import 'package:vaani/features/player/providers/audiobook_player.dart';
|
||||||
import 'package:vaani/features/player/providers/currently_playing_provider.dart';
|
import 'package:vaani/features/player/providers/currently_playing_provider.dart';
|
||||||
import 'package:vaani/features/player/view/player_when_expanded.dart';
|
import 'package:vaani/features/player/view/player_when_expanded.dart';
|
||||||
|
import 'package:vaani/main.dart';
|
||||||
import 'package:vaani/shared/extensions/chapter.dart';
|
import 'package:vaani/shared/extensions/chapter.dart';
|
||||||
import 'package:vaani/shared/extensions/duration_format.dart';
|
import 'package:vaani/shared/extensions/duration_format.dart';
|
||||||
import 'package:vaani/shared/hooks.dart';
|
import 'package:vaani/shared/hooks.dart';
|
||||||
|
|
@ -55,7 +56,7 @@ class ChapterSelectionModal extends HookConsumerWidget {
|
||||||
final currentChapterIndex = currentChapter?.id;
|
final currentChapterIndex = currentChapter?.id;
|
||||||
final chapterKey = GlobalKey();
|
final chapterKey = GlobalKey();
|
||||||
scrollToCurrentChapter() async {
|
scrollToCurrentChapter() async {
|
||||||
debugPrint('scrolling to chapter');
|
appLogger.fine('scrolling to chapter');
|
||||||
await Scrollable.ensureVisible(
|
await Scrollable.ensureVisible(
|
||||||
chapterKey.currentContext!,
|
chapterKey.currentContext!,
|
||||||
duration: 200.ms,
|
duration: 200.ms,
|
||||||
|
|
|
||||||
|
|
@ -69,7 +69,7 @@ class SleepTimer {
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
_logger.fine('created with duration: $duration');
|
_logger.info('created with duration: $duration');
|
||||||
}
|
}
|
||||||
|
|
||||||
/// resets the timer and stops it
|
/// resets the timer and stops it
|
||||||
|
|
@ -90,7 +90,7 @@ class SleepTimer {
|
||||||
if (player.playing) {
|
if (player.playing) {
|
||||||
startCountDown();
|
startCountDown();
|
||||||
}
|
}
|
||||||
_logger.fine('restarted timer');
|
_logger.info('restarted timer');
|
||||||
}
|
}
|
||||||
|
|
||||||
/// starts the timer with the given duration or the default duration
|
/// starts the timer with the given duration or the default duration
|
||||||
|
|
@ -105,7 +105,7 @@ class SleepTimer {
|
||||||
_logger.fine('paused player after $duration');
|
_logger.fine('paused player after $duration');
|
||||||
});
|
});
|
||||||
startedAt = DateTime.now();
|
startedAt = DateTime.now();
|
||||||
_logger.fine('started for $duration at $startedAt');
|
_logger.info('started for $duration at $startedAt');
|
||||||
}
|
}
|
||||||
|
|
||||||
Duration get remainingTime {
|
Duration get remainingTime {
|
||||||
|
|
@ -130,6 +130,6 @@ class SleepTimer {
|
||||||
for (var sub in _subscriptions) {
|
for (var sub in _subscriptions) {
|
||||||
sub.cancel();
|
sub.cancel();
|
||||||
}
|
}
|
||||||
_logger.fine('disposed');
|
_logger.info('disposed');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,12 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:vaani/api/api_provider.dart';
|
import 'package:vaani/api/api_provider.dart';
|
||||||
import 'package:vaani/api/authenticated_user_provider.dart';
|
import 'package:vaani/api/authenticated_user_provider.dart';
|
||||||
import 'package:vaani/api/server_provider.dart';
|
import 'package:vaani/api/server_provider.dart';
|
||||||
|
import 'package:vaani/main.dart';
|
||||||
import 'package:vaani/models/error_response.dart';
|
import 'package:vaani/models/error_response.dart';
|
||||||
import 'package:vaani/router/router.dart';
|
import 'package:vaani/router/router.dart';
|
||||||
import 'package:vaani/settings/api_settings_provider.dart';
|
import 'package:vaani/settings/api_settings_provider.dart';
|
||||||
import 'package:vaani/settings/models/models.dart' as model;
|
import 'package:vaani/settings/models/models.dart' as model;
|
||||||
|
import 'package:vaani/shared/extensions/obfuscation.dart';
|
||||||
import 'package:vaani/shared/widgets/add_new_server.dart';
|
import 'package:vaani/shared/widgets/add_new_server.dart';
|
||||||
|
|
||||||
class ServerManagerPage extends HookConsumerWidget {
|
class ServerManagerPage extends HookConsumerWidget {
|
||||||
|
|
@ -25,8 +27,8 @@ class ServerManagerPage extends HookConsumerWidget {
|
||||||
final serverURIController = useTextEditingController();
|
final serverURIController = useTextEditingController();
|
||||||
final formKey = GlobalKey<FormState>();
|
final formKey = GlobalKey<FormState>();
|
||||||
|
|
||||||
debugPrint('registered servers: $registeredServers');
|
appLogger.fine('registered servers: ${registeredServers.obfuscate()}');
|
||||||
debugPrint('available users: $availableUsers');
|
appLogger.fine('available users: ${availableUsers.obfuscate()}');
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text('Manage Accounts'),
|
title: const Text('Manage Accounts'),
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:shelfsdk/audiobookshelf_api.dart';
|
|
||||||
import 'package:vaani/api/api_provider.dart';
|
import 'package:vaani/api/api_provider.dart';
|
||||||
import 'package:vaani/router/router.dart';
|
import 'package:vaani/router/router.dart';
|
||||||
|
import 'package:vaani/settings/constants.dart';
|
||||||
import 'package:vaani/shared/utils.dart';
|
import 'package:vaani/shared/utils.dart';
|
||||||
import 'package:vaani/shared/widgets/not_implemented.dart';
|
import 'package:vaani/shared/widgets/not_implemented.dart';
|
||||||
|
|
||||||
|
|
@ -12,27 +12,6 @@ class YouPage extends HookConsumerWidget {
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
|
||||||
final me = ref.watch(meProvider);
|
|
||||||
return me.when(
|
|
||||||
data: (data) {
|
|
||||||
return _YouPage(userData: data);
|
|
||||||
},
|
|
||||||
loading: () => const CircularProgressIndicator(),
|
|
||||||
error: (error, stack) => Text('Error: $error'),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _YouPage extends HookConsumerWidget {
|
|
||||||
const _YouPage({
|
|
||||||
super.key,
|
|
||||||
required this.userData,
|
|
||||||
});
|
|
||||||
|
|
||||||
final User userData;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final api = ref.watch(authenticatedApiProvider);
|
final api = ref.watch(authenticatedApiProvider);
|
||||||
|
|
@ -41,14 +20,21 @@ class _YouPage extends HookConsumerWidget {
|
||||||
// title: const Text('You'),
|
// title: const Text('You'),
|
||||||
backgroundColor: Colors.transparent,
|
backgroundColor: Colors.transparent,
|
||||||
actions: [
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
tooltip: 'Logs',
|
||||||
|
icon: const Icon(Icons.bug_report),
|
||||||
|
onPressed: () {
|
||||||
|
context.pushNamed(Routes.logs.name);
|
||||||
|
},
|
||||||
|
),
|
||||||
// IconButton(
|
// IconButton(
|
||||||
// icon: const Icon(Icons.edit),
|
// icon: const Icon(Icons.edit),
|
||||||
// onPressed: () {
|
// onPressed: () {
|
||||||
// // Handle edit profile
|
// // Handle edit profile
|
||||||
// },
|
// },
|
||||||
// ),
|
// ),
|
||||||
// settings button
|
|
||||||
IconButton(
|
IconButton(
|
||||||
|
tooltip: 'Settings',
|
||||||
icon: const Icon(Icons.settings),
|
icon: const Icon(Icons.settings),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
context.pushNamed(Routes.settings.name);
|
context.pushNamed(Routes.settings.name);
|
||||||
|
|
@ -64,30 +50,7 @@ class _YouPage extends HookConsumerWidget {
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Row(
|
UserBar(),
|
||||||
children: [
|
|
||||||
CircleAvatar(
|
|
||||||
radius: 40,
|
|
||||||
// backgroundImage: NetworkImage(userData.avatarUrl),
|
|
||||||
// first letter of the username
|
|
||||||
child: Text(
|
|
||||||
userData.username[0].toUpperCase(),
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 32,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 16),
|
|
||||||
Text(
|
|
||||||
userData.username,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 24,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Wrap(
|
Wrap(
|
||||||
spacing: 8,
|
spacing: 8,
|
||||||
|
|
@ -121,21 +84,6 @@ class _YouPage extends HookConsumerWidget {
|
||||||
title: const Text('My Playlists'),
|
title: const Text('My Playlists'),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
// Handle navigation to playlists
|
// Handle navigation to playlists
|
||||||
},
|
|
||||||
),
|
|
||||||
ListTile(
|
|
||||||
leading: const Icon(Icons.help),
|
|
||||||
title: const Text('Help'),
|
|
||||||
onTap: () {
|
|
||||||
// Handle navigation to help website
|
|
||||||
showNotImplementedToast(context);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
ListTile(
|
|
||||||
leading: const Icon(Icons.info),
|
|
||||||
title: const Text('About'),
|
|
||||||
onTap: () {
|
|
||||||
// Handle navigation to about
|
|
||||||
showNotImplementedToast(context);
|
showNotImplementedToast(context);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
@ -149,10 +97,40 @@ class _YouPage extends HookConsumerWidget {
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
// const SizedBox(height: 16),
|
ListTile(
|
||||||
// const Text('App Version: 1.0.0'),
|
leading: const Icon(Icons.help),
|
||||||
// const Text('Server Version: 1.0.0'),
|
title: const Text('Help'),
|
||||||
// const Text('Author: Your Name'),
|
onTap: () {
|
||||||
|
// Handle navigation to help website
|
||||||
|
showNotImplementedToast(context);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
|
||||||
|
AboutListTile(
|
||||||
|
icon: const Icon(Icons.info),
|
||||||
|
applicationName: AppMetadata.appName,
|
||||||
|
applicationVersion: AppMetadata.version,
|
||||||
|
applicationLegalese:
|
||||||
|
'Made with ❤️ by ${AppMetadata.author}',
|
||||||
|
aboutBoxChildren: [
|
||||||
|
// link to github repo
|
||||||
|
ListTile(
|
||||||
|
leading: Icon(Icons.code),
|
||||||
|
title: Text('Source Code'),
|
||||||
|
onTap: () {
|
||||||
|
handleLaunchUrl(AppMetadata.githubRepo);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
// apply blend mode to the icon to match the primary color
|
||||||
|
applicationIcon: ColorFiltered(
|
||||||
|
colorFilter: ColorFilter.mode(
|
||||||
|
Theme.of(context).colorScheme.primary,
|
||||||
|
BlendMode.srcIn,
|
||||||
|
),
|
||||||
|
child: const VaaniLogo(),
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -162,3 +140,71 @@ class _YouPage extends HookConsumerWidget {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class UserBar extends HookConsumerWidget {
|
||||||
|
const UserBar({
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final me = ref.watch(meProvider);
|
||||||
|
|
||||||
|
return me.when(
|
||||||
|
data: (userData) {
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
CircleAvatar(
|
||||||
|
radius: 40,
|
||||||
|
// backgroundImage: NetworkImage(userData.avatarUrl),
|
||||||
|
// first letter of the username
|
||||||
|
child: Text(
|
||||||
|
userData.username[0].toUpperCase(),
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 32,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Text(
|
||||||
|
userData.username,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 24,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
loading: () => const CircularProgressIndicator(),
|
||||||
|
error: (error, stack) => Text('Error: $error'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class VaaniLogo extends StatelessWidget {
|
||||||
|
const VaaniLogo({
|
||||||
|
super.key,
|
||||||
|
this.size,
|
||||||
|
this.duration = const Duration(milliseconds: 750),
|
||||||
|
this.curve = Curves.fastOutSlowIn,
|
||||||
|
});
|
||||||
|
|
||||||
|
final double? size;
|
||||||
|
final Duration duration;
|
||||||
|
final Curve curve;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final IconThemeData iconTheme = IconTheme.of(context);
|
||||||
|
final double? iconSize = size ?? iconTheme.size;
|
||||||
|
return AnimatedContainer(
|
||||||
|
width: iconSize,
|
||||||
|
height: iconSize,
|
||||||
|
duration: duration,
|
||||||
|
curve: curve,
|
||||||
|
child: Image.asset('assets/images/vaani_logo_foreground.png'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import 'package:logging/logging.dart';
|
||||||
import 'package:vaani/api/server_provider.dart';
|
import 'package:vaani/api/server_provider.dart';
|
||||||
import 'package:vaani/db/storage.dart';
|
import 'package:vaani/db/storage.dart';
|
||||||
import 'package:vaani/features/downloads/providers/download_manager.dart';
|
import 'package:vaani/features/downloads/providers/download_manager.dart';
|
||||||
|
import 'package:vaani/features/logging/core/logger.dart';
|
||||||
import 'package:vaani/features/playback_reporting/providers/playback_reporter_provider.dart';
|
import 'package:vaani/features/playback_reporting/providers/playback_reporter_provider.dart';
|
||||||
import 'package:vaani/features/player/core/init.dart';
|
import 'package:vaani/features/player/core/init.dart';
|
||||||
import 'package:vaani/features/player/providers/audiobook_player.dart';
|
import 'package:vaani/features/player/providers/audiobook_player.dart';
|
||||||
|
|
@ -12,7 +13,6 @@ import 'package:vaani/features/sleep_timer/providers/sleep_timer_provider.dart';
|
||||||
import 'package:vaani/router/router.dart';
|
import 'package:vaani/router/router.dart';
|
||||||
import 'package:vaani/settings/api_settings_provider.dart';
|
import 'package:vaani/settings/api_settings_provider.dart';
|
||||||
import 'package:vaani/settings/app_settings_provider.dart';
|
import 'package:vaani/settings/app_settings_provider.dart';
|
||||||
import 'package:vaani/shared/extensions/duration_format.dart';
|
|
||||||
import 'package:vaani/theme/theme.dart';
|
import 'package:vaani/theme/theme.dart';
|
||||||
|
|
||||||
final appLogger = Logger('vaani');
|
final appLogger = Logger('vaani');
|
||||||
|
|
@ -20,13 +20,7 @@ final appLogger = Logger('vaani');
|
||||||
void main() async {
|
void main() async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
// Configure the root Logger
|
// Configure the root Logger
|
||||||
Logger.root.level = Level.FINE; // Capture all logs
|
await initLogging();
|
||||||
Logger.root.onRecord.listen((record) {
|
|
||||||
// Print log records to the console
|
|
||||||
debugPrint(
|
|
||||||
'${record.loggerName}: ${record.level.name}: ${record.time.time}: ${record.message}',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
// initialize the storage
|
// initialize the storage
|
||||||
await initStorage();
|
await initStorage();
|
||||||
|
|
@ -34,7 +28,6 @@ void main() async {
|
||||||
// initialize audio player
|
// initialize audio player
|
||||||
await configurePlayer();
|
await configurePlayer();
|
||||||
|
|
||||||
|
|
||||||
// run the app
|
// run the app
|
||||||
runApp(
|
runApp(
|
||||||
const ProviderScope(
|
const ProviderScope(
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
|
import 'package:vaani/shared/extensions/obfuscation.dart';
|
||||||
|
|
||||||
final _logger = Logger('ErrorResponse');
|
final _logger = Logger('ErrorResponse');
|
||||||
|
|
||||||
|
|
@ -13,7 +14,7 @@ class ErrorResponseHandler {
|
||||||
}) : _response = response ?? http.Response('', 418);
|
}) : _response = response ?? http.Response('', 418);
|
||||||
|
|
||||||
void storeError(http.Response response, [Object? error]) {
|
void storeError(http.Response response, [Object? error]) {
|
||||||
_logger.fine('for $name got response: $response');
|
_logger.fine('for $name got response: ${response.obfuscate()}');
|
||||||
_response = response;
|
_response = response;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:vaani/api/api_provider.dart';
|
import 'package:vaani/api/api_provider.dart';
|
||||||
|
import 'package:vaani/main.dart';
|
||||||
import 'package:vaani/router/router.dart';
|
import 'package:vaani/router/router.dart';
|
||||||
import 'package:vaani/settings/api_settings_provider.dart';
|
import 'package:vaani/settings/api_settings_provider.dart';
|
||||||
|
|
||||||
|
|
@ -59,7 +60,7 @@ class HomePage extends HookConsumerWidget {
|
||||||
final shelvesToDisplay = data
|
final shelvesToDisplay = data
|
||||||
// .where((element) => !element.id.contains('discover'))
|
// .where((element) => !element.id.contains('discover'))
|
||||||
.map((shelf) {
|
.map((shelf) {
|
||||||
debugPrint('building shelf ${shelf.label}');
|
appLogger.fine('building shelf ${shelf.label}');
|
||||||
return HomeShelf(
|
return HomeShelf(
|
||||||
title: shelf.label,
|
title: shelf.label,
|
||||||
shelf: shelf,
|
shelf: shelf,
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:vaani/api/api_provider.dart';
|
import 'package:vaani/api/api_provider.dart';
|
||||||
|
import 'package:vaani/main.dart';
|
||||||
import 'package:vaani/settings/api_settings_provider.dart';
|
import 'package:vaani/settings/api_settings_provider.dart';
|
||||||
|
|
||||||
import '../shared/widgets/drawer.dart';
|
import '../shared/widgets/drawer.dart';
|
||||||
|
|
@ -47,7 +48,7 @@ class LibraryPage extends HookConsumerWidget {
|
||||||
final shelvesToDisplay = data
|
final shelvesToDisplay = data
|
||||||
// .where((element) => !element.id.contains('discover'))
|
// .where((element) => !element.id.contains('discover'))
|
||||||
.map((shelf) {
|
.map((shelf) {
|
||||||
debugPrint('building shelf ${shelf.label}');
|
appLogger.fine('building shelf ${shelf.label}');
|
||||||
return HomeShelf(
|
return HomeShelf(
|
||||||
title: shelf.label,
|
title: shelf.label,
|
||||||
shelf: shelf,
|
shelf: shelf,
|
||||||
|
|
|
||||||
|
|
@ -90,6 +90,12 @@ class Routes {
|
||||||
name: 'openIDCallback',
|
name: 'openIDCallback',
|
||||||
parentRoute: onboarding,
|
parentRoute: onboarding,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// logs page
|
||||||
|
static const logs = _SimpleRoute(
|
||||||
|
pathName: 'logs',
|
||||||
|
name: 'logs',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// a class to store path
|
// a class to store path
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,12 @@ import 'package:vaani/features/explore/view/explore_page.dart';
|
||||||
import 'package:vaani/features/explore/view/search_result_page.dart';
|
import 'package:vaani/features/explore/view/search_result_page.dart';
|
||||||
import 'package:vaani/features/item_viewer/view/library_item_page.dart';
|
import 'package:vaani/features/item_viewer/view/library_item_page.dart';
|
||||||
import 'package:vaani/features/library_browser/view/library_browser_page.dart';
|
import 'package:vaani/features/library_browser/view/library_browser_page.dart';
|
||||||
|
import 'package:vaani/features/logging/view/logs_page.dart';
|
||||||
import 'package:vaani/features/onboarding/view/callback_page.dart';
|
import 'package:vaani/features/onboarding/view/callback_page.dart';
|
||||||
import 'package:vaani/features/onboarding/view/onboarding_single_page.dart';
|
import 'package:vaani/features/onboarding/view/onboarding_single_page.dart';
|
||||||
import 'package:vaani/features/you/view/server_manager.dart';
|
import 'package:vaani/features/you/view/server_manager.dart';
|
||||||
import 'package:vaani/features/you/view/you_page.dart';
|
import 'package:vaani/features/you/view/you_page.dart';
|
||||||
|
import 'package:vaani/main.dart';
|
||||||
import 'package:vaani/pages/home_page.dart';
|
import 'package:vaani/pages/home_page.dart';
|
||||||
import 'package:vaani/settings/view/app_settings_page.dart';
|
import 'package:vaani/settings/view/app_settings_page.dart';
|
||||||
import 'package:vaani/settings/view/auto_sleep_timer_settings_page.dart';
|
import 'package:vaani/settings/view/auto_sleep_timer_settings_page.dart';
|
||||||
|
|
@ -215,6 +217,14 @@ class MyAppRouter {
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
||||||
|
// loggers page
|
||||||
|
GoRoute(
|
||||||
|
path: Routes.logs.localPath,
|
||||||
|
name: Routes.logs.name,
|
||||||
|
// builder: (context, state) => const LogsPage(),
|
||||||
|
pageBuilder: defaultPageBuilder(const LogsPage()),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -225,7 +235,7 @@ class MyAppRouter {
|
||||||
// extract the code and state from the uri
|
// extract the code and state from the uri
|
||||||
final code = state.uri.queryParameters['code'];
|
final code = state.uri.queryParameters['code'];
|
||||||
final stateParam = state.uri.queryParameters['state'];
|
final stateParam = state.uri.queryParameters['state'];
|
||||||
debugPrint('deep linking callback: code: $code, state: $stateParam');
|
appLogger.fine('deep linking callback: code: $code, state: $stateParam');
|
||||||
|
|
||||||
var callbackPage =
|
var callbackPage =
|
||||||
CallbackPage(code: code, state: stateParam, key: ValueKey(stateParam));
|
CallbackPage(code: code, state: stateParam, key: ValueKey(stateParam));
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import 'package:vaani/features/explore/providers/search_controller.dart';
|
||||||
import 'package:vaani/features/player/providers/player_form.dart';
|
import 'package:vaani/features/player/providers/player_form.dart';
|
||||||
import 'package:vaani/features/player/view/audiobook_player.dart';
|
import 'package:vaani/features/player/view/audiobook_player.dart';
|
||||||
import 'package:vaani/features/player/view/player_when_expanded.dart';
|
import 'package:vaani/features/player/view/player_when_expanded.dart';
|
||||||
|
import 'package:vaani/main.dart';
|
||||||
import 'package:vaani/router/router.dart';
|
import 'package:vaani/router/router.dart';
|
||||||
|
|
||||||
// stack to track changes in navigationShell.currentIndex
|
// stack to track changes in navigationShell.currentIndex
|
||||||
|
|
@ -42,13 +43,13 @@ class ScaffoldWithNavBar extends HookConsumerWidget {
|
||||||
onBackButtonPressed() async {
|
onBackButtonPressed() async {
|
||||||
final isPlayerExpanded = playerProgress != playerMinHeight;
|
final isPlayerExpanded = playerProgress != playerMinHeight;
|
||||||
|
|
||||||
debugPrint(
|
appLogger.fine(
|
||||||
'BackButtonListener: Back button pressed, isPlayerExpanded: $isPlayerExpanded, stack: $navigationShellStack, pendingPlayerModals: $pendingPlayerModals',
|
'BackButtonListener: Back button pressed, isPlayerExpanded: $isPlayerExpanded, stack: $navigationShellStack, pendingPlayerModals: $pendingPlayerModals',
|
||||||
);
|
);
|
||||||
|
|
||||||
// close miniplayer if it is open
|
// close miniplayer if it is open
|
||||||
if (isPlayerExpanded && pendingPlayerModals == 0) {
|
if (isPlayerExpanded && pendingPlayerModals == 0) {
|
||||||
debugPrint(
|
appLogger.fine(
|
||||||
'BackButtonListener: closing the player',
|
'BackButtonListener: closing the player',
|
||||||
);
|
);
|
||||||
audioBookMiniplayerController.animateToHeight(state: PanelState.MIN);
|
audioBookMiniplayerController.animateToHeight(state: PanelState.MIN);
|
||||||
|
|
@ -59,7 +60,7 @@ class ScaffoldWithNavBar extends HookConsumerWidget {
|
||||||
final canPop = GoRouter.of(context).canPop();
|
final canPop = GoRouter.of(context).canPop();
|
||||||
|
|
||||||
if (canPop) {
|
if (canPop) {
|
||||||
debugPrint(
|
appLogger.fine(
|
||||||
'BackButtonListener: passing it to the router as canPop is true',
|
'BackButtonListener: passing it to the router as canPop is true',
|
||||||
);
|
);
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -69,7 +70,7 @@ class ScaffoldWithNavBar extends HookConsumerWidget {
|
||||||
// pop the last index from the stack and navigate to it
|
// pop the last index from the stack and navigate to it
|
||||||
final index = navigationShellStack.last;
|
final index = navigationShellStack.last;
|
||||||
navigationShellStack.remove(index);
|
navigationShellStack.remove(index);
|
||||||
debugPrint('BackButtonListener: popping the stack, index: $index');
|
appLogger.fine('BackButtonListener: popping the stack, index: $index');
|
||||||
|
|
||||||
// if the stack is empty, navigate to home else navigate to the last index
|
// if the stack is empty, navigate to home else navigate to the last index
|
||||||
if (navigationShellStack.isNotEmpty) {
|
if (navigationShellStack.isNotEmpty) {
|
||||||
|
|
@ -79,12 +80,12 @@ class ScaffoldWithNavBar extends HookConsumerWidget {
|
||||||
}
|
}
|
||||||
if (navigationShell.currentIndex != 0) {
|
if (navigationShell.currentIndex != 0) {
|
||||||
// if the stack is empty and the current branch is not home, navigate to home
|
// if the stack is empty and the current branch is not home, navigate to home
|
||||||
debugPrint('BackButtonListener: navigating to home');
|
appLogger.fine('BackButtonListener: navigating to home');
|
||||||
navigationShell.goBranch(0);
|
navigationShell.goBranch(0);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
debugPrint('BackButtonListener: passing it to the router');
|
appLogger.fine('BackButtonListener: passing it to the router');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -149,12 +150,11 @@ class ScaffoldWithNavBar extends HookConsumerWidget {
|
||||||
navigationShellStack.remove(index);
|
navigationShellStack.remove(index);
|
||||||
}
|
}
|
||||||
navigationShellStack.add(index);
|
navigationShellStack.add(index);
|
||||||
debugPrint('Tapped index: $index, stack: $navigationShellStack');
|
appLogger.fine('Tapped index: $index, stack: $navigationShellStack');
|
||||||
|
|
||||||
// Check if the current branch is the same as the branch that was tapped.
|
// Check if the current branch is the same as the branch that was tapped.
|
||||||
// If it is, debugPrint a message to the console.
|
|
||||||
if (index == navigationShell.currentIndex) {
|
if (index == navigationShell.currentIndex) {
|
||||||
debugPrint('Tapped the current branch');
|
appLogger.fine('Tapped the current branch');
|
||||||
|
|
||||||
// if current branch is explore, open the search view
|
// if current branch is explore, open the search view
|
||||||
if (index == 2) {
|
if (index == 2) {
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import 'package:logging/logging.dart';
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
import 'package:vaani/db/available_boxes.dart';
|
import 'package:vaani/db/available_boxes.dart';
|
||||||
import 'package:vaani/settings/models/api_settings.dart' as model;
|
import 'package:vaani/settings/models/api_settings.dart' as model;
|
||||||
|
import 'package:vaani/shared/extensions/obfuscation.dart';
|
||||||
|
|
||||||
part 'api_settings_provider.g.dart';
|
part 'api_settings_provider.g.dart';
|
||||||
|
|
||||||
|
|
@ -19,6 +20,7 @@ class ApiSettings extends _$ApiSettings {
|
||||||
ref.listenSelf((_, __) {
|
ref.listenSelf((_, __) {
|
||||||
writeToBox();
|
writeToBox();
|
||||||
});
|
});
|
||||||
|
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -33,7 +35,7 @@ class ApiSettings extends _$ApiSettings {
|
||||||
activeServer: foundSettings.activeUser?.server,
|
activeServer: foundSettings.activeUser?.server,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
_logger.fine('found api settings in box: $foundSettings');
|
_logger.fine('found api settings in box: ${foundSettings.obfuscate()}');
|
||||||
return foundSettings;
|
return foundSettings;
|
||||||
} else {
|
} else {
|
||||||
// create a new settings object
|
// create a new settings object
|
||||||
|
|
@ -47,7 +49,7 @@ class ApiSettings extends _$ApiSettings {
|
||||||
void writeToBox() {
|
void writeToBox() {
|
||||||
_box.clear();
|
_box.clear();
|
||||||
_box.add(state);
|
_box.add(state);
|
||||||
_logger.fine('wrote api settings to box: $state');
|
_logger.fine('wrote api settings to box: ${state.obfuscate()}');
|
||||||
}
|
}
|
||||||
|
|
||||||
void updateState(model.ApiSettings newSettings, {bool force = false}) {
|
void updateState(model.ApiSettings newSettings, {bool force = false}) {
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ part of 'api_settings_provider.dart';
|
||||||
// RiverpodGenerator
|
// RiverpodGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$apiSettingsHash() => r'26e7e09e7369bac9fbf0589da9fd97d1f15b7926';
|
String _$apiSettingsHash() => r'5bc1e16e9d72b77fb10637aabadf08e8947da580';
|
||||||
|
|
||||||
/// See also [ApiSettings].
|
/// See also [ApiSettings].
|
||||||
@ProviderFor(ApiSettings)
|
@ProviderFor(ApiSettings)
|
||||||
|
|
|
||||||
|
|
@ -10,5 +10,10 @@ class AppMetadata {
|
||||||
// for deeplinking
|
// for deeplinking
|
||||||
static const String appScheme = 'vaani';
|
static const String appScheme = 'vaani';
|
||||||
|
|
||||||
|
static const version = '1.0.0';
|
||||||
|
static const author = 'Dr.Blank';
|
||||||
|
|
||||||
|
static Uri githubRepo = Uri.parse('https://github.com/Dr-Blank/Vaani');
|
||||||
|
|
||||||
static get appNameLowerCase => appName.toLowerCase().replaceAll(' ', '_');
|
static get appNameLowerCase => appName.toLowerCase().replaceAll(' ', '_');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
125
lib/shared/extensions/obfuscation.dart
Normal file
125
lib/shared/extensions/obfuscation.dart
Normal file
|
|
@ -0,0 +1,125 @@
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:http/http.dart' as http;
|
||||||
|
import 'package:vaani/settings/models/api_settings.dart';
|
||||||
|
import 'package:vaani/settings/models/audiobookshelf_server.dart';
|
||||||
|
import 'package:vaani/settings/models/authenticated_user.dart';
|
||||||
|
|
||||||
|
// bool kReleaseMode = true;
|
||||||
|
|
||||||
|
extension ObfuscateString on String {
|
||||||
|
String obfuscate() {
|
||||||
|
if (!kReleaseMode) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
return 'obfuscated';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ObfuscateURI on Uri {
|
||||||
|
/// keeps everything except the base url for security reasons
|
||||||
|
Uri obfuscate() {
|
||||||
|
if (!kReleaseMode) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
// do not obfuscate the local host
|
||||||
|
if ([null, 'localhost'].contains(host)) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
// do not obfuscate file urls
|
||||||
|
if (scheme == 'file') {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
return replace(
|
||||||
|
userInfo: userInfo == '' ? '' : 'userInfoObfuscated',
|
||||||
|
host: 'hostObfuscated',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ObfuscateList<T> on List<T> {
|
||||||
|
List<T> obfuscate() {
|
||||||
|
return map((e) {
|
||||||
|
if (e is AuthenticatedUser) {
|
||||||
|
return e.obfuscate() as T;
|
||||||
|
} else if (e is AudiobookShelfServer) {
|
||||||
|
return e.obfuscate() as T;
|
||||||
|
} else if (e is Uri) {
|
||||||
|
return e.obfuscate() as T;
|
||||||
|
} else {
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
}).toList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ObfuscateSet<T> on Set<T> {
|
||||||
|
Set<T> obfuscate() {
|
||||||
|
return toList().obfuscate().toSet();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ObfuscateAuthenticatedUser on AuthenticatedUser {
|
||||||
|
AuthenticatedUser obfuscate() {
|
||||||
|
if (!kReleaseMode) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
return copyWith(
|
||||||
|
password: password == null ? null : 'passwordObfuscated',
|
||||||
|
username: username == null ? null : 'usernameObfuscated',
|
||||||
|
authToken: 'authTokenObfuscated',
|
||||||
|
server: server.obfuscate(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ObfuscateServer on AudiobookShelfServer {
|
||||||
|
AudiobookShelfServer obfuscate() {
|
||||||
|
if (!kReleaseMode) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
return copyWith(
|
||||||
|
serverUrl: serverUrl.obfuscate(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ObfuscateApiSettings on ApiSettings {
|
||||||
|
ApiSettings obfuscate() {
|
||||||
|
if (!kReleaseMode) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
return copyWith(
|
||||||
|
activeServer: activeServer?.obfuscate(),
|
||||||
|
activeUser: activeUser?.obfuscate(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ObfuscateRequest on http.BaseRequest {
|
||||||
|
http.BaseRequest obfuscate() {
|
||||||
|
if (!kReleaseMode) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
return http.Request(
|
||||||
|
method,
|
||||||
|
url.obfuscate(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ObfuscateResponse on http.Response {
|
||||||
|
http.Response obfuscate() {
|
||||||
|
if (!kReleaseMode) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
return http.Response(
|
||||||
|
body,
|
||||||
|
statusCode,
|
||||||
|
headers: headers,
|
||||||
|
request: request?.obfuscate(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
44
pubspec.lock
44
pubspec.lock
|
|
@ -47,7 +47,7 @@ packages:
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.10"
|
version: "2.0.10"
|
||||||
archive:
|
archive:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: archive
|
name: archive
|
||||||
sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d
|
sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d
|
||||||
|
|
@ -366,6 +366,22 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "7.0.1"
|
version: "7.0.1"
|
||||||
|
dio:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: dio
|
||||||
|
sha256: "5598aa796bbf4699afd5c67c0f5f6e2ed542afc956884b9cd58c306966efc260"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "5.7.0"
|
||||||
|
dio_web_adapter:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: dio_web_adapter
|
||||||
|
sha256: "33259a9276d6cea88774a0000cfae0d861003497755969c92faa223108620dc8"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.0"
|
||||||
duration_picker:
|
duration_picker:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
|
@ -407,7 +423,7 @@ packages:
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "7.0.0"
|
version: "7.0.0"
|
||||||
file_picker:
|
file_picker:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: file_picker
|
name: file_picker
|
||||||
sha256: "167bb619cdddaa10ef2907609feb8a79c16dfa479d3afaf960f8e223f754bf12"
|
sha256: "167bb619cdddaa10ef2907609feb8a79c16dfa479d3afaf960f8e223f754bf12"
|
||||||
|
|
@ -782,6 +798,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.2.0"
|
version: "1.2.0"
|
||||||
|
logging_appenders:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: logging_appenders
|
||||||
|
sha256: e329e7472f99416d0edaaf6451fe6c02dec91d34535bd252e284a0b94ab23d79
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.3.1"
|
||||||
lottie:
|
lottie:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
|
@ -1095,6 +1119,22 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.0"
|
version: "2.0.0"
|
||||||
|
share_plus:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: share_plus
|
||||||
|
sha256: "468c43f285207c84bcabf5737f33b914ceb8eb38398b91e5e3ad1698d1b72a52"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "10.0.2"
|
||||||
|
share_plus_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: share_plus_platform_interface
|
||||||
|
sha256: "6ababf341050edff57da8b6990f11f4e99eaba837865e2e6defe16d039619db5"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "5.0.0"
|
||||||
shelf:
|
shelf:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ isar_version: &isar_version ^4.0.0-dev.13 # define the version to be used
|
||||||
dependencies:
|
dependencies:
|
||||||
animated_list_plus: ^0.5.2
|
animated_list_plus: ^0.5.2
|
||||||
animated_theme_switcher: ^2.0.10
|
animated_theme_switcher: ^2.0.10
|
||||||
|
archive: ^3.6.1
|
||||||
audio_service: ^0.18.15
|
audio_service: ^0.18.15
|
||||||
audio_session: ^0.1.19
|
audio_session: ^0.1.19
|
||||||
audio_video_progress_bar: ^2.0.2
|
audio_video_progress_bar: ^2.0.2
|
||||||
|
|
@ -44,6 +45,7 @@ dependencies:
|
||||||
device_info_plus: ^10.1.0
|
device_info_plus: ^10.1.0
|
||||||
duration_picker: ^1.2.0
|
duration_picker: ^1.2.0
|
||||||
easy_stepper: ^0.8.4
|
easy_stepper: ^0.8.4
|
||||||
|
file_picker: ^8.1.2
|
||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
flutter_animate: ^4.5.0
|
flutter_animate: ^4.5.0
|
||||||
|
|
@ -69,6 +71,7 @@ dependencies:
|
||||||
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
|
logging: ^1.2.0
|
||||||
|
logging_appenders: ^1.3.1
|
||||||
lottie: ^3.1.0
|
lottie: ^3.1.0
|
||||||
material_symbols_icons: ^4.2785.1
|
material_symbols_icons: ^4.2785.1
|
||||||
media_kit_libs_linux: any
|
media_kit_libs_linux: any
|
||||||
|
|
@ -84,6 +87,7 @@ dependencies:
|
||||||
riverpod_annotation: ^2.3.5
|
riverpod_annotation: ^2.3.5
|
||||||
scroll_loop_auto_scroll: ^0.0.5
|
scroll_loop_auto_scroll: ^0.0.5
|
||||||
sensors_plus: ^6.0.1
|
sensors_plus: ^6.0.1
|
||||||
|
share_plus: ^10.0.2
|
||||||
shelfsdk:
|
shelfsdk:
|
||||||
path: ./shelfsdk
|
path: ./shelfsdk
|
||||||
shimmer: ^3.0.0
|
shimmer: ^3.0.0
|
||||||
|
|
@ -114,6 +118,7 @@ flutter:
|
||||||
- assets/
|
- assets/
|
||||||
- assets/animations/
|
- assets/animations/
|
||||||
- assets/sounds/
|
- assets/sounds/
|
||||||
|
- assets/images/
|
||||||
# - images/a_dot_burr.jpeg
|
# - images/a_dot_burr.jpeg
|
||||||
# - images/a_dot_ham.jpeg
|
# - images/a_dot_ham.jpeg
|
||||||
# An image asset can refer to one or more resolution-specific "variants", see
|
# An image asset can refer to one or more resolution-specific "variants", see
|
||||||
|
|
|
||||||
24
test/features/logging/providers/logs_provider_test.dart
Normal file
24
test/features/logging/providers/logs_provider_test.dart
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
import 'package:logging_appenders/logging_appenders.dart';
|
||||||
|
import 'package:vaani/features/logging/providers/logs_provider.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
test(
|
||||||
|
'Should parse log line',
|
||||||
|
() async {
|
||||||
|
final formatter = DefaultLogRecordFormatter();
|
||||||
|
final logRecord = LogRecord(
|
||||||
|
Level.INFO,
|
||||||
|
'getting location for name: "logs"',
|
||||||
|
'GoRouter',
|
||||||
|
);
|
||||||
|
final expected = parseLogLine(
|
||||||
|
formatter.format(logRecord),
|
||||||
|
);
|
||||||
|
expect(logRecord.message, expected.message);
|
||||||
|
expect(logRecord.level, expected.level);
|
||||||
|
expect(logRecord.loggerName, expected.loggerName);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
|
|
||||||
#include <isar_flutter_libs/isar_flutter_libs_plugin.h>
|
#include <isar_flutter_libs/isar_flutter_libs_plugin.h>
|
||||||
#include <media_kit_libs_windows_audio/media_kit_libs_windows_audio_plugin_c_api.h>
|
#include <media_kit_libs_windows_audio/media_kit_libs_windows_audio_plugin_c_api.h>
|
||||||
|
#include <share_plus/share_plus_windows_plugin_c_api.h>
|
||||||
#include <url_launcher_windows/url_launcher_windows.h>
|
#include <url_launcher_windows/url_launcher_windows.h>
|
||||||
|
|
||||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||||
|
|
@ -15,6 +16,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||||
registry->GetRegistrarForPlugin("IsarFlutterLibsPlugin"));
|
registry->GetRegistrarForPlugin("IsarFlutterLibsPlugin"));
|
||||||
MediaKitLibsWindowsAudioPluginCApiRegisterWithRegistrar(
|
MediaKitLibsWindowsAudioPluginCApiRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("MediaKitLibsWindowsAudioPluginCApi"));
|
registry->GetRegistrarForPlugin("MediaKitLibsWindowsAudioPluginCApi"));
|
||||||
|
SharePlusWindowsPluginCApiRegisterWithRegistrar(
|
||||||
|
registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi"));
|
||||||
UrlLauncherWindowsRegisterWithRegistrar(
|
UrlLauncherWindowsRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
|
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
list(APPEND FLUTTER_PLUGIN_LIST
|
list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
isar_flutter_libs
|
isar_flutter_libs
|
||||||
media_kit_libs_windows_audio
|
media_kit_libs_windows_audio
|
||||||
|
share_plus
|
||||||
url_launcher_windows
|
url_launcher_windows
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue