feat: add embedded and external media players
This commit is contained in:
@@ -2,12 +2,14 @@ import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
import 'package:media_kit/media_kit.dart';
|
||||
import 'core/storage/storage_manager.dart';
|
||||
import 'core/http/dio_client.dart';
|
||||
import 'app/app.dart';
|
||||
|
||||
void main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
MediaKit.ensureInitialized();
|
||||
|
||||
// Init Hive for persistent storage
|
||||
await StorageManager.init();
|
||||
|
||||
@@ -11,6 +11,7 @@ import '../models/media_library.dart';
|
||||
import '../providers/auth_provider.dart';
|
||||
import '../providers/file_provider.dart';
|
||||
import '../providers/media_library_provider.dart';
|
||||
import '../widgets/media_player_dialog.dart';
|
||||
|
||||
enum _MediaWallFilter { all, movies, series, collections, unmatched }
|
||||
|
||||
@@ -445,9 +446,14 @@ class _MediaLibraryPageState extends ConsumerState<MediaLibraryPage> {
|
||||
onBack: () => setState(() => _detailWork = null),
|
||||
onDownload: (item) =>
|
||||
ref.read(fileProvider.notifier).downloadFile(item.file),
|
||||
onPlay: (item) => ref.read(fileProvider.notifier).playFile(item.file),
|
||||
onPlayInBrowser: (item) =>
|
||||
ref.read(fileProvider.notifier).playInBrowser(item.file),
|
||||
onPlay: (item) => showShadDialog(
|
||||
context: context,
|
||||
builder: (_) => MediaPlayerDialog(file: item.file),
|
||||
),
|
||||
onExternalPlay: (item) => showShadDialog(
|
||||
context: context,
|
||||
builder: (_) => ExternalPlayerDialog(file: item.file),
|
||||
),
|
||||
onManualMatch: () => _showManualTMDBMatch(current ?? _detailWork!),
|
||||
onRescan: state.isScanning
|
||||
? null
|
||||
@@ -1783,7 +1789,7 @@ class _MediaDetailPanel extends ConsumerStatefulWidget {
|
||||
final VoidCallback onBack;
|
||||
final ValueChanged<MediaLibraryItem> onDownload;
|
||||
final ValueChanged<MediaLibraryItem> onPlay;
|
||||
final ValueChanged<MediaLibraryItem> onPlayInBrowser;
|
||||
final ValueChanged<MediaLibraryItem> onExternalPlay;
|
||||
final VoidCallback onManualMatch;
|
||||
final VoidCallback? onRescan;
|
||||
|
||||
@@ -1792,7 +1798,7 @@ class _MediaDetailPanel extends ConsumerStatefulWidget {
|
||||
required this.onBack,
|
||||
required this.onDownload,
|
||||
required this.onPlay,
|
||||
required this.onPlayInBrowser,
|
||||
required this.onExternalPlay,
|
||||
required this.onManualMatch,
|
||||
this.onRescan,
|
||||
});
|
||||
@@ -1848,9 +1854,9 @@ class _MediaDetailPanelState extends ConsumerState<_MediaDetailPanel> {
|
||||
child: const Text('播放'),
|
||||
),
|
||||
ShadButton.outline(
|
||||
onPressed: () => widget.onPlayInBrowser(_resource),
|
||||
leading: const Icon(Icons.open_in_browser_rounded, size: 16),
|
||||
child: const Text('浏览器播放'),
|
||||
onPressed: () => widget.onExternalPlay(_resource),
|
||||
leading: const Icon(Icons.launch_rounded, size: 16),
|
||||
child: const Text('外部播放器'),
|
||||
),
|
||||
ShadButton.outline(
|
||||
onPressed: () => widget.onDownload(_resource),
|
||||
@@ -2050,9 +2056,9 @@ class _MediaDetailPanelState extends ConsumerState<_MediaDetailPanel> {
|
||||
child: const Text('播放'),
|
||||
),
|
||||
ShadContextMenuItem.inset(
|
||||
leading: const Icon(LucideIcons.externalLink, size: 16),
|
||||
onPressed: () => widget.onPlayInBrowser(resource),
|
||||
child: const Text('浏览器播放'),
|
||||
leading: const Icon(LucideIcons.monitorPlay, size: 16),
|
||||
onPressed: () => widget.onExternalPlay(resource),
|
||||
child: const Text('外部播放器'),
|
||||
),
|
||||
ShadContextMenuItem.inset(
|
||||
leading: const Icon(LucideIcons.download, size: 16),
|
||||
|
||||
@@ -10,6 +10,13 @@ import '../models/cloud_file.dart';
|
||||
|
||||
enum FileSort { name, size, modifiedAt, createdAt, type }
|
||||
|
||||
class ExternalPlayer {
|
||||
final String name;
|
||||
final String bundleID;
|
||||
|
||||
const ExternalPlayer(this.name, this.bundleID);
|
||||
}
|
||||
|
||||
extension FileSortExt on FileSort {
|
||||
String get title {
|
||||
switch (this) {
|
||||
@@ -655,14 +662,65 @@ class FileNotifier extends StateNotifier<FileState> {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> playInBrowser(CloudFile file) async {
|
||||
await _openRemoteFile(
|
||||
file,
|
||||
preparingMessage: '正在准备浏览器播放…',
|
||||
completedMessage: '已在外部浏览器打开播放链接',
|
||||
);
|
||||
static const supportedExternalPlayers = [
|
||||
ExternalPlayer('IINA', 'com.colliderli.iina'),
|
||||
ExternalPlayer('VLC', 'org.videolan.vlc'),
|
||||
ExternalPlayer('Infuse', 'com.firecore.Infuse'),
|
||||
ExternalPlayer('nPlayer', 'com.nplayer.nplayer'),
|
||||
ExternalPlayer('Movist Pro', 'com.movist.MovistPro'),
|
||||
ExternalPlayer('VidHub', 'com.mac.utility.media.hub'),
|
||||
ExternalPlayer('Forward', 'flux.inchmade.app'),
|
||||
ExternalPlayer('SenPlayer', 'com.wuziqi.SenPlayer'),
|
||||
ExternalPlayer('PotPlayer', 'com.kakao.PotPlayer'),
|
||||
ExternalPlayer('mpv', 'io.mpv'),
|
||||
];
|
||||
|
||||
Future<List<ExternalPlayer>> availableExternalPlayers() async {
|
||||
if (!Platform.isMacOS) return const [];
|
||||
final installed = <ExternalPlayer>[];
|
||||
for (final player in supportedExternalPlayers) {
|
||||
try {
|
||||
final result = await Process.run('/usr/bin/open', ['-Ra', player.name]);
|
||||
if (result.exitCode == 0) installed.add(player);
|
||||
} catch (_) {
|
||||
// A missing application is expected and should not affect playback.
|
||||
}
|
||||
}
|
||||
return installed;
|
||||
}
|
||||
|
||||
Future<void> playWithExternalPlayer(
|
||||
CloudFile file, [
|
||||
ExternalPlayer? player,
|
||||
]) async {
|
||||
if (_api == null) return;
|
||||
try {
|
||||
state = state.copyWith(
|
||||
statusMessage: player == null ? '正在准备外部播放…' : '正在使用 ${player.name} 播放…',
|
||||
);
|
||||
final url = await _resolveOpenUrl(file);
|
||||
if (Platform.isMacOS && player != null) {
|
||||
final result = await Process.run('/usr/bin/open', [
|
||||
'-b',
|
||||
player.bundleID,
|
||||
url.toString(),
|
||||
]);
|
||||
if (result.exitCode != 0) {
|
||||
throw Exception('无法启动 ${player.name}:${result.stderr}');
|
||||
}
|
||||
} else if (!await launchUrl(url, mode: LaunchMode.externalApplication)) {
|
||||
throw Exception('无法调用系统默认外部播放器');
|
||||
}
|
||||
state = state.copyWith(
|
||||
statusMessage: player == null ? '已交给外部播放器' : '已交给 ${player.name}',
|
||||
);
|
||||
} catch (e) {
|
||||
state = state.copyWith(errorMessage: e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
Future<Uri> playbackUrl(CloudFile file) => _resolveOpenUrl(file);
|
||||
|
||||
Future<void> _openRemoteFile(
|
||||
CloudFile file, {
|
||||
required String preparingMessage,
|
||||
|
||||
@@ -0,0 +1,190 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:media_kit/media_kit.dart';
|
||||
import 'package:media_kit_video/media_kit_video.dart';
|
||||
import 'package:shadcn_ui/shadcn_ui.dart';
|
||||
|
||||
import '../models/cloud_file.dart';
|
||||
import '../providers/file_provider.dart';
|
||||
|
||||
class MediaPlayerDialog extends ConsumerStatefulWidget {
|
||||
final CloudFile file;
|
||||
|
||||
const MediaPlayerDialog({super.key, required this.file});
|
||||
|
||||
@override
|
||||
ConsumerState<MediaPlayerDialog> createState() => _MediaPlayerDialogState();
|
||||
}
|
||||
|
||||
class _MediaPlayerDialogState extends ConsumerState<MediaPlayerDialog> {
|
||||
late final Player _player;
|
||||
late final VideoController _controller;
|
||||
String? _error;
|
||||
bool _loading = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_player = Player();
|
||||
_controller = VideoController(_player);
|
||||
Future.microtask(_open);
|
||||
}
|
||||
|
||||
Future<void> _open() async {
|
||||
try {
|
||||
final url = await ref
|
||||
.read(fileProvider.notifier)
|
||||
.playbackUrl(widget.file);
|
||||
await _player.open(Media(url.toString()), play: true);
|
||||
} catch (error) {
|
||||
_error = error.toString();
|
||||
} finally {
|
||||
if (mounted) setState(() => _loading = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_player.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cs = ShadTheme.of(context).colorScheme;
|
||||
return ShadDialog(
|
||||
title: Text(
|
||||
widget.file.name,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
description: const Text('内置播放器'),
|
||||
actions: [
|
||||
ShadButton.outline(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('关闭'),
|
||||
),
|
||||
],
|
||||
child: SizedBox(
|
||||
width: 960,
|
||||
height: 610,
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
child: ColoredBox(
|
||||
color: Colors.black,
|
||||
child: _loading
|
||||
? const Center(child: ShadProgress())
|
||||
: _error != null
|
||||
? Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Text(
|
||||
'无法播放:$_error',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(color: cs.destructive),
|
||||
),
|
||||
),
|
||||
)
|
||||
: Video(controller: _controller),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ExternalPlayerDialog extends ConsumerStatefulWidget {
|
||||
final CloudFile file;
|
||||
|
||||
const ExternalPlayerDialog({super.key, required this.file});
|
||||
|
||||
@override
|
||||
ConsumerState<ExternalPlayerDialog> createState() =>
|
||||
_ExternalPlayerDialogState();
|
||||
}
|
||||
|
||||
class _ExternalPlayerDialogState extends ConsumerState<ExternalPlayerDialog> {
|
||||
late final Future<List<ExternalPlayer>> _players;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_players = ref.read(fileProvider.notifier).availableExternalPlayers();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cs = ShadTheme.of(context).colorScheme;
|
||||
return ShadDialog(
|
||||
title: const Text('选择外部播放器'),
|
||||
description: Text(
|
||||
widget.file.name,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
actions: [
|
||||
ShadButton.outline(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('取消'),
|
||||
),
|
||||
],
|
||||
child: SizedBox(
|
||||
width: 420,
|
||||
child: FutureBuilder<List<ExternalPlayer>>(
|
||||
future: _players,
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData) {
|
||||
return const SizedBox(
|
||||
height: 100,
|
||||
child: Center(child: ShadProgress()),
|
||||
);
|
||||
}
|
||||
final players = snapshot.data!;
|
||||
if (players.isEmpty) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'未发现支持的外部播放器',
|
||||
style: TextStyle(color: cs.mutedForeground),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
ShadButton.outline(
|
||||
onPressed: () {
|
||||
ref
|
||||
.read(fileProvider.notifier)
|
||||
.playWithExternalPlayer(widget.file);
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
leading: const Icon(Icons.play_arrow_rounded, size: 16),
|
||||
child: const Text('使用系统默认播放器'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
return Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: [
|
||||
for (final player in players)
|
||||
ShadButton.outline(
|
||||
onPressed: () {
|
||||
ref
|
||||
.read(fileProvider.notifier)
|
||||
.playWithExternalPlayer(widget.file, player);
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
leading: const Icon(
|
||||
Icons.play_circle_outline_rounded,
|
||||
size: 16,
|
||||
),
|
||||
child: Text(player.name),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -9,12 +9,15 @@ import desktop_drop
|
||||
import device_info_plus
|
||||
import file_picker
|
||||
import file_selector_macos
|
||||
import media_kit_libs_macos_video
|
||||
import media_kit_video
|
||||
import package_info_plus
|
||||
import pasteboard
|
||||
import screen_retriever_macos
|
||||
import share_plus
|
||||
import sqflite_darwin
|
||||
import url_launcher_macos
|
||||
import wakelock_plus
|
||||
import window_manager
|
||||
|
||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
@@ -22,11 +25,14 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
|
||||
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
|
||||
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
|
||||
MediaKitLibsMacosVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitLibsMacosVideoPlugin"))
|
||||
MediaKitVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitVideoPlugin"))
|
||||
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
|
||||
PasteboardPlugin.register(with: registry.registrar(forPlugin: "PasteboardPlugin"))
|
||||
ScreenRetrieverMacosPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverMacosPlugin"))
|
||||
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
|
||||
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
|
||||
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
||||
WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin"))
|
||||
WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin"))
|
||||
}
|
||||
|
||||
@@ -805,6 +805,70 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.13.0"
|
||||
media_kit:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: media_kit
|
||||
sha256: ae9e79597500c7ad6083a3c7b7b7544ddabfceacce7ae5c9709b0ec16a5d6643
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.6"
|
||||
media_kit_libs_android_video:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: media_kit_libs_android_video
|
||||
sha256: "3f6274e5ab2de512c286a25c327288601ee445ed8ac319e0ef0b66148bd8f76c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.8"
|
||||
media_kit_libs_ios_video:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: media_kit_libs_ios_video
|
||||
sha256: b5382994eb37a4564c368386c154ad70ba0cc78dacdd3fb0cd9f30db6d837991
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.4"
|
||||
media_kit_libs_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: media_kit_libs_linux
|
||||
sha256: "2b473399a49ec94452c4d4ae51cfc0f6585074398d74216092bf3d54aac37ecf"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.1"
|
||||
media_kit_libs_macos_video:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: media_kit_libs_macos_video
|
||||
sha256: f26aa1452b665df288e360393758f84b911f70ffb3878032e1aabba23aa1032d
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.4"
|
||||
media_kit_libs_video:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: media_kit_libs_video
|
||||
sha256: "2b235b5dac79c6020e01eef5022c6cc85fedc0df1738aadc6ea489daa12a92a9"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.7"
|
||||
media_kit_libs_windows_video:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: media_kit_libs_windows_video
|
||||
sha256: dff76da2778729ab650229e6b4ec6ec111eb5151431002cbd7ea304ff1f112ab
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.11"
|
||||
media_kit_video:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: media_kit_video
|
||||
sha256: afaa509e7b7e0bf247557a3a740cde903a52c34ace9810f94500e127bd7b043d
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.1"
|
||||
meta:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1125,6 +1189,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.28.0"
|
||||
safe_local_storage:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: safe_local_storage
|
||||
sha256: "7483b3d5e8976f0bd263647c03b96131ee8e43f48b56fa8a8ec459e8515d74b0"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.4"
|
||||
screen_retriever:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1450,6 +1522,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
uri_parser:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: uri_parser
|
||||
sha256: "051c62e5f693de98ca9f130ee707f8916e2266945565926be3ff20659f7853ce"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.2"
|
||||
url_launcher:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -1562,6 +1642,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "15.2.0"
|
||||
wakelock_plus:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: wakelock_plus
|
||||
sha256: "824c5bba0f800e86d32e57d3d1843c531f090005cc89d9a837933e6601093d53"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.6.1"
|
||||
wakelock_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: wakelock_plus_platform_interface
|
||||
sha256: b13f99e992e7ae6a152e16c5559d3c07ff445b13330192662494e614ca3e7d7b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.5.1"
|
||||
watcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@@ -43,6 +43,9 @@ dependencies:
|
||||
image_picker: ^1.2.2
|
||||
qr_flutter: ^4.1.0
|
||||
desktop_drop: ^0.7.1
|
||||
media_kit: ^1.2.4
|
||||
media_kit_video: ^2.0.1
|
||||
media_kit_libs_video: ^1.0.6
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
|
||||
#include <desktop_drop/desktop_drop_plugin.h>
|
||||
#include <file_selector_windows/file_selector_windows.h>
|
||||
#include <media_kit_libs_windows_video/media_kit_libs_windows_video_plugin_c_api.h>
|
||||
#include <media_kit_video/media_kit_video_plugin_c_api.h>
|
||||
#include <pasteboard/pasteboard_plugin.h>
|
||||
#include <permission_handler_windows/permission_handler_windows_plugin.h>
|
||||
#include <screen_retriever_windows/screen_retriever_windows_plugin_c_api.h>
|
||||
@@ -20,6 +22,10 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||
registry->GetRegistrarForPlugin("DesktopDropPlugin"));
|
||||
FileSelectorWindowsRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("FileSelectorWindows"));
|
||||
MediaKitLibsWindowsVideoPluginCApiRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("MediaKitLibsWindowsVideoPluginCApi"));
|
||||
MediaKitVideoPluginCApiRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("MediaKitVideoPluginCApi"));
|
||||
PasteboardPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("PasteboardPlugin"));
|
||||
PermissionHandlerWindowsPluginRegisterWithRegistrar(
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
desktop_drop
|
||||
file_selector_windows
|
||||
media_kit_libs_windows_video
|
||||
media_kit_video
|
||||
pasteboard
|
||||
permission_handler_windows
|
||||
screen_retriever_windows
|
||||
|
||||
Reference in New Issue
Block a user