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:
Dr.Blank 2024-10-03 05:54:29 -04:00 committed by GitHub
parent 7b0c2c4b88
commit 35a2d7cfce
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
44 changed files with 861 additions and 176 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

View file

@ -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

View file

@ -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
/// ///

View file

@ -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}) {

View file

@ -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
/// ///

View file

@ -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();

View file

@ -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
/// ///

View file

@ -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();
} }

View file

@ -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',
); );

View file

@ -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();
} }

View file

@ -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)

View file

@ -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;
} }

View file

@ -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');
} }
}); });
} }

View 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}',
);
});
}
}

View 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);
}

View 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

View 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;
}
}

View file

@ -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(

View file

@ -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',
); );

View file

@ -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;

View file

@ -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();
} }
} }

View file

@ -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)

View file

@ -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;
} }
} }

View file

@ -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)

View file

@ -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,

View file

@ -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');
} }
} }

View file

@ -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'),

View file

@ -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'),
);
}
}

View file

@ -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(

View file

@ -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;
} }

View file

@ -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,

View file

@ -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,

View file

@ -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

View file

@ -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));

View file

@ -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) {

View file

@ -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}) {

View file

@ -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)

View file

@ -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(' ', '_');
} }

View 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(),
);
}
}

View file

@ -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:

View file

@ -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

View 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);
},
);
}

View file

@ -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"));
} }

View file

@ -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
) )