diff --git a/.untranslated-messages.json b/.untranslated-messages.json index 07df05a..4df108f 100644 --- a/.untranslated-messages.json +++ b/.untranslated-messages.json @@ -22,6 +22,8 @@ "resourcesSortByTitle", "resourcesSortByUpdated", "settingsAboutActionsSupport", + "settingsAboutShareLogs", + "settingsAboutChooseLog", "settingsNetworkOptionsOfflineMode", "settingsNetworkOptionsOfflineModeOff", "settingsNetworkOptionsOfflineModeOn", @@ -53,6 +55,8 @@ "resourcesSortByTitle", "resourcesSortByUpdated", "settingsAboutActionsSupport", + "settingsAboutShareLogs", + "settingsAboutChooseLog", "settingsNetworkOptionsOfflineMode", "settingsNetworkOptionsOfflineModeOff", "settingsNetworkOptionsOfflineModeOn", @@ -62,32 +66,12 @@ ], "cs": [ - "actionsCancel", - "actionsDelete", - "actionsDownload", - "actionsDownloadCancel", - "actionsDownloadDelete", - "actionsOk", - "controlsShuffle", "resourcesAlbumCount", "resourcesArtistCount", - "resourcesFilterAlbum", - "resourcesFilterArtist", - "resourcesFilterOwner", - "resourcesFilterYear", "resourcesPlaylistCount", "resourcesSongCount", - "resourcesSongListDeleteAllContent", - "resourcesSongListDeleteAllTitle", - "resourcesSortByAlbum", - "resourcesSortByAlbumCount", - "resourcesSortByTitle", - "resourcesSortByUpdated", - "settingsAboutActionsLicenses", - "settingsAboutActionsProjectHomepage", - "settingsAboutActionsSupport", - "settingsAboutName", - "settingsAboutVersion", + "settingsAboutShareLogs", + "settingsAboutChooseLog", "settingsMusicName", "settingsMusicOptionsScrobbleDescriptionOff", "settingsMusicOptionsScrobbleDescriptionOn", @@ -96,12 +80,7 @@ "settingsNetworkOptionsMinBufferTitle", "settingsNetworkOptionsOfflineMode", "settingsNetworkOptionsOfflineModeOff", - "settingsNetworkOptionsOfflineModeOn", - "settingsNetworkOptionsStreamFormat", - "settingsNetworkOptionsStreamFormatServerDefault", - "settingsResetActionsClearImageCache", - "settingsResetName", - "settingsServersFieldsName" + "settingsNetworkOptionsOfflineModeOn" ], "da": [ @@ -133,6 +112,8 @@ "resourcesSortByTitle", "resourcesSortByUpdated", "settingsAboutActionsSupport", + "settingsAboutShareLogs", + "settingsAboutChooseLog", "settingsMusicOptionsScrobbleDescriptionOff", "settingsNetworkOptionsOfflineMode", "settingsNetworkOptionsOfflineModeOff", @@ -163,6 +144,8 @@ "resourcesSortByTitle", "resourcesSortByUpdated", "settingsAboutActionsSupport", + "settingsAboutShareLogs", + "settingsAboutChooseLog", "settingsNetworkOptionsOfflineMode", "settingsNetworkOptionsOfflineModeOff", "settingsNetworkOptionsOfflineModeOn", @@ -187,6 +170,8 @@ "resourcesSortByTitle", "resourcesSortByUpdated", "settingsAboutActionsSupport", + "settingsAboutShareLogs", + "settingsAboutChooseLog", "settingsNetworkOptionsOfflineMode", "settingsNetworkOptionsOfflineModeOff", "settingsNetworkOptionsOfflineModeOn", @@ -218,6 +203,8 @@ "resourcesSortByTitle", "resourcesSortByUpdated", "settingsAboutActionsSupport", + "settingsAboutShareLogs", + "settingsAboutChooseLog", "settingsNetworkOptionsOfflineMode", "settingsNetworkOptionsOfflineModeOff", "settingsNetworkOptionsOfflineModeOn", @@ -226,6 +213,11 @@ "settingsServersFieldsName" ], + "gl": [ + "settingsAboutShareLogs", + "settingsAboutChooseLog" + ], + "it": [ "actionsCancel", "actionsDelete", @@ -249,6 +241,8 @@ "resourcesSortByTitle", "resourcesSortByUpdated", "settingsAboutActionsSupport", + "settingsAboutShareLogs", + "settingsAboutChooseLog", "settingsNetworkOptionsOfflineMode", "settingsNetworkOptionsOfflineModeOff", "settingsNetworkOptionsOfflineModeOn", @@ -300,6 +294,8 @@ "settingsAboutActionsLicenses", "settingsAboutActionsSupport", "settingsAboutName", + "settingsAboutShareLogs", + "settingsAboutChooseLog", "settingsAboutVersion", "settingsMusicOptionsScrobbleDescriptionOff", "settingsMusicOptionsScrobbleDescriptionOn", @@ -356,6 +352,8 @@ "resourcesSortByTitle", "resourcesSortByUpdated", "settingsAboutActionsSupport", + "settingsAboutShareLogs", + "settingsAboutChooseLog", "settingsNetworkOptionsOfflineMode", "settingsNetworkOptionsOfflineModeOff", "settingsNetworkOptionsOfflineModeOn", @@ -387,6 +385,8 @@ "resourcesSortByTitle", "resourcesSortByUpdated", "settingsAboutActionsSupport", + "settingsAboutShareLogs", + "settingsAboutChooseLog", "settingsNetworkOptionsOfflineMode", "settingsNetworkOptionsOfflineModeOff", "settingsNetworkOptionsOfflineModeOn", @@ -418,6 +418,8 @@ "resourcesSortByTitle", "resourcesSortByUpdated", "settingsAboutActionsSupport", + "settingsAboutShareLogs", + "settingsAboutChooseLog", "settingsNetworkOptionsOfflineMode", "settingsNetworkOptionsOfflineModeOff", "settingsNetworkOptionsOfflineModeOn", @@ -427,32 +429,17 @@ ], "pt": [ - "actionsCancel", - "actionsDelete", - "actionsDownload", - "actionsDownloadCancel", - "actionsDownloadDelete", - "actionsOk", - "controlsShuffle", "resourcesAlbumCount", "resourcesArtistCount", - "resourcesFilterAlbum", - "resourcesFilterArtist", "resourcesFilterOwner", - "resourcesFilterYear", "resourcesPlaylistCount", "resourcesSongCount", "resourcesSongListDeleteAllContent", "resourcesSongListDeleteAllTitle", - "resourcesSortByAlbum", "resourcesSortByAlbumCount", - "resourcesSortByTitle", "resourcesSortByUpdated", - "settingsAboutActionsSupport", - "settingsNetworkOptionsOfflineMode", - "settingsNetworkOptionsOfflineModeOff", - "settingsNetworkOptionsOfflineModeOn", - "settingsNetworkOptionsStreamFormat", + "settingsAboutShareLogs", + "settingsAboutChooseLog", "settingsNetworkOptionsStreamFormatServerDefault", "settingsServersFieldsName" ], @@ -480,6 +467,8 @@ "resourcesSortByTitle", "resourcesSortByUpdated", "settingsAboutActionsSupport", + "settingsAboutShareLogs", + "settingsAboutChooseLog", "settingsNetworkOptionsOfflineMode", "settingsNetworkOptionsOfflineModeOff", "settingsNetworkOptionsOfflineModeOn", @@ -511,6 +500,8 @@ "resourcesSortByTitle", "resourcesSortByUpdated", "settingsAboutActionsSupport", + "settingsAboutShareLogs", + "settingsAboutChooseLog", "settingsNetworkOptionsOfflineMode", "settingsNetworkOptionsOfflineModeOff", "settingsNetworkOptionsOfflineModeOn", @@ -542,6 +533,8 @@ "resourcesSortByTitle", "resourcesSortByUpdated", "settingsAboutActionsSupport", + "settingsAboutShareLogs", + "settingsAboutChooseLog", "settingsNetworkOptionsOfflineMode", "settingsNetworkOptionsOfflineModeOff", "settingsNetworkOptionsOfflineModeOn", @@ -556,6 +549,8 @@ "resourcesArtistCount", "resourcesPlaylistCount", "resourcesSongCount", + "settingsAboutShareLogs", + "settingsAboutChooseLog", "settingsNetworkOptionsOfflineMode", "settingsNetworkOptionsOfflineModeOff", "settingsNetworkOptionsOfflineModeOn", diff --git a/lib/app/pages/settings_page.dart b/lib/app/pages/settings_page.dart index 7f744b6..ab90bdf 100644 --- a/lib/app/pages/settings_page.dart +++ b/lib/app/pages/settings_page.dart @@ -1,11 +1,16 @@ +import 'dart:math'; + import 'package:auto_route/auto_route.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:path/path.dart' as p; +import 'package:share_plus/share_plus.dart'; import 'package:url_launcher/url_launcher.dart'; +import '../../log.dart'; import '../../models/support.dart'; import '../../services/settings_service.dart'; import '../../state/init.dart'; @@ -162,6 +167,54 @@ class _About extends HookConsumerWidget { mode: LaunchMode.externalApplication, ), ), + const SizedBox(height: 12), + const _ShareLogsButton(), + ], + ); + } +} + +class _ShareLogsButton extends StatelessWidget { + const _ShareLogsButton(); + + @override + Widget build(BuildContext context) { + final l = AppLocalizations.of(context); + + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + OutlinedButton.icon( + icon: const Icon(Icons.share), + label: Text(l.settingsAboutShareLogs), + onPressed: () async { + final files = await logFiles(); + if (files.isEmpty) return; + + // ignore: use_build_context_synchronously + final value = await showDialog( + context: context, + builder: (context) => MultipleChoiceDialog( + title: l.settingsAboutChooseLog, + current: files.first.path, + options: files + .map((e) => MultiChoiceOption.string( + title: p.basename(e.path), + option: e.path, + )) + .toIList(), + ), + ); + + if (value == null) return; + Share.shareXFiles( + [XFile(value, mimeType: 'text/plain')], + subject: 'Logs from subtracks: ${String.fromCharCodes( + List.generate(8, (_) => Random().nextInt(26) + 65), + )}', + ); + }, + ), ], ); } diff --git a/lib/http/client.dart b/lib/http/client.dart index f58c777..f4f32aa 100644 --- a/lib/http/client.dart +++ b/lib/http/client.dart @@ -1,7 +1,8 @@ -import 'package:flutter/foundation.dart'; import 'package:http/http.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; +import '../log.dart'; + part 'client.g.dart'; const Map subtracksHeaders = { @@ -14,11 +15,26 @@ class SubtracksHttpClient extends BaseClient { @override Future send(BaseRequest request) { request.headers.addAll(subtracksHeaders); - if (kDebugMode) print('${request.method} ${request.url}'); + log.info('${request.method} ${_redactUri(request.url)}'); return request.send(); } } +String _redactUri(Uri uri) { + var redacted = uri.toString(); + redacted = _redactParam(redacted, 'u'); + redacted = _redactParam(redacted, 'p'); + redacted = _redactParam(redacted, 's'); + redacted = _redactParam(redacted, 't'); + + return redacted.toString(); +} + +RegExp _queryReplace(String key) => RegExp('$key=([^&|\\n|\\t\\s]+)'); + +String _redactParam(String url, String key) => + url.replaceFirst(_queryReplace(key), '$key=REDACTED'); + @Riverpod(keepAlive: true) BaseClient httpClient(HttpClientRef ref) { return SubtracksHttpClient(); diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 1c5cf44..df13de9 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -173,6 +173,10 @@ "@settingsAboutActionsSupport": {}, "settingsAboutName": "About", "@settingsAboutName": {}, + "settingsAboutShareLogs": "Share logs", + "@settingsAboutShareLogs": {}, + "settingsAboutChooseLog": "Choose logs file", + "@settingsAboutChooseLog": {}, "settingsAboutVersion": "version {version}", "@settingsAboutVersion": { "placeholders": { diff --git a/lib/log.dart b/lib/log.dart new file mode 100644 index 0000000..a438576 --- /dev/null +++ b/lib/log.dart @@ -0,0 +1,169 @@ +// import 'dart:convert'; + +import 'dart:io'; + +import 'package:collection/collection.dart'; +import 'package:flutter/foundation.dart'; +import 'package:logging/logging.dart'; +import 'package:path/path.dart' as p; +import 'package:path_provider/path_provider.dart'; + +class AnsiColor { + /// ANSI Control Sequence Introducer, signals the terminal for new settings. + static const ansiEsc = '\x1B['; + + /// Reset all colors and options for current SGRs to terminal defaults. + static const ansiDefault = '${ansiEsc}0m'; + + final int? fg; + final int? bg; + final bool color; + + AnsiColor.none() + : fg = null, + bg = null, + color = false; + + AnsiColor.fg(this.fg) + : bg = null, + color = true; + + AnsiColor.bg(this.bg) + : fg = null, + color = true; + + @override + String toString() { + if (fg != null) { + return '${ansiEsc}38;5;${fg}m'; + } else if (bg != null) { + return '${ansiEsc}48;5;${bg}m'; + } else { + return ''; + } + } + + String call(String msg) { + if (color) { + // ignore: unnecessary_brace_in_string_interps + return '${this}$msg$ansiDefault'; + } else { + return msg; + } + } + + AnsiColor toFg() => AnsiColor.fg(bg); + + AnsiColor toBg() => AnsiColor.bg(fg); + + /// Defaults the terminal's foreground color without altering the background. + String get resetForeground => color ? '${ansiEsc}39m' : ''; + + /// Defaults the terminal's background color without altering the foreground. + String get resetBackground => color ? '${ansiEsc}49m' : ''; + + static int grey(double level) => 232 + (level.clamp(0.0, 1.0) * 23).round(); +} + +final levelColors = { + Level.FINEST: AnsiColor.fg(AnsiColor.grey(0.5)), + Level.FINER: AnsiColor.fg(AnsiColor.grey(0.5)), + Level.FINE: AnsiColor.fg(AnsiColor.grey(0.5)), + Level.CONFIG: AnsiColor.fg(81), + Level.INFO: AnsiColor.fg(12), + Level.WARNING: AnsiColor.fg(208), + Level.SEVERE: AnsiColor.fg(196), + Level.SHOUT: AnsiColor.fg(199), +}; + +class LogData { + final String? message; + final Object? data; + + const LogData(this.message, this.data); +} + +String _format( + LogRecord event, { + bool color = false, + bool time = true, + bool level = true, +}) { + var message = ''; + if (time) message += '${event.time.toIso8601String()} '; + if (level) message += '${event.level.name} '; + + final object = event.object; + if (object is LogData) { + message += '${object.message}'; + message += '\n${object.data}'; + } else if (object != null) { + message += 'Object'; + message += '\n$object'; + } else { + message += event.message; + } + + if (event.error != null) { + message += '\n${event.error}'; + } + if (event.stackTrace != null) { + message += '\n${event.stackTrace}'; + } + + return color + ? message.split('\n').map((e) => levelColors[event.level]!(e)).join('\n') + : message; +} + +Future _printFile(String event, String dir) async { + final now = DateTime.now(); + final file = File(p.join(dir, '${now.year}-${now.month}-${now.day}.txt')); + + if (!event.endsWith('\n')) { + event += '\n'; + } + + await file.writeAsString(event, mode: FileMode.writeOnlyAppend, flush: true); +} + +Future logDirectory() async { + return Directory( + p.join((await getApplicationDocumentsDirectory()).path, 'logs'), + ); +} + +Future> logFiles() async { + final dir = await logDirectory(); + return dir.listSync().whereType().toList() + ..sort( + (a, b) => b.statSync().modified.compareTo(a.statSync().modified), + ); +} + +final log = Logger('default'); + +Future initLogging() async { + final dir = (await logDirectory())..create(); + + final files = await logFiles(); + if (files.length > 7) { + for (var file in files.slice(7)) { + await file.delete(); + } + } + + Logger.root.level = kDebugMode ? Level.ALL : Level.INFO; + Logger.root.onRecord.asyncMap((event) async { + if (kDebugMode) { + print(_format(event, color: true, time: false, level: false)); + } else { + await _printFile( + _format(event, color: false, time: true, level: true), + dir.path, + ); + } + }).listen((_) {}, cancelOnError: false); + + log.info('start'); +} diff --git a/lib/main.dart b/lib/main.dart index cbadc06..b2a5b29 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -4,6 +4,7 @@ import 'package:stack_trace/stack_trace.dart' as stack_trace; import 'package:worker_manager/worker_manager.dart'; import 'app/app.dart'; +import 'log.dart'; void main() async { // TOOD: probably remove before live @@ -18,5 +19,8 @@ void main() async { await Executor().warmUp(); WidgetsFlutterBinding.ensureInitialized(); + + await initLogging(); + runApp(const ProviderScope(child: MyApp())); } diff --git a/lib/services/audio_service.dart b/lib/services/audio_service.dart index 9b67689..af7de98 100644 --- a/lib/services/audio_service.dart +++ b/lib/services/audio_service.dart @@ -4,7 +4,6 @@ import 'dart:math'; import 'package:audio_service/audio_service.dart'; import 'package:collection/collection.dart'; import 'package:drift/drift.dart' show Value; -import 'package:flutter/foundation.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:just_audio/just_audio.dart'; import 'package:pool/pool.dart'; @@ -14,6 +13,7 @@ import 'package:synchronized/synchronized.dart'; import '../cache/image_cache.dart'; import '../database/database.dart'; +import '../log.dart'; import '../models/music.dart'; import '../models/query.dart'; import '../models/support.dart'; @@ -137,7 +137,7 @@ class AudioControl extends BaseAudioHandler with QueueHandler, SeekHandler { _player.processingStateStream.listen((event) async { if (event == ProcessingState.completed) { if (_audioSource.length > 0) { - yell('completed'); + log.fine('completed'); await stop(); await seek(Duration.zero); } @@ -386,7 +386,7 @@ class AudioControl extends BaseAudioHandler with QueueHandler, SeekHandler { mediaItem.add(slice.current!.mediaItem); queue.add(list.map((e) => e.mediaItem).toList()); - yell('addAll'); + log.fine('addAll'); await _audioSource.addAll(list.map((e) => e.audioSource).toList()); await _player.seek(Duration.zero, index: list.indexOf(slice.current!)); } @@ -410,7 +410,7 @@ class AudioControl extends BaseAudioHandler with QueueHandler, SeekHandler { final sourceNeedsPrev = sourceIndex == 0; if (sourceNeedsNext && slice.next != null) { - yell('add'); + log.fine('add'); await _audioSource.add(slice.next!.audioSource); } if (sourceNeedsPrev && slice.prev != null) { @@ -497,7 +497,7 @@ class AudioControl extends BaseAudioHandler with QueueHandler, SeekHandler { } Future _insertFirstAudioSource(AudioSource source) { - yell('insert'); + log.fine('insert'); final wait = _audioSource.insert(0, source); _currentIndexIgnore.add(1); return wait; @@ -505,20 +505,20 @@ class AudioControl extends BaseAudioHandler with QueueHandler, SeekHandler { Future _pruneAudioSources(int keepIndex) async { if (keepIndex > 0) { - yell('removeRange 0'); + log.fine('removeRange 0'); final wait = _audioSource.removeRange(0, keepIndex); _currentIndexIgnore.add(0); await wait; } if (_audioSource.length > 1) { - yell('removeRange 1'); + log.fine('removeRange 1'); await _audioSource.removeRange(1, _audioSource.length); } } Future _clearAudioSource([bool clearMetadata = false]) async { // await _player.stop(); - yell('_clearAudioSource'); + log.fine('_clearAudioSource'); await _audioSource.clear(); if (clearMetadata) { @@ -697,11 +697,3 @@ class AudioControl extends BaseAudioHandler with QueueHandler, SeekHandler { } } } - -void yell(String msg) { - if (kDebugMode) { - print('=================================================================<'); - print(msg); - print('=================================================================>'); - } -} diff --git a/pubspec.lock b/pubspec.lock index 0848795..1d12ea2 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -290,6 +290,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.6.3" + cross_file: + dependency: transitive + description: + name: cross_file + sha256: "0b0036e8cccbfbe0555fd83c1d31a6f30b77a96b598b35a5d36dd41f718695e9" + url: "https://pub.dev" + source: hosted + version: "0.3.3+4" crypto: dependency: "direct main" description: @@ -679,7 +687,7 @@ packages: source: hosted version: "2.0.1" logging: - dependency: transitive + dependency: "direct main" description: name: logging sha256: "04094f2eb032cbb06c6f6e8d3607edcfcb0455e2bb6cbc010cb01171dcb64e6d" @@ -834,10 +842,10 @@ packages: dependency: transitive description: name: path_provider_windows - sha256: f53720498d5a543f9607db4b0e997c4b5438884de25b0f73098cc2671a51b130 + sha256: d3f80b32e83ec208ac95253e0cd4d298e104fbc63cb29c5c69edaed43b0c69d6 url: "https://pub.dev" source: hosted - version: "2.1.5" + version: "2.1.6" pedantic: dependency: transitive description: @@ -950,6 +958,22 @@ packages: url: "https://pub.dev" source: hosted version: "0.27.7" + share_plus: + dependency: "direct main" + description: + name: share_plus + sha256: "322a1ec9d9fe07e2e2252c098ce93d12dbd06133cc4c00ffe6a4ef505c295c17" + url: "https://pub.dev" + source: hosted + version: "7.0.0" + share_plus_platform_interface: + dependency: transitive + description: + name: share_plus_platform_interface + sha256: "0c6e61471bd71b04a138b8b588fa388e66d8b005e6f2deda63371c5c505a0981" + url: "https://pub.dev" + source: hosted + version: "3.2.1" shelf: dependency: transitive description: @@ -1319,10 +1343,10 @@ packages: dependency: transitive description: name: win32 - sha256: a6f0236dbda0f63aa9a25ad1ff9a9d8a4eaaa5012da0dc59d21afdb1dc361ca4 + sha256: "5a751eddf9db89b3e5f9d50c20ab8612296e4e8db69009788d6c8b060a84191c" url: "https://pub.dev" source: hosted - version: "3.1.4" + version: "4.1.4" worker_manager: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index fa82427..21d35ef 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -57,6 +57,8 @@ dependencies: connectivity_plus: ^3.0.4 package_info_plus: ^3.1.1 url_launcher: ^6.1.10 + logging: ^1.1.1 + share_plus: ^7.0.0 # https://github.com/dart-lang/intl/issues/522#issuecomment-1469961807 dependency_overrides: