diff --git a/lib/collections/routes.dart b/lib/collections/routes.dart index 44f57def..9662419d 100644 --- a/lib/collections/routes.dart +++ b/lib/collections/routes.dart @@ -8,6 +8,7 @@ import 'package:spotube/pages/lyrics/mini_lyrics.dart'; import 'package:spotube/pages/search/search.dart'; import 'package:spotube/pages/settings/blacklist.dart'; import 'package:spotube/pages/settings/about.dart'; +import 'package:spotube/pages/settings/logs.dart'; import 'package:spotube/utils/platform.dart'; import 'package:spotube/components/shared/spotube_page_route.dart'; import 'package:spotube/pages/album/album.dart'; @@ -83,10 +84,16 @@ final router = GoRouter( child: const BlackListPage(), ), ), + GoRoute( + path: "logs", + pageBuilder: (context, state) => SpotubeSlidePage( + child: const LogsPage(), + ), + ), GoRoute( path: "about", - pageBuilder: (context, state) => const SpotubePage( - child: AboutSpotube(), + pageBuilder: (context, state) => SpotubeSlidePage( + child: const AboutSpotube(), ), ), ], diff --git a/lib/collections/spotube_icons.dart b/lib/collections/spotube_icons.dart index f5caac09..5db6e172 100644 --- a/lib/collections/spotube_icons.dart +++ b/lib/collections/spotube_icons.dart @@ -87,4 +87,6 @@ abstract class SpotubeIcons { static const volumeLow = FeatherIcons.volume; static const volumeMute = FeatherIcons.volumeX; static const timer = FeatherIcons.clock; + static const logs = FeatherIcons.fileText; + static const clipboard = FeatherIcons.clipboard; } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 16806c5e..85263046 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -244,5 +244,7 @@ "mins": "{minutes} Minutes", "hours": "{hours} Hours", "hour": "{hours} Hour", - "custom_hours": "Custom Hours" + "custom_hours": "Custom Hours", + "logs": "Logs", + "developers": "Developers" } \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 66a21772..f9037e35 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -26,7 +26,6 @@ import 'package:spotube/provider/palette_provider.dart'; import 'package:spotube/provider/user_preferences_provider.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:spotube/themes/theme.dart'; -import 'package:spotube/utils/custom_toast_handler.dart'; import 'package:spotube/utils/persisted_state_notifier.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; import 'package:system_theme/system_theme.dart'; @@ -122,16 +121,11 @@ Future main(List rawArgs) async { releaseConfig: CatcherOptions( SilentReportMode(), [ - if (arguments["verbose"] ?? false) - ConsoleHandler( - enableDeviceParameters: false, - enableApplicationParameters: false, - ), + if (arguments["verbose"] ?? false) ConsoleHandler(), FileHandler( await getLogsPath(), printLogs: false, ), - CustomToastHandler(), ], ), runAppFunction: () { diff --git a/lib/pages/settings/logs.dart b/lib/pages/settings/logs.dart new file mode 100644 index 00000000..3bc1319f --- /dev/null +++ b/lib/pages/settings/logs.dart @@ -0,0 +1,139 @@ +import 'dart:async'; +import 'dart:io'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:spotube/collections/spotube_icons.dart'; +import 'package:spotube/components/settings/section_card_with_heading.dart'; +import 'package:spotube/components/shared/page_window_title_bar.dart'; +import 'package:spotube/extensions/context.dart'; +import 'package:spotube/models/logger.dart'; + +class LogsPage extends HookWidget { + const LogsPage({Key? key}) : super(key: key); + + List<({DateTime? date, String body})> parseLogs(String raw) { + return raw + .split( + "======================================================================", + ) + .map( + (line) { + DateTime? date; + line = line + .replaceAll( + "============================== CATCHER LOG ==============================", + "", + ) + .split("\n") + .map((l) { + if (l.startsWith("Crash occurred on")) { + date = DateTime.parse( + l.split("Crash occurred on")[1].trim(), + ); + return ""; + } + return l; + }) + .where((l) => l.replaceAll("\n", "").trim().isNotEmpty) + .join("\n"); + + return ( + date: date, + body: line, + ); + }, + ) + .where((e) => e.date != null && e.body.isNotEmpty) + .toList() + ..sort((a, b) => b.date!.compareTo(a.date!)); + } + + @override + Widget build(BuildContext context) { + final logs = useState>([]); + final rawLogs = useRef(""); + final path = useRef(null); + + useEffect(() { + final timer = Timer.periodic(const Duration(seconds: 5), (t) async { + path.value ??= await getLogsPath(); + final raw = await path.value!.readAsString(); + final hasChanged = rawLogs.value != raw; + rawLogs.value = raw; + if (hasChanged) logs.value = parseLogs(rawLogs.value); + }); + + return () { + timer.cancel(); + }; + }, []); + + return Scaffold( + appBar: PageWindowTitleBar( + title: Text(context.l10n.logs), + leading: const BackButton(), + actions: [ + IconButton( + icon: const Icon(SpotubeIcons.clipboard), + iconSize: 16, + onPressed: () async { + await Clipboard.setData(ClipboardData(text: rawLogs.value)); + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(context.l10n.copied_to_clipboard("")), + ), + ); + } + }, + ), + ], + ), + body: SafeArea( + child: ListView.builder( + itemCount: logs.value.length, + itemBuilder: (context, index) { + final log = logs.value[index]; + return Stack( + children: [ + SectionCardWithHeading( + heading: log.date.toString(), + children: [ + Padding( + padding: const EdgeInsets.all(12.0), + child: SelectableText(log.body), + ), + ], + ), + Positioned( + right: 10, + top: 0, + child: IconButton( + icon: const Icon(SpotubeIcons.clipboard), + onPressed: () async { + await Clipboard.setData( + ClipboardData(text: log.body), + ); + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + context.l10n.copied_to_clipboard( + log.date.toString(), + ), + ), + ), + ); + } + }, + ), + ), + ], + ); + }, + ), + ), + ); + } +} diff --git a/lib/pages/settings/settings.dart b/lib/pages/settings/settings.dart index 39e7d2bb..ebcfb58d 100644 --- a/lib/pages/settings/settings.dart +++ b/lib/pages/settings/settings.dart @@ -406,6 +406,19 @@ class SettingsPage extends HookConsumerWidget { ), ], ), + SectionCardWithHeading( + heading: context.l10n.developers, + children: [ + ListTile( + leading: const Icon(SpotubeIcons.logs), + title: Text(context.l10n.logs), + trailing: const Icon(SpotubeIcons.angleRight), + onTap: () { + GoRouter.of(context).push("/settings/logs"); + }, + ) + ], + ), SectionCardWithHeading( heading: context.l10n.about, children: [ diff --git a/lib/utils/custom_toast_handler.dart b/lib/utils/custom_toast_handler.dart deleted file mode 100644 index fa725962..00000000 --- a/lib/utils/custom_toast_handler.dart +++ /dev/null @@ -1,58 +0,0 @@ -import 'package:catcher/model/platform_type.dart'; -import 'package:catcher/model/report.dart'; -import 'package:catcher/model/report_handler.dart'; -import 'package:flutter/material.dart'; -import 'package:motion_toast/motion_toast.dart'; -import 'package:spotube/collections/spotube_icons.dart'; -import 'package:spotube/extensions/context.dart'; - -class CustomToastHandler extends ReportHandler { - CustomToastHandler(); - - @override - Future handle(Report error, BuildContext? context) async { - final theme = Theme.of(context!); - - MotionToast( - primaryColor: theme.colorScheme.errorContainer, - icon: SpotubeIcons.error, - title: Text( - context.l10n.something_went_wrong, - style: theme.textTheme.bodyMedium?.copyWith( - fontWeight: FontWeight.bold, - color: theme.colorScheme.onError, - ), - ), - description: Text( - error.error.toString(), - style: theme.textTheme.bodyMedium?.copyWith( - color: theme.colorScheme.onError, - ), - ), - dismissable: true, - toastDuration: const Duration(seconds: 5), - borderRadius: 10, - ).show(context); - return true; - } - - @override - List getSupportedPlatforms() => [ - PlatformType.android, - PlatformType.iOS, - PlatformType.web, - PlatformType.linux, - PlatformType.macOS, - PlatformType.windows, - ]; - - @override - bool isContextRequired() { - return true; - } - - @override - bool shouldHandleWhenRejected() { - return false; - } -} diff --git a/pubspec.lock b/pubspec.lock index 6cbba78f..14f36478 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1134,14 +1134,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.4" - motion_toast: - dependency: "direct main" - description: - name: motion_toast - sha256: f33fad8264d6d5359e41f2027d2d833614401c3983102e8f0aa13ccbbdcdeecd - url: "https://pub.dev" - source: hosted - version: "2.6.8" mutex: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 66fb8eac..880f68d6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -102,7 +102,6 @@ dependencies: device_preview: ^1.1.0 media_kit_native_event_loop: ^1.0.4 dbus: ^0.7.8 - motion_toast: ^2.6.8 background_downloader: ^7.4.0 dev_dependencies: