mirror of
https://github.com/Dr-Blank/Vaani.git
synced 2025-12-06 02:59:28 +00:00
feat: error reporting with logs (#45)
* feat: add ability to get logs file from ui * test: add unit test for log line parsing in logs_provider * refactor: update all logs to obfuscate sensitive information * feat: generate dynamic zip file name for logs export * feat: enhance logging in audiobook player and provider for better debugging * refactor: extract user display logic into UserBar widget for offline access of settings and logs * feat: add About section with app metadata and source code link in YouPage
This commit is contained in:
parent
7b0c2c4b88
commit
35a2d7cfce
44 changed files with 861 additions and 176 deletions
BIN
assets/images/vaani_logo_foreground.png
Normal file
BIN
assets/images/vaani_logo_foreground.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.8 KiB |
|
|
@ -8,6 +8,7 @@ import 'package:riverpod_annotation/riverpod_annotation.dart';
|
|||
import 'package:shelfsdk/audiobookshelf_api.dart';
|
||||
import 'package: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<ServerStatusResponse?> 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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
///
|
||||
|
|
|
|||
|
|
@ -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<model.AuthenticatedUser> 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}) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
///
|
||||
|
|
|
|||
|
|
@ -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<model.AudiobookShelfServer> 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();
|
||||
|
|
|
|||
|
|
@ -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
|
||||
///
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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<void> queueAudioBookDownload(
|
||||
LibraryItemExpanded item,
|
||||
) async {
|
||||
_logger.fine('queueing download for ${item.id}');
|
||||
await state.queueAudioBookDownload(
|
||||
item,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> deleteDownloadedItem(LibraryItemExpanded item) async {
|
||||
_logger.fine('deleting downloaded item ${item.id}');
|
||||
await state.deleteDownloadedItem(item);
|
||||
ref.notifyListeners();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -158,7 +158,7 @@ class _DownloadHistoryProviderElement
|
|||
}
|
||||
|
||||
String _$simpleDownloadManagerHash() =>
|
||||
r'cec95717c86e422f88f78aa014d29e800e5a2089';
|
||||
r'8ab13f06ec5f2f73b73064bd285813dc890b7f36';
|
||||
|
||||
/// See also [SimpleDownloadManager].
|
||||
@ProviderFor(SimpleDownloadManager)
|
||||
|
|
@ -174,7 +174,7 @@ final simpleDownloadManagerProvider = NotifierProvider<SimpleDownloadManager,
|
|||
);
|
||||
|
||||
typedef _$SimpleDownloadManager = Notifier<core.AudiobookDownloadManager>;
|
||||
String _$downloadManagerHash() => r'7296a39439230f77abbe7d3231dae748f09c7ecf';
|
||||
String _$downloadManagerHash() => r'852012e32e613f86445afc7f7e4e85bec808e982';
|
||||
|
||||
/// See also [DownloadManager].
|
||||
@ProviderFor(DownloadManager)
|
||||
|
|
|
|||
|
|
@ -518,7 +518,7 @@ Future<void> 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<void> libraryItemPlayButtonOnPressed({
|
|||
Future<void>? 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<void> 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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -383,7 +383,7 @@ class _BookCover extends HookConsumerWidget {
|
|||
: themeData,
|
||||
);
|
||||
} catch (e) {
|
||||
appLogger.shout('Error changing theme: $e');
|
||||
appLogger.severe('Error changing theme: $e');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
36
lib/features/logging/core/logger.dart
Normal file
36
lib/features/logging/core/logger.dart
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:logging_appenders/logging_appenders.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:vaani/shared/extensions/duration_format.dart';
|
||||
|
||||
Future<String> getLoggingFilePath() async {
|
||||
final Directory directory = await getApplicationDocumentsDirectory();
|
||||
return '${directory.path}/vaani.log';
|
||||
}
|
||||
|
||||
Future<void> initLogging() async {
|
||||
final formatter = const DefaultLogRecordFormatter();
|
||||
if (kReleaseMode) {
|
||||
Logger.root.level = Level.INFO; // is also the default
|
||||
// Write to a file
|
||||
RotatingFileAppender(
|
||||
baseFilePath: await getLoggingFilePath(),
|
||||
formatter: formatter,
|
||||
).attachToLogger(Logger.root);
|
||||
} else {
|
||||
Logger.root.level = Level.FINE; // Capture all logs
|
||||
RotatingFileAppender(
|
||||
baseFilePath: await getLoggingFilePath(),
|
||||
formatter: formatter,
|
||||
).attachToLogger(Logger.root);
|
||||
Logger.root.onRecord.listen((record) {
|
||||
// Print log records to the console
|
||||
debugPrint(
|
||||
'${record.loggerName}: ${record.level.name}: ${record.time.time}: ${record.message}',
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
76
lib/features/logging/providers/logs_provider.dart
Normal file
76
lib/features/logging/providers/logs_provider.dart
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:archive/archive_io.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:vaani/features/logging/core/logger.dart';
|
||||
|
||||
part 'logs_provider.g.dart';
|
||||
|
||||
@riverpod
|
||||
class Logs extends _$Logs {
|
||||
@override
|
||||
Future<List<LogRecord>> build() async {
|
||||
final path = await getLoggingFilePath();
|
||||
final file = File(path);
|
||||
if (!file.existsSync()) {
|
||||
return [];
|
||||
}
|
||||
final lines = await file.readAsLines();
|
||||
return lines.map(parseLogLine).toList();
|
||||
}
|
||||
|
||||
Future<void> clear() async {
|
||||
final path = await getLoggingFilePath();
|
||||
final file = File(path);
|
||||
await file.writeAsString('');
|
||||
state = AsyncData([]);
|
||||
}
|
||||
|
||||
Future<String> getZipFilePath() async {
|
||||
var encoder = ZipFileEncoder();
|
||||
encoder.create(await generateZipFilePath());
|
||||
encoder.addFile(File(await getLoggingFilePath()));
|
||||
encoder.close();
|
||||
return encoder.zipPath;
|
||||
}
|
||||
}
|
||||
|
||||
Future<String> generateZipFilePath() async {
|
||||
Directory appDocDirectory = await getTemporaryDirectory();
|
||||
return '${appDocDirectory.path}/${generateZipFileName()}';
|
||||
}
|
||||
|
||||
String generateZipFileName() {
|
||||
return 'vaani-${DateTime.now().toIso8601String()}.zip';
|
||||
}
|
||||
|
||||
Level parseLevel(String level) {
|
||||
return Level.LEVELS
|
||||
.firstWhere((l) => l.name == level, orElse: () => Level.ALL);
|
||||
}
|
||||
|
||||
LogRecord parseLogLine(String line) {
|
||||
// 2024-10-03 00:48:58.012400 INFO GoRouter - getting location for name: "logs"
|
||||
|
||||
final RegExp logLineRegExp = RegExp(
|
||||
r'(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{6}) (\w+) (\w+) - (.+)',
|
||||
);
|
||||
|
||||
final match = logLineRegExp.firstMatch(line);
|
||||
if (match == null) {
|
||||
// return as is
|
||||
return LogRecord(Level.ALL, line, 'Unknown');
|
||||
}
|
||||
|
||||
final timeString = match.group(1)!;
|
||||
final levelString = match.group(2)!;
|
||||
final loggerName = match.group(3)!;
|
||||
final message = match.group(4)!;
|
||||
|
||||
final time = DateTime.parse(timeString);
|
||||
final level = parseLevel(levelString);
|
||||
|
||||
return LogRecord(level, message, loggerName, time);
|
||||
}
|
||||
25
lib/features/logging/providers/logs_provider.g.dart
Normal file
25
lib/features/logging/providers/logs_provider.g.dart
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'logs_provider.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$logsHash() => r'901376741d17ddbb889d1b7b96bc2882289720a0';
|
||||
|
||||
/// See also [Logs].
|
||||
@ProviderFor(Logs)
|
||||
final logsProvider =
|
||||
AutoDisposeAsyncNotifierProvider<Logs, List<LogRecord>>.internal(
|
||||
Logs.new,
|
||||
name: r'logsProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product') ? null : _$logsHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef _$Logs = AutoDisposeAsyncNotifier<List<LogRecord>>;
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
|
||||
266
lib/features/logging/view/logs_page.dart
Normal file
266
lib/features/logging/view/logs_page.dart
Normal file
|
|
@ -0,0 +1,266 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
import 'package:vaani/features/logging/providers/logs_provider.dart';
|
||||
import 'package:vaani/main.dart';
|
||||
|
||||
class LogsPage extends HookConsumerWidget {
|
||||
const LogsPage({super.key});
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final logs = ref.watch(logsProvider);
|
||||
final theme = Theme.of(context);
|
||||
final scrollController = useScrollController();
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Logs'),
|
||||
actions: [
|
||||
IconButton(
|
||||
tooltip: 'Clear logs',
|
||||
icon: const Icon(Icons.delete_forever),
|
||||
onPressed: () async {
|
||||
// ask for confirmation
|
||||
final shouldClear = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return AlertDialog(
|
||||
title: const Text('Clear logs?'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop(false);
|
||||
},
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop(true);
|
||||
},
|
||||
child: const Text('Clear'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
if (shouldClear == true) {
|
||||
ref.read(logsProvider.notifier).clear();
|
||||
}
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
tooltip: 'Share logs',
|
||||
icon: const Icon(Icons.share),
|
||||
onPressed: () async {
|
||||
appLogger.info('Preparing logs for sharing');
|
||||
final zipLogFilePath =
|
||||
await ref.read(logsProvider.notifier).getZipFilePath();
|
||||
|
||||
// submit logs
|
||||
final result = await Share.shareXFiles([XFile(zipLogFilePath)]);
|
||||
|
||||
switch (result.status) {
|
||||
case ShareResultStatus.success:
|
||||
appLogger.info('Share success');
|
||||
break;
|
||||
case ShareResultStatus.dismissed:
|
||||
appLogger.info('Share dismissed');
|
||||
break;
|
||||
case ShareResultStatus.unavailable:
|
||||
appLogger.severe('Share unavailable');
|
||||
break;
|
||||
}
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
tooltip: 'Download logs',
|
||||
icon: const Icon(Icons.download),
|
||||
onPressed: () async {
|
||||
appLogger.info('Preparing logs for download');
|
||||
final zipLogFilePath =
|
||||
await ref.read(logsProvider.notifier).getZipFilePath();
|
||||
|
||||
// save to folder
|
||||
String? outputFile = await FilePicker.platform.saveFile(
|
||||
dialogTitle: 'Please select an output file:',
|
||||
fileName: zipLogFilePath.split('/').last,
|
||||
);
|
||||
if (outputFile != null) {
|
||||
try {
|
||||
final file = File(outputFile);
|
||||
final zipFile = File(zipLogFilePath);
|
||||
await zipFile.copy(file.path);
|
||||
} catch (e) {
|
||||
appLogger.severe('Error saving file: $e');
|
||||
}
|
||||
} else {
|
||||
appLogger.info('Download cancelled');
|
||||
}
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
tooltip: 'Refresh logs',
|
||||
icon: const Icon(Icons.refresh),
|
||||
onPressed: () {
|
||||
ref.invalidate(logsProvider);
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
tooltip: 'Scroll to top',
|
||||
icon: const Icon(Icons.arrow_upward),
|
||||
onPressed: () {
|
||||
scrollController.animateTo(
|
||||
0,
|
||||
duration: const Duration(milliseconds: 500),
|
||||
curve: Curves.easeInOut,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
// a column with listview.builder and a scrollable list of logs
|
||||
body: Column(
|
||||
children: [
|
||||
// a filter for log levels, loggers, and search
|
||||
// TODO: implement filters and search
|
||||
|
||||
Expanded(
|
||||
child: logs.when(
|
||||
data: (logRecords) {
|
||||
return Scrollbar(
|
||||
controller: scrollController,
|
||||
child: ListView.builder(
|
||||
controller: scrollController,
|
||||
itemCount: logRecords.length,
|
||||
itemBuilder: (context, index) {
|
||||
final logRecord = logRecords[index];
|
||||
return LogRecordTile(logRecord: logRecord, theme: theme);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error: (error, stackTrace) => Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Text('Error loading logs'),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
ref.invalidate(logsProvider);
|
||||
},
|
||||
child: const Text('Retry'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class LogRecordTile extends StatelessWidget {
|
||||
final LogRecord logRecord;
|
||||
final ThemeData theme;
|
||||
const LogRecordTile({
|
||||
required this.logRecord,
|
||||
required this.theme,
|
||||
super.key,
|
||||
});
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListTile(
|
||||
leading: CircleAvatar(
|
||||
backgroundColor: getLogLevelColor(logRecord.level, theme),
|
||||
child: Icon(
|
||||
getLogLevelIcon(logRecord.level),
|
||||
color: getLogLevelTextColor(logRecord.level, theme),
|
||||
),
|
||||
),
|
||||
title: Text(logRecord.loggerName),
|
||||
subtitle: Text(logRecord.message),
|
||||
onTap: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return AlertDialog(
|
||||
icon: Icon(getLogLevelIcon(logRecord.level)),
|
||||
title: Text(logRecord.loggerName),
|
||||
content: Text.rich(
|
||||
TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text: logRecord.time.toIso8601String(),
|
||||
style: const TextStyle(fontStyle: FontStyle.italic),
|
||||
),
|
||||
const TextSpan(text: '\n\n'),
|
||||
TextSpan(
|
||||
text: logRecord.message,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: const Text('Close'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
IconData getLogLevelIcon(Level level) {
|
||||
switch (level) {
|
||||
case Level.INFO:
|
||||
return (Icons.info);
|
||||
case Level.WARNING:
|
||||
return (Icons.warning);
|
||||
case Level.SEVERE:
|
||||
case Level.SHOUT:
|
||||
return (Icons.error);
|
||||
default:
|
||||
return (Icons.bug_report);
|
||||
}
|
||||
}
|
||||
|
||||
Color? getLogLevelColor(Level level, ThemeData theme) {
|
||||
switch (level) {
|
||||
case Level.INFO:
|
||||
return theme.colorScheme.surfaceContainerLow;
|
||||
case Level.WARNING:
|
||||
return theme.colorScheme.surfaceBright;
|
||||
case Level.SEVERE:
|
||||
case Level.SHOUT:
|
||||
return theme.colorScheme.errorContainer;
|
||||
default:
|
||||
return theme.colorScheme.primaryContainer;
|
||||
}
|
||||
}
|
||||
|
||||
Color? getLogLevelTextColor(Level level, ThemeData theme) {
|
||||
switch (level) {
|
||||
case Level.INFO:
|
||||
return theme.colorScheme.onSurface;
|
||||
case Level.WARNING:
|
||||
return theme.colorScheme.onSurface;
|
||||
case Level.SEVERE:
|
||||
case Level.SHOUT:
|
||||
return theme.colorScheme.onErrorContainer;
|
||||
default:
|
||||
return theme.colorScheme.onPrimaryContainer;
|
||||
}
|
||||
}
|
||||
|
|
@ -11,6 +11,7 @@ import 'package:vaani/models/error_response.dart';
|
|||
import 'package:vaani/router/router.dart';
|
||||
import 'package:vaani/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(
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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>(Duration.zero,
|
||||
(previousValue, element) {
|
||||
return previousValue + element.duration;
|
||||
});
|
||||
final total = book.tracks.sublist(0, index).fold<Duration>(
|
||||
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<Uri>? 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<void> 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<void> 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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
|
||||
_logger.finer('created player');
|
||||
|
||||
return player;
|
||||
}
|
||||
|
||||
void notifyListeners() {
|
||||
ref.notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> setSpeed(double speed) async {
|
||||
await state.setSpeed(speed);
|
||||
notifyListeners();
|
||||
ref.notifyListeners();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<core.AudiobookPlayer>;
|
||||
String _$audiobookPlayerHash() => r'44394b1dbbf85eb19ef1f693717e8cbc15b768e5';
|
||||
String _$audiobookPlayerHash() => r'0f180308067486896fec6a65a6afb0e6686ac4a0';
|
||||
|
||||
/// See also [AudiobookPlayer].
|
||||
@ProviderFor(AudiobookPlayer)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ part of 'currently_playing_provider.dart';
|
|||
// **************************************************************************
|
||||
|
||||
String _$currentlyPlayingBookHash() =>
|
||||
r'52334c7b4d68fd498a2a00208d8d7f1ba0085237';
|
||||
r'7440b0d54cb364f66e704783652e8f1490ae90e0';
|
||||
|
||||
/// See also [currentlyPlayingBook].
|
||||
@ProviderFor(currentlyPlayingBook)
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|||
import 'package:vaani/features/player/providers/audiobook_player.dart';
|
||||
import 'package:vaani/features/player/providers/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,
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<FormState>();
|
||||
|
||||
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'),
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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}) {
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ part of 'api_settings_provider.dart';
|
|||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$apiSettingsHash() => r'26e7e09e7369bac9fbf0589da9fd97d1f15b7926';
|
||||
String _$apiSettingsHash() => r'5bc1e16e9d72b77fb10637aabadf08e8947da580';
|
||||
|
||||
/// See also [ApiSettings].
|
||||
@ProviderFor(ApiSettings)
|
||||
|
|
|
|||
|
|
@ -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(' ', '_');
|
||||
}
|
||||
|
|
|
|||
125
lib/shared/extensions/obfuscation.dart
Normal file
125
lib/shared/extensions/obfuscation.dart
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:vaani/settings/models/api_settings.dart';
|
||||
import 'package:vaani/settings/models/audiobookshelf_server.dart';
|
||||
import 'package:vaani/settings/models/authenticated_user.dart';
|
||||
|
||||
// bool kReleaseMode = true;
|
||||
|
||||
extension ObfuscateString on String {
|
||||
String obfuscate() {
|
||||
if (!kReleaseMode) {
|
||||
return this;
|
||||
}
|
||||
return 'obfuscated';
|
||||
}
|
||||
}
|
||||
|
||||
extension ObfuscateURI on Uri {
|
||||
/// keeps everything except the base url for security reasons
|
||||
Uri obfuscate() {
|
||||
if (!kReleaseMode) {
|
||||
return this;
|
||||
}
|
||||
|
||||
// do not obfuscate the local host
|
||||
if ([null, 'localhost'].contains(host)) {
|
||||
return this;
|
||||
}
|
||||
|
||||
// do not obfuscate file urls
|
||||
if (scheme == 'file') {
|
||||
return this;
|
||||
}
|
||||
|
||||
return replace(
|
||||
userInfo: userInfo == '' ? '' : 'userInfoObfuscated',
|
||||
host: 'hostObfuscated',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
extension ObfuscateList<T> on List<T> {
|
||||
List<T> obfuscate() {
|
||||
return map((e) {
|
||||
if (e is AuthenticatedUser) {
|
||||
return e.obfuscate() as T;
|
||||
} else if (e is AudiobookShelfServer) {
|
||||
return e.obfuscate() as T;
|
||||
} else if (e is Uri) {
|
||||
return e.obfuscate() as T;
|
||||
} else {
|
||||
return e;
|
||||
}
|
||||
}).toList();
|
||||
}
|
||||
}
|
||||
|
||||
extension ObfuscateSet<T> on Set<T> {
|
||||
Set<T> obfuscate() {
|
||||
return toList().obfuscate().toSet();
|
||||
}
|
||||
}
|
||||
|
||||
extension ObfuscateAuthenticatedUser on AuthenticatedUser {
|
||||
AuthenticatedUser obfuscate() {
|
||||
if (!kReleaseMode) {
|
||||
return this;
|
||||
}
|
||||
return copyWith(
|
||||
password: password == null ? null : 'passwordObfuscated',
|
||||
username: username == null ? null : 'usernameObfuscated',
|
||||
authToken: 'authTokenObfuscated',
|
||||
server: server.obfuscate(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
extension ObfuscateServer on AudiobookShelfServer {
|
||||
AudiobookShelfServer obfuscate() {
|
||||
if (!kReleaseMode) {
|
||||
return this;
|
||||
}
|
||||
return copyWith(
|
||||
serverUrl: serverUrl.obfuscate(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
extension ObfuscateApiSettings on ApiSettings {
|
||||
ApiSettings obfuscate() {
|
||||
if (!kReleaseMode) {
|
||||
return this;
|
||||
}
|
||||
return copyWith(
|
||||
activeServer: activeServer?.obfuscate(),
|
||||
activeUser: activeUser?.obfuscate(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
extension ObfuscateRequest on http.BaseRequest {
|
||||
http.BaseRequest obfuscate() {
|
||||
if (!kReleaseMode) {
|
||||
return this;
|
||||
}
|
||||
return http.Request(
|
||||
method,
|
||||
url.obfuscate(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
extension ObfuscateResponse on http.Response {
|
||||
http.Response obfuscate() {
|
||||
if (!kReleaseMode) {
|
||||
return this;
|
||||
}
|
||||
return http.Response(
|
||||
body,
|
||||
statusCode,
|
||||
headers: headers,
|
||||
request: request?.obfuscate(),
|
||||
);
|
||||
}
|
||||
}
|
||||
44
pubspec.lock
44
pubspec.lock
|
|
@ -47,7 +47,7 @@ packages:
|
|||
source: hosted
|
||||
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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
24
test/features/logging/providers/logs_provider_test.dart
Normal file
24
test/features/logging/providers/logs_provider_test.dart
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:logging_appenders/logging_appenders.dart';
|
||||
import 'package:vaani/features/logging/providers/logs_provider.dart';
|
||||
|
||||
void main() {
|
||||
test(
|
||||
'Should parse log line',
|
||||
() async {
|
||||
final formatter = DefaultLogRecordFormatter();
|
||||
final logRecord = LogRecord(
|
||||
Level.INFO,
|
||||
'getting location for name: "logs"',
|
||||
'GoRouter',
|
||||
);
|
||||
final expected = parseLogLine(
|
||||
formatter.format(logRecord),
|
||||
);
|
||||
expect(logRecord.message, expected.message);
|
||||
expect(logRecord.level, expected.level);
|
||||
expect(logRecord.loggerName, expected.loggerName);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
#include <isar_flutter_libs/isar_flutter_libs_plugin.h>
|
||||
#include <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>
|
||||
|
||||
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"));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
isar_flutter_libs
|
||||
media_kit_libs_windows_audio
|
||||
share_plus
|
||||
url_launcher_windows
|
||||
)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue