From 35a2d7cfce0e9b604f411ef3694026a433cd9453 Mon Sep 17 00:00:00 2001 From: "Dr.Blank" Date: Thu, 3 Oct 2024 05:54:29 -0400 Subject: [PATCH] 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 --- assets/images/vaani_logo_foreground.png | Bin 0 -> 3869 bytes lib/api/api_provider.dart | 4 +- lib/api/api_provider.g.dart | 2 +- lib/api/authenticated_user_provider.dart | 10 +- lib/api/authenticated_user_provider.g.dart | 2 +- lib/api/server_provider.dart | 18 +- lib/api/server_provider.g.dart | 2 +- lib/db/init.dart | 12 +- .../downloads/core/download_manager.dart | 5 +- .../downloads/providers/download_manager.dart | 4 + .../providers/download_manager.g.dart | 4 +- .../view/library_item_actions.dart | 10 +- .../view/library_item_hero_section.dart | 2 +- lib/features/logging/core/logger.dart | 36 +++ .../logging/providers/logs_provider.dart | 76 +++++ .../logging/providers/logs_provider.g.dart | 25 ++ lib/features/logging/view/logs_page.dart | 266 ++++++++++++++++++ .../view/user_login_with_open_id.dart | 5 +- .../core/playback_reporter.dart | 6 +- .../player/core/audiobook_player.dart | 45 +-- .../player/providers/audiobook_player.dart | 27 +- .../player/providers/audiobook_player.g.dart | 4 +- .../providers/currently_playing_provider.dart | 4 + .../currently_playing_provider.g.dart | 2 +- .../widgets/chapter_selection_button.dart | 3 +- .../sleep_timer/core/sleep_timer.dart | 8 +- lib/features/you/view/server_manager.dart | 6 +- lib/features/you/view/you_page.dart | 178 +++++++----- lib/main.dart | 11 +- lib/models/error_response.dart | 3 +- lib/pages/home_page.dart | 3 +- lib/pages/library_page.dart | 3 +- lib/router/constants.dart | 6 + lib/router/router.dart | 12 +- lib/router/scaffold_with_nav_bar.dart | 18 +- lib/settings/api_settings_provider.dart | 6 +- lib/settings/api_settings_provider.g.dart | 2 +- lib/settings/constants.dart | 5 + lib/shared/extensions/obfuscation.dart | 125 ++++++++ pubspec.lock | 44 ++- pubspec.yaml | 5 + .../logging/providers/logs_provider_test.dart | 24 ++ .../flutter/generated_plugin_registrant.cc | 3 + windows/flutter/generated_plugins.cmake | 1 + 44 files changed, 861 insertions(+), 176 deletions(-) create mode 100644 assets/images/vaani_logo_foreground.png create mode 100644 lib/features/logging/core/logger.dart create mode 100644 lib/features/logging/providers/logs_provider.dart create mode 100644 lib/features/logging/providers/logs_provider.g.dart create mode 100644 lib/features/logging/view/logs_page.dart create mode 100644 lib/shared/extensions/obfuscation.dart create mode 100644 test/features/logging/providers/logs_provider_test.dart diff --git a/assets/images/vaani_logo_foreground.png b/assets/images/vaani_logo_foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..cb8014fdd92400fcd32a8791e6d07d7bbdd4d448 GIT binary patch literal 3869 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 literal 0 HcmV?d00001 diff --git a/lib/api/api_provider.dart b/lib/api/api_provider.dart index daffff8..1d3745f 100644 --- a/lib/api/api_provider.dart +++ b/lib/api/api_provider.dart @@ -8,6 +8,7 @@ import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:shelfsdk/audiobookshelf_api.dart'; import 'package:vaani/db/cache_manager.dart'; import 'package:vaani/settings/api_settings_provider.dart'; +import 'package:vaani/shared/extensions/obfuscation.dart'; part 'api_provider.g.dart'; @@ -80,7 +81,7 @@ FutureOr serverStatus( Uri baseUrl, [ ResponseErrorHandler? responseErrorHandler, ]) async { - _logger.fine('fetching server status: $baseUrl'); + _logger.fine('fetching server status: ${baseUrl.obfuscate()}'); final api = ref.watch(audiobookshelfApiProvider(baseUrl)); final res = await api.server.status(responseErrorHandler: responseErrorHandler); @@ -145,7 +146,6 @@ class PersonalizedView extends _$PersonalizedView { _logger.warning('failed to fetch personalized view'); yield []; } - } // method to force refresh the view and ignore the cache diff --git a/lib/api/api_provider.g.dart b/lib/api/api_provider.g.dart index 67e86e2..7758c7b 100644 --- a/lib/api/api_provider.g.dart +++ b/lib/api/api_provider.g.dart @@ -327,7 +327,7 @@ class _IsServerAliveProviderElement String get address => (origin as IsServerAliveProvider).address; } -String _$serverStatusHash() => r'2739906a1862d09b098588ebd16749a09032ee99'; +String _$serverStatusHash() => r'd7079e19e68f5f61b0afa0f73a2af8807c4b3cf6'; /// fetch status of server /// diff --git a/lib/api/authenticated_user_provider.dart b/lib/api/authenticated_user_provider.dart index c10f799..c4f065c 100644 --- a/lib/api/authenticated_user_provider.dart +++ b/lib/api/authenticated_user_provider.dart @@ -5,8 +5,8 @@ import 'package:vaani/api/server_provider.dart' import 'package:vaani/db/storage.dart'; import 'package:vaani/settings/api_settings_provider.dart'; import 'package:vaani/settings/models/audiobookshelf_server.dart'; -import 'package:vaani/settings/models/authenticated_user.dart' - as model; +import 'package:vaani/settings/models/authenticated_user.dart' as model; +import 'package:vaani/shared/extensions/obfuscation.dart'; part 'authenticated_user_provider.g.dart'; @@ -35,7 +35,9 @@ class AuthenticatedUser extends _$AuthenticatedUser { Set readFromBoxOrCreate() { if (_box.isNotEmpty) { 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(); } else { _logger.fine('no settings found in box'); @@ -49,7 +51,7 @@ class AuthenticatedUser extends _$AuthenticatedUser { return; } _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}) { diff --git a/lib/api/authenticated_user_provider.g.dart b/lib/api/authenticated_user_provider.g.dart index 7fca094..65f7c32 100644 --- a/lib/api/authenticated_user_provider.g.dart +++ b/lib/api/authenticated_user_provider.g.dart @@ -6,7 +6,7 @@ part of 'authenticated_user_provider.dart'; // RiverpodGenerator // ************************************************************************** -String _$authenticatedUserHash() => r'308f19b33ae04af6340fb83167fa64aa23400a09'; +String _$authenticatedUserHash() => r'1983527595207c63a12bc84cf0bf1a3c1d729506'; /// provides with a set of authenticated users /// diff --git a/lib/api/server_provider.dart b/lib/api/server_provider.dart index 4ad4b1e..f4d8a21 100644 --- a/lib/api/server_provider.dart +++ b/lib/api/server_provider.dart @@ -1,16 +1,18 @@ import 'package:collection/collection.dart'; -import 'package:flutter/material.dart'; +import 'package:logging/logging.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:vaani/api/authenticated_user_provider.dart'; import 'package:vaani/db/storage.dart'; import 'package:vaani/settings/api_settings_provider.dart'; -import 'package:vaani/settings/models/audiobookshelf_server.dart' - as model; +import 'package:vaani/settings/models/audiobookshelf_server.dart' as model; +import 'package:vaani/shared/extensions/obfuscation.dart'; part 'server_provider.g.dart'; final _box = AvailableHiveBoxes.serverBox; +final _logger = Logger('AudiobookShelfServerProvider'); + class ServerAlreadyExistsException implements Exception { final model.AudiobookShelfServer server; @@ -47,10 +49,10 @@ class AudiobookShelfServer extends _$AudiobookShelfServer { Set readFromBoxOrCreate() { if (_box.isNotEmpty) { 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(); } else { - debugPrint('no settings found in box'); + _logger.info('no settings found in box'); return {}; } } @@ -61,7 +63,7 @@ class AudiobookShelfServer extends _$AudiobookShelfServer { return; } _box.addAll(state); - debugPrint('writing state to box: $state'); + _logger.info('writing state to box: ${state.obfuscate()}'); } void addServer(model.AudiobookShelfServer server) { @@ -71,8 +73,8 @@ class AudiobookShelfServer extends _$AudiobookShelfServer { state = {...state, server}; } - void removeServer(model.AudiobookShelfServer server, - { + void removeServer( + model.AudiobookShelfServer server, { bool removeUsers = false, }) { state = state.where((s) => s != server).toSet(); diff --git a/lib/api/server_provider.g.dart b/lib/api/server_provider.g.dart index 69478cc..7ff40c2 100644 --- a/lib/api/server_provider.g.dart +++ b/lib/api/server_provider.g.dart @@ -7,7 +7,7 @@ part of 'server_provider.dart'; // ************************************************************************** String _$audiobookShelfServerHash() => - r'f0d645bb42233c59886bc43fdc473897484ceca1'; + r'0084fb72c4c54323207928b95716cfd9ca496c11'; /// provides with a set of servers added by the user /// diff --git a/lib/db/init.dart b/lib/db/init.dart index ad875db..8b5e7d0 100644 --- a/lib/db/init.dart +++ b/lib/db/init.dart @@ -1,28 +1,28 @@ -// does the initial setup of the storage - import 'dart:io'; -import 'package:flutter/material.dart'; import 'package:hive/hive.dart'; import 'package:path/path.dart' as p; import 'package:path_provider/path_provider.dart'; +import 'package:vaani/main.dart'; import 'package:vaani/settings/constants.dart'; import 'register_models.dart'; +// does the initial setup of the storage Future initStorage() async { final dir = await getApplicationDocumentsDirectory(); // use vaani as the directory for hive - final storageDir = Directory(p.join( - dir.path, + final storageDir = Directory( + p.join( + dir.path, AppMetadata.appNameLowerCase, ), ); await storageDir.create(recursive: true); Hive.defaultDirectory = storageDir.path; - debugPrint('Hive storage directory init: ${Hive.defaultDirectory}'); + appLogger.config('Hive storage directory init: ${Hive.defaultDirectory}'); await registerModels(); } diff --git a/lib/features/downloads/core/download_manager.dart b/lib/features/downloads/core/download_manager.dart index 5b6229d..0e808b1 100644 --- a/lib/features/downloads/core/download_manager.dart +++ b/lib/features/downloads/core/download_manager.dart @@ -8,6 +8,7 @@ import 'package:logging/logging.dart'; import 'package:path_provider/path_provider.dart'; import 'package:shelfsdk/audiobookshelf_api.dart'; import 'package:vaani/shared/extensions/model_conversions.dart'; +import 'package:vaani/shared/extensions/obfuscation.dart'; final _logger = Logger('AudiobookDownloadManager'); final tq = MemoryTaskQueue(); @@ -35,7 +36,9 @@ class AudiobookDownloadManager { 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( 'requiresWiFi: $requiresWiFi, retries: $retries, allowPause: $allowPause', ); diff --git a/lib/features/downloads/providers/download_manager.dart b/lib/features/downloads/providers/download_manager.dart index 87a2191..9f56129 100644 --- a/lib/features/downloads/providers/download_manager.dart +++ b/lib/features/downloads/providers/download_manager.dart @@ -31,9 +31,11 @@ class SimpleDownloadManager extends _$SimpleDownloadManager { core.tq.maxConcurrentByGroup = downloadSettings.maxConcurrentByGroup; ref.onDispose(() { + _logger.info('disposing download manager'); manager.dispose(); }); + _logger.config('initialized download manager'); return manager; } } @@ -52,12 +54,14 @@ class DownloadManager extends _$DownloadManager { Future queueAudioBookDownload( LibraryItemExpanded item, ) async { + _logger.fine('queueing download for ${item.id}'); await state.queueAudioBookDownload( item, ); } Future deleteDownloadedItem(LibraryItemExpanded item) async { + _logger.fine('deleting downloaded item ${item.id}'); await state.deleteDownloadedItem(item); ref.notifyListeners(); } diff --git a/lib/features/downloads/providers/download_manager.g.dart b/lib/features/downloads/providers/download_manager.g.dart index 43fc01c..47cbf4d 100644 --- a/lib/features/downloads/providers/download_manager.g.dart +++ b/lib/features/downloads/providers/download_manager.g.dart @@ -158,7 +158,7 @@ class _DownloadHistoryProviderElement } String _$simpleDownloadManagerHash() => - r'cec95717c86e422f88f78aa014d29e800e5a2089'; + r'8ab13f06ec5f2f73b73064bd285813dc890b7f36'; /// See also [SimpleDownloadManager]. @ProviderFor(SimpleDownloadManager) @@ -174,7 +174,7 @@ final simpleDownloadManagerProvider = NotifierProvider; -String _$downloadManagerHash() => r'7296a39439230f77abbe7d3231dae748f09c7ecf'; +String _$downloadManagerHash() => r'852012e32e613f86445afc7f7e4e85bec808e982'; /// See also [DownloadManager]. @ProviderFor(DownloadManager) diff --git a/lib/features/item_viewer/view/library_item_actions.dart b/lib/features/item_viewer/view/library_item_actions.dart index f1eec3d..1b1b5a8 100644 --- a/lib/features/item_viewer/view/library_item_actions.dart +++ b/lib/features/item_viewer/view/library_item_actions.dart @@ -518,7 +518,7 @@ Future libraryItemPlayButtonOnPressed({ required shelfsdk.BookExpanded book, shelfsdk.MediaProgress? userMediaProgress, }) async { - debugPrint('Pressed play/resume button'); + appLogger.info('Pressed play/resume button'); final player = ref.watch(audiobookPlayerProvider); final isCurrentBookSetInPlayer = player.book == book; @@ -527,8 +527,8 @@ Future libraryItemPlayButtonOnPressed({ Future? setSourceFuture; // set the book to the player if not already set if (!isCurrentBookSetInPlayer) { - debugPrint('Setting the book ${book.libraryItemId}'); - debugPrint('Initial position: ${userMediaProgress?.currentTime}'); + appLogger.info('Setting the book ${book.libraryItemId}'); + appLogger.info('Initial position: ${userMediaProgress?.currentTime}'); final downloadManager = ref.watch(simpleDownloadManagerProvider); final libItem = await ref.read(libraryItemProvider(book.libraryItemId).future); @@ -539,9 +539,9 @@ Future libraryItemPlayButtonOnPressed({ downloadedUris: downloadedUris, ); } else { - debugPrint('Book was already set'); + appLogger.info('Book was already set'); if (isPlayingThisBook) { - debugPrint('Pausing the book'); + appLogger.info('Pausing the book'); await player.pause(); return; } diff --git a/lib/features/item_viewer/view/library_item_hero_section.dart b/lib/features/item_viewer/view/library_item_hero_section.dart index ea21da6..83b5555 100644 --- a/lib/features/item_viewer/view/library_item_hero_section.dart +++ b/lib/features/item_viewer/view/library_item_hero_section.dart @@ -383,7 +383,7 @@ class _BookCover extends HookConsumerWidget { : themeData, ); } catch (e) { - appLogger.shout('Error changing theme: $e'); + appLogger.severe('Error changing theme: $e'); } }); } diff --git a/lib/features/logging/core/logger.dart b/lib/features/logging/core/logger.dart new file mode 100644 index 0000000..52eb099 --- /dev/null +++ b/lib/features/logging/core/logger.dart @@ -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 getLoggingFilePath() async { + final Directory directory = await getApplicationDocumentsDirectory(); + return '${directory.path}/vaani.log'; +} + +Future 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}', + ); + }); + } +} diff --git a/lib/features/logging/providers/logs_provider.dart b/lib/features/logging/providers/logs_provider.dart new file mode 100644 index 0000000..ecdc0b6 --- /dev/null +++ b/lib/features/logging/providers/logs_provider.dart @@ -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> 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 clear() async { + final path = await getLoggingFilePath(); + final file = File(path); + await file.writeAsString(''); + state = AsyncData([]); + } + + Future getZipFilePath() async { + var encoder = ZipFileEncoder(); + encoder.create(await generateZipFilePath()); + encoder.addFile(File(await getLoggingFilePath())); + encoder.close(); + return encoder.zipPath; + } +} + +Future 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); +} diff --git a/lib/features/logging/providers/logs_provider.g.dart b/lib/features/logging/providers/logs_provider.g.dart new file mode 100644 index 0000000..094893e --- /dev/null +++ b/lib/features/logging/providers/logs_provider.g.dart @@ -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>.internal( + Logs.new, + name: r'logsProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') ? null : _$logsHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef _$Logs = AutoDisposeAsyncNotifier>; +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/lib/features/logging/view/logs_page.dart b/lib/features/logging/view/logs_page.dart new file mode 100644 index 0000000..f5ee104 --- /dev/null +++ b/lib/features/logging/view/logs_page.dart @@ -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( + 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; + } +} diff --git a/lib/features/onboarding/view/user_login_with_open_id.dart b/lib/features/onboarding/view/user_login_with_open_id.dart index e403c81..71baa42 100644 --- a/lib/features/onboarding/view/user_login_with_open_id.dart +++ b/lib/features/onboarding/view/user_login_with_open_id.dart @@ -11,6 +11,7 @@ import 'package:vaani/models/error_response.dart'; import 'package:vaani/router/router.dart'; import 'package:vaani/settings/constants.dart'; import 'package:vaani/settings/models/models.dart' as model; +import 'package:vaani/shared/extensions/obfuscation.dart'; import 'package:vaani/shared/utils.dart'; class UserLoginWithOpenID extends HookConsumerWidget { @@ -89,7 +90,9 @@ class UserLoginWithOpenID extends HookConsumerWidget { return; } - appLogger.fine('Got OpenID login endpoint: $openIDLoginEndpoint'); + appLogger.fine( + 'Got OpenID login endpoint: ${openIDLoginEndpoint.obfuscate()}', + ); // add the flow to the provider ref.read(oauthFlowsProvider.notifier).addFlow( diff --git a/lib/features/playback_reporting/core/playback_reporter.dart b/lib/features/playback_reporting/core/playback_reporter.dart index faffb4d..b2be336 100644 --- a/lib/features/playback_reporting/core/playback_reporter.dart +++ b/lib/features/playback_reporting/core/playback_reporter.dart @@ -1,8 +1,10 @@ import 'dart:async'; +import 'package:http/http.dart' as http; import 'package:logging/logging.dart'; import 'package:shelfsdk/audiobookshelf_api.dart'; import 'package:vaani/features/player/core/audiobook_player.dart'; +import 'package:vaani/shared/extensions/obfuscation.dart'; final _logger = Logger('PlaybackReporter'); @@ -255,9 +257,9 @@ class PlaybackReporter { _logger.fine('cancelled timer'); } - void _responseErrorHandler(response, [error]) { + void _responseErrorHandler(http.Response response, [error]) { if (response.statusCode != 200) { - _logger.shout('Error with api: $response, $error'); + _logger.severe('Error with api: ${response.obfuscate()}, $error'); throw PlaybackSyncError( 'Error syncing position: ${response.body}, $error', ); diff --git a/lib/features/player/core/audiobook_player.dart b/lib/features/player/core/audiobook_player.dart index 726e91d..87247fe 100644 --- a/lib/features/player/core/audiobook_player.dart +++ b/lib/features/player/core/audiobook_player.dart @@ -11,38 +11,38 @@ import 'package:shelfsdk/audiobookshelf_api.dart'; import 'package:vaani/settings/app_settings_provider.dart'; import 'package:vaani/settings/models/app_settings.dart'; import 'package:vaani/shared/extensions/model_conversions.dart'; +import 'package:vaani/shared/extensions/obfuscation.dart'; final _logger = Logger('AudiobookPlayer'); /// returns the sum of the duration of all the previous tracks before the [index] Duration sumOfTracks(BookExpanded book, int? index) { + _logger.fine('Calculating sum of tracks for index: $index'); // return 0 if index is less than 0 if (index == null || index < 0) { + _logger.warning('Index is null or less than 0, returning 0'); return Duration.zero; } - return book.tracks.sublist(0, index).fold(Duration.zero, - (previousValue, element) { - return previousValue + element.duration; - }); + final total = book.tracks.sublist(0, index).fold( + Duration.zero, + (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] AudioTrack getTrackToPlay(BookExpanded book, Duration position) { - // var totalDuration = Duration.zero; - // for (var track in book.tracks) { - // totalDuration += track.duration; - // if (totalDuration >= position) { - // return track; - // } - // } - // return book.tracks.last; - return book.tracks.firstWhere( + _logger.fine('Getting track to play for position: $position'); + final track = book.tracks.firstWhere( (element) { return element.startOffset <= position && (element.startOffset + element.duration) >= position; }, orElse: () => book.tracks.last, ); + _logger.fine('Track to play for position: $position is $track'); + return track; } /// will manage the audio player instance @@ -50,6 +50,7 @@ class AudiobookPlayer extends AudioPlayer { // constructor which takes in the BookExpanded object AudiobookPlayer(this.token, this.baseUrl) : super() { // set the source of the player to the first track in the book + _logger.config('Setting up audiobook player'); } /// the [BookExpanded] being played @@ -84,20 +85,23 @@ class AudiobookPlayer extends AudioPlayer { List? downloadedUris, Uri? artworkUri, }) async { + _logger.finer( + 'Initial position: $initialPosition, Downloaded URIs: $downloadedUris', + ); final appSettings = loadOrCreateAppSettings(); - // if the book is null, stop the player if (book == null) { _book = null; _logger.info('Book is null, stopping player'); return stop(); } - // see if the book is the same as the current book if (_book == book) { _logger.info('Book is the same, doing nothing'); return; } - // first stop the player and clear the source + _logger.info('Setting source for book: $book'); + + _logger.fine('Stopping player'); await stop(); _book = book; @@ -114,6 +118,7 @@ class AudiobookPlayer extends AudioPlayer { ? initialPosition - trackToPlay.startOffset : null; + _logger.finer('Setting audioSource'); await setAudioSource( preload: preload, initialIndex: initialIndex, @@ -124,7 +129,7 @@ class AudiobookPlayer extends AudioPlayer { final retrievedUri = _getUri(track, downloadedUris, baseUrl: baseUrl, token: token); _logger.fine( - 'Setting source for track: ${track.title}, URI: $retrievedUri', + 'Setting source for track: ${track.title}, URI: ${retrievedUri.obfuscate()}', ); return AudioSource.uri( retrievedUri, @@ -145,7 +150,7 @@ class AudiobookPlayer extends AudioPlayer { }).toList(), ), ).catchError((error) { - _logger.shout('Error: $error'); + _logger.shout('Error in setting audio source: $error'); }); } @@ -153,7 +158,7 @@ class AudiobookPlayer extends AudioPlayer { Future togglePlayPause() { // check if book is set 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 @@ -169,9 +174,11 @@ class AudiobookPlayer extends AudioPlayer { @override Future seek(Duration? positionInBook, {int? index}) async { if (_book == null) { + _logger.warning('No book is set, not seeking'); return; } if (positionInBook == null) { + _logger.warning('Position given is null, not seeking'); return; } final tracks = _book!.tracks; diff --git a/lib/features/player/providers/audiobook_player.dart b/lib/features/player/providers/audiobook_player.dart index 947ca57..91b6483 100644 --- a/lib/features/player/providers/audiobook_player.dart +++ b/lib/features/player/providers/audiobook_player.dart @@ -1,21 +1,11 @@ +import 'package:logging/logging.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:vaani/api/api_provider.dart'; -import 'package:vaani/features/player/core/audiobook_player.dart' - as core; +import 'package:vaani/features/player/core/audiobook_player.dart' as core; part 'audiobook_player.g.dart'; -// @Riverpod(keepAlive: true) -// core.AudiobookPlayer audiobookPlayer( -// AudiobookPlayerRef ref, -// ) { -// final api = ref.watch(authenticatedApiProvider); -// final player = core.AudiobookPlayer(api.token!, api.baseUrl); - -// ref.onDispose(player.dispose); - -// return player; -// } +final _logger = Logger('AudiobookPlayerProvider'); const playerId = 'audiobook_player'; @@ -32,6 +22,7 @@ class SimpleAudiobookPlayer extends _$SimpleAudiobookPlayer { ); ref.onDispose(player.dispose); + _logger.finer('created simple player'); return player; } @@ -47,18 +38,16 @@ class AudiobookPlayer extends _$AudiobookPlayer { // bind notify listeners to the player player.playerStateStream.listen((_) { - notifyListeners(); + ref.notifyListeners(); }); - return player; - } + _logger.finer('created player'); - void notifyListeners() { - ref.notifyListeners(); + return player; } Future setSpeed(double speed) async { await state.setSpeed(speed); - notifyListeners(); + ref.notifyListeners(); } } diff --git a/lib/features/player/providers/audiobook_player.g.dart b/lib/features/player/providers/audiobook_player.g.dart index 849cde6..a1068eb 100644 --- a/lib/features/player/providers/audiobook_player.g.dart +++ b/lib/features/player/providers/audiobook_player.g.dart @@ -7,7 +7,7 @@ part of 'audiobook_player.dart'; // ************************************************************************** String _$simpleAudiobookPlayerHash() => - r'9e11ed2791d35e308f8cbe61a79a45cf51466ebb'; + r'5e94bbff4314adceb5affa704fc4d079d4016afa'; /// Simple because it doesn't rebuild when the player state changes /// it only rebuilds when the token changes @@ -26,7 +26,7 @@ final simpleAudiobookPlayerProvider = ); typedef _$SimpleAudiobookPlayer = Notifier; -String _$audiobookPlayerHash() => r'44394b1dbbf85eb19ef1f693717e8cbc15b768e5'; +String _$audiobookPlayerHash() => r'0f180308067486896fec6a65a6afb0e6686ac4a0'; /// See also [AudiobookPlayer]. @ProviderFor(AudiobookPlayer) diff --git a/lib/features/player/providers/currently_playing_provider.dart b/lib/features/player/providers/currently_playing_provider.dart index 67e0a35..3ceff3e 100644 --- a/lib/features/player/providers/currently_playing_provider.dart +++ b/lib/features/player/providers/currently_playing_provider.dart @@ -1,3 +1,4 @@ +import 'package:logging/logging.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:shelfsdk/audiobookshelf_api.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'; +final _logger = Logger('CurrentlyPlayingProvider'); + @riverpod BookExpanded? currentlyPlayingBook(CurrentlyPlayingBookRef ref) { try { final player = ref.watch(audiobookPlayerProvider); return player.book; } catch (e) { + _logger.warning('Error getting currently playing book: $e'); return null; } } diff --git a/lib/features/player/providers/currently_playing_provider.g.dart b/lib/features/player/providers/currently_playing_provider.g.dart index fb22028..6dc1c2a 100644 --- a/lib/features/player/providers/currently_playing_provider.g.dart +++ b/lib/features/player/providers/currently_playing_provider.g.dart @@ -7,7 +7,7 @@ part of 'currently_playing_provider.dart'; // ************************************************************************** String _$currentlyPlayingBookHash() => - r'52334c7b4d68fd498a2a00208d8d7f1ba0085237'; + r'7440b0d54cb364f66e704783652e8f1490ae90e0'; /// See also [currentlyPlayingBook]. @ProviderFor(currentlyPlayingBook) diff --git a/lib/features/player/view/widgets/chapter_selection_button.dart b/lib/features/player/view/widgets/chapter_selection_button.dart index d4da604..889392a 100644 --- a/lib/features/player/view/widgets/chapter_selection_button.dart +++ b/lib/features/player/view/widgets/chapter_selection_button.dart @@ -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/currently_playing_provider.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/duration_format.dart'; import 'package:vaani/shared/hooks.dart'; @@ -55,7 +56,7 @@ class ChapterSelectionModal extends HookConsumerWidget { final currentChapterIndex = currentChapter?.id; final chapterKey = GlobalKey(); scrollToCurrentChapter() async { - debugPrint('scrolling to chapter'); + appLogger.fine('scrolling to chapter'); await Scrollable.ensureVisible( chapterKey.currentContext!, duration: 200.ms, diff --git a/lib/features/sleep_timer/core/sleep_timer.dart b/lib/features/sleep_timer/core/sleep_timer.dart index 9350436..2d9ea31 100644 --- a/lib/features/sleep_timer/core/sleep_timer.dart +++ b/lib/features/sleep_timer/core/sleep_timer.dart @@ -69,7 +69,7 @@ class SleepTimer { }), ); - _logger.fine('created with duration: $duration'); + _logger.info('created with duration: $duration'); } /// resets the timer and stops it @@ -90,7 +90,7 @@ class SleepTimer { if (player.playing) { startCountDown(); } - _logger.fine('restarted timer'); + _logger.info('restarted timer'); } /// starts the timer with the given duration or the default duration @@ -105,7 +105,7 @@ class SleepTimer { _logger.fine('paused player after $duration'); }); startedAt = DateTime.now(); - _logger.fine('started for $duration at $startedAt'); + _logger.info('started for $duration at $startedAt'); } Duration get remainingTime { @@ -130,6 +130,6 @@ class SleepTimer { for (var sub in _subscriptions) { sub.cancel(); } - _logger.fine('disposed'); + _logger.info('disposed'); } } diff --git a/lib/features/you/view/server_manager.dart b/lib/features/you/view/server_manager.dart index 2ab70ea..8a385f2 100644 --- a/lib/features/you/view/server_manager.dart +++ b/lib/features/you/view/server_manager.dart @@ -5,10 +5,12 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:vaani/api/api_provider.dart'; import 'package:vaani/api/authenticated_user_provider.dart'; import 'package:vaani/api/server_provider.dart'; +import 'package:vaani/main.dart'; import 'package:vaani/models/error_response.dart'; import 'package:vaani/router/router.dart'; import 'package:vaani/settings/api_settings_provider.dart'; 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'; class ServerManagerPage extends HookConsumerWidget { @@ -25,8 +27,8 @@ class ServerManagerPage extends HookConsumerWidget { final serverURIController = useTextEditingController(); final formKey = GlobalKey(); - debugPrint('registered servers: $registeredServers'); - debugPrint('available users: $availableUsers'); + appLogger.fine('registered servers: ${registeredServers.obfuscate()}'); + appLogger.fine('available users: ${availableUsers.obfuscate()}'); return Scaffold( appBar: AppBar( title: const Text('Manage Accounts'), diff --git a/lib/features/you/view/you_page.dart b/lib/features/you/view/you_page.dart index a171a70..2a7bee2 100644 --- a/lib/features/you/view/you_page.dart +++ b/lib/features/you/view/you_page.dart @@ -1,9 +1,9 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:shelfsdk/audiobookshelf_api.dart'; import 'package:vaani/api/api_provider.dart'; import 'package:vaani/router/router.dart'; +import 'package:vaani/settings/constants.dart'; import 'package:vaani/shared/utils.dart'; import 'package:vaani/shared/widgets/not_implemented.dart'; @@ -12,27 +12,6 @@ class YouPage extends HookConsumerWidget { 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 Widget build(BuildContext context, WidgetRef ref) { final api = ref.watch(authenticatedApiProvider); @@ -41,14 +20,21 @@ class _YouPage extends HookConsumerWidget { // title: const Text('You'), backgroundColor: Colors.transparent, actions: [ + IconButton( + tooltip: 'Logs', + icon: const Icon(Icons.bug_report), + onPressed: () { + context.pushNamed(Routes.logs.name); + }, + ), // IconButton( // icon: const Icon(Icons.edit), // onPressed: () { // // Handle edit profile // }, // ), - // settings button IconButton( + tooltip: 'Settings', icon: const Icon(Icons.settings), onPressed: () { context.pushNamed(Routes.settings.name); @@ -64,30 +50,7 @@ class _YouPage extends HookConsumerWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - 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, - ), - ), - ], - ), + UserBar(), const SizedBox(height: 16), Wrap( spacing: 8, @@ -121,21 +84,6 @@ class _YouPage extends HookConsumerWidget { title: const Text('My Playlists'), onTap: () { // 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); }, ), @@ -149,10 +97,40 @@ class _YouPage extends HookConsumerWidget { ); }, ), - // const SizedBox(height: 16), - // const Text('App Version: 1.0.0'), - // const Text('Server Version: 1.0.0'), - // const Text('Author: Your Name'), + ListTile( + leading: const Icon(Icons.help), + title: const Text('Help'), + 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'), + ); + } +} diff --git a/lib/main.dart b/lib/main.dart index 8eef350..7f54ec1 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -4,6 +4,7 @@ import 'package:logging/logging.dart'; import 'package:vaani/api/server_provider.dart'; import 'package:vaani/db/storage.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/player/core/init.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/settings/api_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'; final appLogger = Logger('vaani'); @@ -20,13 +20,7 @@ final appLogger = Logger('vaani'); void main() async { WidgetsFlutterBinding.ensureInitialized(); // Configure the root Logger - Logger.root.level = Level.FINE; // 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}', - ); - }); + await initLogging(); // initialize the storage await initStorage(); @@ -34,7 +28,6 @@ void main() async { // initialize audio player await configurePlayer(); - // run the app runApp( const ProviderScope( diff --git a/lib/models/error_response.dart b/lib/models/error_response.dart index 2e4c926..954f3f3 100644 --- a/lib/models/error_response.dart +++ b/lib/models/error_response.dart @@ -1,5 +1,6 @@ import 'package:http/http.dart' as http; import 'package:logging/logging.dart'; +import 'package:vaani/shared/extensions/obfuscation.dart'; final _logger = Logger('ErrorResponse'); @@ -13,7 +14,7 @@ class ErrorResponseHandler { }) : _response = response ?? http.Response('', 418); void storeError(http.Response response, [Object? error]) { - _logger.fine('for $name got response: $response'); + _logger.fine('for $name got response: ${response.obfuscate()}'); _response = response; } diff --git a/lib/pages/home_page.dart b/lib/pages/home_page.dart index 5b3ab41..4d402b3 100644 --- a/lib/pages/home_page.dart +++ b/lib/pages/home_page.dart @@ -3,6 +3,7 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:vaani/api/api_provider.dart'; +import 'package:vaani/main.dart'; import 'package:vaani/router/router.dart'; import 'package:vaani/settings/api_settings_provider.dart'; @@ -59,7 +60,7 @@ class HomePage extends HookConsumerWidget { final shelvesToDisplay = data // .where((element) => !element.id.contains('discover')) .map((shelf) { - debugPrint('building shelf ${shelf.label}'); + appLogger.fine('building shelf ${shelf.label}'); return HomeShelf( title: shelf.label, shelf: shelf, diff --git a/lib/pages/library_page.dart b/lib/pages/library_page.dart index fb97221..18eedee 100644 --- a/lib/pages/library_page.dart +++ b/lib/pages/library_page.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:vaani/api/api_provider.dart'; +import 'package:vaani/main.dart'; import 'package:vaani/settings/api_settings_provider.dart'; import '../shared/widgets/drawer.dart'; @@ -47,7 +48,7 @@ class LibraryPage extends HookConsumerWidget { final shelvesToDisplay = data // .where((element) => !element.id.contains('discover')) .map((shelf) { - debugPrint('building shelf ${shelf.label}'); + appLogger.fine('building shelf ${shelf.label}'); return HomeShelf( title: shelf.label, shelf: shelf, diff --git a/lib/router/constants.dart b/lib/router/constants.dart index 4d8e7c1..9d01c29 100644 --- a/lib/router/constants.dart +++ b/lib/router/constants.dart @@ -90,6 +90,12 @@ class Routes { name: 'openIDCallback', parentRoute: onboarding, ); + + // logs page + static const logs = _SimpleRoute( + pathName: 'logs', + name: 'logs', + ); } // a class to store path diff --git a/lib/router/router.dart b/lib/router/router.dart index 8910fc3..c5b1a67 100644 --- a/lib/router/router.dart +++ b/lib/router/router.dart @@ -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/item_viewer/view/library_item_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/onboarding_single_page.dart'; import 'package:vaani/features/you/view/server_manager.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/settings/view/app_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 final code = state.uri.queryParameters['code']; 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 = CallbackPage(code: code, state: stateParam, key: ValueKey(stateParam)); diff --git a/lib/router/scaffold_with_nav_bar.dart b/lib/router/scaffold_with_nav_bar.dart index 467ec71..0a89162 100644 --- a/lib/router/scaffold_with_nav_bar.dart +++ b/lib/router/scaffold_with_nav_bar.dart @@ -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/view/audiobook_player.dart'; import 'package:vaani/features/player/view/player_when_expanded.dart'; +import 'package:vaani/main.dart'; import 'package:vaani/router/router.dart'; // stack to track changes in navigationShell.currentIndex @@ -42,13 +43,13 @@ class ScaffoldWithNavBar extends HookConsumerWidget { onBackButtonPressed() async { final isPlayerExpanded = playerProgress != playerMinHeight; - debugPrint( + appLogger.fine( 'BackButtonListener: Back button pressed, isPlayerExpanded: $isPlayerExpanded, stack: $navigationShellStack, pendingPlayerModals: $pendingPlayerModals', ); // close miniplayer if it is open if (isPlayerExpanded && pendingPlayerModals == 0) { - debugPrint( + appLogger.fine( 'BackButtonListener: closing the player', ); audioBookMiniplayerController.animateToHeight(state: PanelState.MIN); @@ -59,7 +60,7 @@ class ScaffoldWithNavBar extends HookConsumerWidget { final canPop = GoRouter.of(context).canPop(); if (canPop) { - debugPrint( + appLogger.fine( 'BackButtonListener: passing it to the router as canPop is true', ); return false; @@ -69,7 +70,7 @@ class ScaffoldWithNavBar extends HookConsumerWidget { // pop the last index from the stack and navigate to it final index = navigationShellStack.last; 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 (navigationShellStack.isNotEmpty) { @@ -79,12 +80,12 @@ class ScaffoldWithNavBar extends HookConsumerWidget { } if (navigationShell.currentIndex != 0) { // 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); return true; } - debugPrint('BackButtonListener: passing it to the router'); + appLogger.fine('BackButtonListener: passing it to the router'); return false; } @@ -149,12 +150,11 @@ class ScaffoldWithNavBar extends HookConsumerWidget { navigationShellStack.remove(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. - // If it is, debugPrint a message to the console. 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 (index == 2) { diff --git a/lib/settings/api_settings_provider.dart b/lib/settings/api_settings_provider.dart index ecd297e..91334f0 100644 --- a/lib/settings/api_settings_provider.dart +++ b/lib/settings/api_settings_provider.dart @@ -4,6 +4,7 @@ import 'package:logging/logging.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:vaani/db/available_boxes.dart'; import 'package:vaani/settings/models/api_settings.dart' as model; +import 'package:vaani/shared/extensions/obfuscation.dart'; part 'api_settings_provider.g.dart'; @@ -19,6 +20,7 @@ class ApiSettings extends _$ApiSettings { ref.listenSelf((_, __) { writeToBox(); }); + return state; } @@ -33,7 +35,7 @@ class ApiSettings extends _$ApiSettings { activeServer: foundSettings.activeUser?.server, ); } - _logger.fine('found api settings in box: $foundSettings'); + _logger.fine('found api settings in box: ${foundSettings.obfuscate()}'); return foundSettings; } else { // create a new settings object @@ -47,7 +49,7 @@ class ApiSettings extends _$ApiSettings { void writeToBox() { _box.clear(); _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}) { diff --git a/lib/settings/api_settings_provider.g.dart b/lib/settings/api_settings_provider.g.dart index 0bfacbf..d9a222c 100644 --- a/lib/settings/api_settings_provider.g.dart +++ b/lib/settings/api_settings_provider.g.dart @@ -6,7 +6,7 @@ part of 'api_settings_provider.dart'; // RiverpodGenerator // ************************************************************************** -String _$apiSettingsHash() => r'26e7e09e7369bac9fbf0589da9fd97d1f15b7926'; +String _$apiSettingsHash() => r'5bc1e16e9d72b77fb10637aabadf08e8947da580'; /// See also [ApiSettings]. @ProviderFor(ApiSettings) diff --git a/lib/settings/constants.dart b/lib/settings/constants.dart index 2429545..9ab7b55 100644 --- a/lib/settings/constants.dart +++ b/lib/settings/constants.dart @@ -10,5 +10,10 @@ class AppMetadata { // for deeplinking 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(' ', '_'); } diff --git a/lib/shared/extensions/obfuscation.dart b/lib/shared/extensions/obfuscation.dart new file mode 100644 index 0000000..c70715a --- /dev/null +++ b/lib/shared/extensions/obfuscation.dart @@ -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 on List { + List 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 on Set { + Set 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(), + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index e675e19..b6de7a9 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -47,7 +47,7 @@ packages: source: hosted version: "2.0.10" archive: - dependency: transitive + dependency: "direct main" description: name: archive sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d @@ -366,6 +366,22 @@ packages: url: "https://pub.dev" source: hosted 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: dependency: "direct main" description: @@ -407,7 +423,7 @@ packages: source: hosted version: "7.0.0" file_picker: - dependency: transitive + dependency: "direct main" description: name: file_picker sha256: "167bb619cdddaa10ef2907609feb8a79c16dfa479d3afaf960f8e223f754bf12" @@ -782,6 +798,14 @@ packages: url: "https://pub.dev" source: hosted 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: dependency: "direct main" description: @@ -1095,6 +1119,22 @@ packages: url: "https://pub.dev" source: hosted 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: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index fc1bfdb..0a4ac94 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -32,6 +32,7 @@ isar_version: &isar_version ^4.0.0-dev.13 # define the version to be used dependencies: animated_list_plus: ^0.5.2 animated_theme_switcher: ^2.0.10 + archive: ^3.6.1 audio_service: ^0.18.15 audio_session: ^0.1.19 audio_video_progress_bar: ^2.0.2 @@ -44,6 +45,7 @@ dependencies: device_info_plus: ^10.1.0 duration_picker: ^1.2.0 easy_stepper: ^0.8.4 + file_picker: ^8.1.2 flutter: sdk: flutter flutter_animate: ^4.5.0 @@ -69,6 +71,7 @@ dependencies: just_audio_media_kit: ^2.0.4 list_wheel_scroll_view_nls: ^0.0.3 logging: ^1.2.0 + logging_appenders: ^1.3.1 lottie: ^3.1.0 material_symbols_icons: ^4.2785.1 media_kit_libs_linux: any @@ -84,6 +87,7 @@ dependencies: riverpod_annotation: ^2.3.5 scroll_loop_auto_scroll: ^0.0.5 sensors_plus: ^6.0.1 + share_plus: ^10.0.2 shelfsdk: path: ./shelfsdk shimmer: ^3.0.0 @@ -114,6 +118,7 @@ flutter: - assets/ - assets/animations/ - assets/sounds/ + - assets/images/ # - images/a_dot_burr.jpeg # - images/a_dot_ham.jpeg # An image asset can refer to one or more resolution-specific "variants", see diff --git a/test/features/logging/providers/logs_provider_test.dart b/test/features/logging/providers/logs_provider_test.dart new file mode 100644 index 0000000..68590b8 --- /dev/null +++ b/test/features/logging/providers/logs_provider_test.dart @@ -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); + }, + ); +} diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 1e47bc9..8d09818 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -8,6 +8,7 @@ #include #include +#include #include void RegisterPlugins(flutter::PluginRegistry* registry) { @@ -15,6 +16,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("IsarFlutterLibsPlugin")); MediaKitLibsWindowsAudioPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("MediaKitLibsWindowsAudioPluginCApi")); + SharePlusWindowsPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi")); UrlLauncherWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("UrlLauncherWindows")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index c41e9ee..51689fc 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -5,6 +5,7 @@ list(APPEND FLUTTER_PLUGIN_LIST isar_flutter_libs media_kit_libs_windows_audio + share_plus url_launcher_windows )