diff --git a/assets/icon/logo.ico b/assets/icon/logo.ico new file mode 100644 index 0000000..860c542 Binary files /dev/null and b/assets/icon/logo.ico differ diff --git a/lib/features/player/view/widgets/speed_selector.dart b/lib/features/player/view/widgets/speed_selector.dart index ab564b1..7e6ad89 100644 --- a/lib/features/player/view/widgets/speed_selector.dart +++ b/lib/features/player/view/widgets/speed_selector.dart @@ -28,7 +28,9 @@ class SpeedSelector extends HookConsumerWidget { // hook the onSpeedSelected function to the state useEffect( () { - onSpeedSelected(speedState.value); + Future.microtask(() { + onSpeedSelected(speedState.value); + }); return null; }, [speedState.value], diff --git a/lib/main.dart b/lib/main.dart index 2fc55f4..4d4c52d 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -14,19 +14,34 @@ import 'package:vaani/features/player/providers/audiobook_player.dart' import 'package:vaani/features/shake_detection/providers/shake_detector.dart'; import 'package:vaani/features/sleep_timer/providers/sleep_timer_provider.dart'; import 'package:vaani/generated/l10n.dart'; +import 'package:vaani/models/tray.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/settings/settings.dart'; +import 'package:vaani/shared/utils/utils.dart'; import 'package:vaani/theme/providers/system_theme_provider.dart'; import 'package:vaani/theme/providers/theme_from_cover_provider.dart'; import 'package:vaani/theme/theme.dart'; +import 'package:window_manager/window_manager.dart'; final appLogger = Logger(AppMetadata.appName); void main() async { WidgetsFlutterBinding.ensureInitialized(); + // 初始化窗口管理器 + if (Utils.isDesktop()) { + await windowManager.ensureInitialized(); + final windowOptions = WindowOptions( + minimumSize: Size(1050, 700), + center: true, + skipTaskbar: false, + ); + await windowManager.waitUntilReadyToShow(windowOptions, () async { + await windowManager.setPreventClose(true); + }); + } // Configure the root Logger await initLogging(); @@ -39,7 +54,7 @@ void main() async { // run the app runApp( const ProviderScope( - child: _EagerInitialization(child: MyApp()), + child: _EagerInitialization(child: TrayFramework(MyApp())), ), ); } diff --git a/lib/models/tray.dart b/lib/models/tray.dart new file mode 100644 index 0000000..5d44e22 --- /dev/null +++ b/lib/models/tray.dart @@ -0,0 +1,130 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:tray_manager/tray_manager.dart'; +import 'package:vaani/features/player/providers/audiobook_player.dart'; +import 'package:vaani/settings/constants.dart'; +import 'package:vaani/shared/utils/utils.dart'; +import 'package:window_manager/window_manager.dart'; + +class TrayFramework extends ConsumerStatefulWidget { + final Widget child; + const TrayFramework(this.child, {super.key}); + + @override + ConsumerState createState() => _TrayFrameworkState(); +} + +class _TrayFrameworkState extends ConsumerState + with TrayListener, WindowListener { + @override + void initState() { + if (Utils.isDesktop()) { + windowManager.addListener(this); + _init(); + } + + super.initState(); + } + + @override + void dispose() { + trayManager.removeListener(this); + super.dispose(); + } + + void _init() async { + await trayManager.setIcon( + Utils.isWindows() ? 'assets/icon/logo.ico' : 'assets/icon/logo.png', + ); + await trayManager.setToolTip(AppMetadata.appName); + Menu menu = Menu( + items: [ + MenuItem( + key: 'show_window', + // label: 'Show Window', + label: '显示主窗口', + onClick: (menuItem) => windowManager.show(), + ), + MenuItem.separator(), + MenuItem( + key: 'play_pause', + label: '播放/暂停', + onClick: (menuItem) => + ref.read(audiobookPlayerProvider).togglePlayPause(), + ), + MenuItem( + key: 'previous', + label: '上一个', + onClick: (menuItem) => + ref.read(audiobookPlayerProvider).seekToPrevious(), + ), + MenuItem( + key: 'next', + label: '下一个', + onClick: (menuItem) => ref.read(audiobookPlayerProvider).seekToNext(), + ), + MenuItem.separator(), + MenuItem( + key: 'exit_app', + // label: 'Exit App', + label: '退出', + onClick: (menuItem) => windowManager.destroy(), + ), + ], + ); + await trayManager.setContextMenu(menu); + trayManager.addListener(this); + } + + @override + Widget build(BuildContext context) { + return widget.child; + } + + @override + void onTrayIconMouseDown() { + // do something, for example pop up the menu + // print('onTrayIconMouseDown'); + windowManager.show(); + } + + @override + void onTrayIconMouseUp() { + // do something, for example pop up the menu + // print('onTrayIconMouseUp'); + } + + @override + void onTrayIconRightMouseDown() { + // do something + // print('onTrayIconRightMouseDown'); + trayManager.popUpContextMenu(bringAppToFront: true); + } + + @override + void onTrayIconRightMouseUp() { + // do something + // print('onTrayIconRightMouseUp'); + } + + // @override + // void onTrayMenuItemClick(MenuItem menuItem) { + // print(menuItem.key); + // if (menuItem.key == 'show_window') { + // // do something + // } else if (menuItem.key == 'exit_app') { + // // do something + + // } else if (menuItem.key == 'play_pause'){ + + // } + // } + + @override + void onWindowClose() async { + final isPreventClose = await windowManager.isPreventClose(); + if (isPreventClose) { + windowManager.hide(); + } + } +} diff --git a/lib/shared/utils/utils.dart b/lib/shared/utils/utils.dart new file mode 100644 index 0000000..93a14d6 --- /dev/null +++ b/lib/shared/utils/utils.dart @@ -0,0 +1,40 @@ +import 'dart:io'; + +import 'package:flutter/foundation.dart'; + +class Utils { + static isAndroid() { + return !kIsWeb && Platform.isAndroid; + } + + static isIOS() { + return !kIsWeb && Platform.isIOS; + } + + static isMobile() { + return !kIsWeb && (Platform.isAndroid || Platform.isIOS); + } + + static isDesktop() { + if (kIsWeb) { + return false; + } + return Platform.isWindows || Platform.isLinux || Platform.isMacOS; + } + + static isWindows() { + return !kIsWeb && Platform.isWindows; + } + + static isMacos() { + return !kIsWeb && Platform.isMacOS; + } + + static isLinux() { + return !kIsWeb && Platform.isLinux; + } + + static isWeb() { + return kIsWeb; + } +} diff --git a/lib/shared/widgets/expandable_description.dart b/lib/shared/widgets/expandable_description.dart index 203c7b0..c79f6be 100644 --- a/lib/shared/widgets/expandable_description.dart +++ b/lib/shared/widgets/expandable_description.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:flutter_html/flutter_html.dart'; class ExpandableDescription extends HookWidget { const ExpandableDescription({ @@ -78,18 +79,28 @@ class ExpandableDescription extends HookWidget { padding: const EdgeInsets.symmetric(horizontal: 8.0), child: AnimatedSwitcher( duration: duration * 3, - child: isDescExpanded.value - ? Text( - style: textTheme.bodyMedium, - content, - maxLines: null, - ) - : Text( - style: textTheme.bodyMedium, - content, - maxLines: 3, - overflow: TextOverflow.ellipsis, - ), + // child: isDescExpanded.value + // ? Text( + // style: textTheme.bodyMedium, + // content, + // maxLines: null, + // ) + // : Text( + // style: textTheme.bodyMedium, + // content, + // maxLines: 3, + // overflow: TextOverflow.ellipsis, + // ), + child: Html( + data: '
$content
', + style: { + "div": Style( + maxLines: isDescExpanded.value ? null : 3, + textOverflow: TextOverflow.ellipsis, + fontStyle: textTheme.bodyMedium?.fontStyle, + ), + }, + ), ), ), diff --git a/lib/shared/widgets/shelves/home_shelf.dart b/lib/shared/widgets/shelves/home_shelf.dart index 9a59a54..34e492e 100644 --- a/lib/shared/widgets/shelves/home_shelf.dart +++ b/lib/shared/widgets/shelves/home_shelf.dart @@ -70,7 +70,7 @@ class SimpleHomeShelf extends HookConsumerWidget { ), // fix the height of the shelf as a percentage of the screen height SizedBox( - height: height ?? getDefaultShelfHeight(context, perCent: 0.5), + height: height ?? getDefaultShelfHeight(context, perCent: 0.45), child: ListView.separated( scrollDirection: Axis.horizontal, itemBuilder: (context, index) { diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index 606c5a6..a380286 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -9,7 +9,10 @@ #include #include #include +#include +#include #include +#include void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) dynamic_color_registrar = @@ -21,7 +24,16 @@ void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) media_kit_libs_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "MediaKitLibsLinuxPlugin"); media_kit_libs_linux_plugin_register_with_registrar(media_kit_libs_linux_registrar); + g_autoptr(FlPluginRegistrar) screen_retriever_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "ScreenRetrieverLinuxPlugin"); + screen_retriever_linux_plugin_register_with_registrar(screen_retriever_linux_registrar); + g_autoptr(FlPluginRegistrar) tray_manager_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "TrayManagerPlugin"); + tray_manager_plugin_register_with_registrar(tray_manager_registrar); g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); + g_autoptr(FlPluginRegistrar) window_manager_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "WindowManagerPlugin"); + window_manager_plugin_register_with_registrar(window_manager_registrar); } diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 6023074..30a795e 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -6,7 +6,10 @@ list(APPEND FLUTTER_PLUGIN_LIST dynamic_color isar_flutter_libs media_kit_libs_linux + screen_retriever_linux + tray_manager url_launcher_linux + window_manager ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index e83c5d8..435043d 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -14,9 +14,12 @@ import isar_flutter_libs import just_audio import package_info_plus import path_provider_foundation +import screen_retriever_macos import share_plus import sqflite_darwin +import tray_manager import url_launcher_macos +import window_manager func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { AudioServicePlugin.register(with: registry.registrar(forPlugin: "AudioServicePlugin")) @@ -28,7 +31,10 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { JustAudioPlugin.register(with: registry.registrar(forPlugin: "JustAudioPlugin")) FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) + ScreenRetrieverMacosPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverMacosPlugin")) SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) + TrayManagerPlugin.register(with: registry.registrar(forPlugin: "TrayManagerPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) + WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin")) } diff --git a/pubspec.lock b/pubspec.lock index fa15a52..31f219d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -318,6 +318,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.6" + csslib: + dependency: transitive + description: + name: csslib + sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e" + url: "https://pub.dev" + source: hosted + version: "1.0.2" cupertino_icons: dependency: "direct main" description: @@ -499,6 +507,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.21.3+1" + flutter_html: + dependency: "direct main" + description: + name: flutter_html + sha256: "38a2fd702ffdf3243fb7441ab58aa1bc7e6922d95a50db76534de8260638558d" + url: "https://pub.dev" + source: hosted + version: "3.0.0" flutter_launcher_icons: dependency: "direct dev" description: @@ -642,6 +658,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.3.0" + html: + dependency: transitive + description: + name: html + sha256: "6d1264f2dffa1b1101c25a91dff0dc2daee4c18e87cd8538729773c073dbf602" + url: "https://pub.dev" + source: hosted + version: "0.15.6" http: dependency: transitive description: @@ -811,6 +835,14 @@ packages: url: "https://pub.dev" source: hosted version: "5.1.1" + list_counter: + dependency: transitive + description: + name: list_counter + sha256: c447ae3dfcd1c55f0152867090e67e219d42fe6d4f2807db4bbe8b8d69912237 + url: "https://pub.dev" + source: hosted + version: "1.0.2" list_wheel_scroll_view_nls: dependency: "direct main" description: @@ -899,6 +931,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.9" + menu_base: + dependency: transitive + description: + name: menu_base + sha256: "820368014a171bd1241030278e6c2617354f492f5c703d7b7d4570a6b8b84405" + url: "https://pub.dev" + source: hosted + version: "0.1.1" meta: dependency: transitive description: @@ -1180,6 +1220,46 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.1" + screen_retriever: + dependency: transitive + description: + name: screen_retriever + sha256: "570dbc8e4f70bac451e0efc9c9bb19fa2d6799a11e6ef04f946d7886d2e23d0c" + url: "https://pub.dev" + source: hosted + version: "0.2.0" + screen_retriever_linux: + dependency: transitive + description: + name: screen_retriever_linux + sha256: f7f8120c92ef0784e58491ab664d01efda79a922b025ff286e29aa123ea3dd18 + url: "https://pub.dev" + source: hosted + version: "0.2.0" + screen_retriever_macos: + dependency: transitive + description: + name: screen_retriever_macos + sha256: "71f956e65c97315dd661d71f828708bd97b6d358e776f1a30d5aa7d22d78a149" + url: "https://pub.dev" + source: hosted + version: "0.2.0" + screen_retriever_platform_interface: + dependency: transitive + description: + name: screen_retriever_platform_interface + sha256: ee197f4581ff0d5608587819af40490748e1e39e648d7680ecf95c05197240c0 + url: "https://pub.dev" + source: hosted + version: "0.2.0" + screen_retriever_windows: + dependency: transitive + description: + name: screen_retriever_windows + sha256: "449ee257f03ca98a57288ee526a301a430a344a161f9202b4fcc38576716fe13" + url: "https://pub.dev" + source: hosted + version: "0.2.0" scroll_loop_auto_scroll: dependency: "direct main" description: @@ -1251,6 +1331,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.0" + shortid: + dependency: transitive + description: + name: shortid + sha256: d0b40e3dbb50497dad107e19c54ca7de0d1a274eb9b4404991e443dadb9ebedb + url: "https://pub.dev" + source: hosted + version: "0.1.2" sky_engine: dependency: transitive description: flutter @@ -1416,6 +1504,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.2" + tray_manager: + dependency: "direct main" + description: + name: tray_manager + sha256: c5fd83b0ae4d80be6eaedfad87aaefab8787b333b8ebd064b0e442a81006035b + url: "https://pub.dev" + source: hosted + version: "0.5.2" typed_data: dependency: transitive description: @@ -1592,6 +1688,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.0" + window_manager: + dependency: "direct main" + description: + name: window_manager + sha256: "7eb6d6c4164ec08e1bf978d6e733f3cebe792e2a23fb07cbca25c2872bfdbdcd" + url: "https://pub.dev" + source: hosted + version: "0.5.1" xdg_directories: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 24c0e6b..4c36578 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -96,8 +96,11 @@ dependencies: shimmer: ^3.0.0 url_launcher: ^6.2.6 vibration: ^3.1.3 + flutter_html: ^3.0.0 flutter_localizations: sdk: flutter + window_manager: ^0.5.1 + tray_manager: ^0.5.2 dev_dependencies: build_runner: ^2.4.9 custom_lint: ^0.7.0 @@ -126,6 +129,7 @@ flutter: - assets/sounds/ - assets/images/ - assets/fonts/ + - assets/icon/ # - images/a_dot_burr.jpeg # - images/a_dot_ham.jpeg # An image asset can refer to one or more resolution-specific "variants", see diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index de21556..4f18e92 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -10,8 +10,11 @@ #include #include #include +#include #include +#include #include +#include void RegisterPlugins(flutter::PluginRegistry* registry) { DynamicColorPluginCApiRegisterWithRegistrar( @@ -22,8 +25,14 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("MediaKitLibsWindowsAudioPluginCApi")); PermissionHandlerWindowsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin")); + ScreenRetrieverWindowsPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("ScreenRetrieverWindowsPluginCApi")); SharePlusWindowsPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi")); + TrayManagerPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("TrayManagerPlugin")); UrlLauncherWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("UrlLauncherWindows")); + WindowManagerPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("WindowManagerPlugin")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 13d504d..ed05458 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -7,8 +7,11 @@ list(APPEND FLUTTER_PLUGIN_LIST isar_flutter_libs media_kit_libs_windows_audio permission_handler_windows + screen_retriever_windows share_plus + tray_manager url_launcher_windows + window_manager ) list(APPEND FLUTTER_FFI_PLUGIN_LIST