加入在线观看的功能

This commit is contained in:
DelLevin-Home
2026-08-12 11:50:50 +08:00
parent ce5007bbaa
commit 05c3eaeed5
11 changed files with 833 additions and 193 deletions

View File

@@ -12,6 +12,7 @@ import 'package:sqflite_common_ffi/sqflite_ffi.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart'; import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:window_manager/window_manager.dart'; import 'package:window_manager/window_manager.dart';
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
import 'package:media_kit/media_kit.dart';
import 'pages/home/home_page.dart'; import 'pages/home/home_page.dart';
import 'utils/theme/app_theme.dart'; import 'utils/theme/app_theme.dart';
import 'utils/app_router.dart'; import 'utils/app_router.dart';
@@ -29,6 +30,7 @@ WebViewEnvironment? windowsWebViewEnvironment;
void main() async { void main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
MediaKit.ensureInitialized();
// Windows 桌面:使用 FFI 初始化 sqflite // Windows 桌面:使用 FFI 初始化 sqflite
if (Platform.isWindows) { if (Platform.isWindows) {
sqfliteFfiInit(); sqfliteFfiInit();

View File

@@ -1,7 +1,10 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:media_kit/media_kit.dart';
import 'package:media_kit_video/media_kit_video.dart';
import 'package:path/path.dart' as p; import 'package:path/path.dart' as p;
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
@@ -21,7 +24,8 @@ class MovieDetailPage extends StatefulWidget {
State<MovieDetailPage> createState() => _MovieDetailPageState(); State<MovieDetailPage> createState() => _MovieDetailPageState();
} }
class _MovieDetailPageState extends State<MovieDetailPage> { class _MovieDetailPageState extends State<MovieDetailPage>
with WidgetsBindingObserver {
Map<String, dynamic>? _data; Map<String, dynamic>? _data;
List<Map<String, dynamic>> _staffList = []; List<Map<String, dynamic>> _staffList = [];
bool _loading = true; bool _loading = true;
@@ -32,12 +36,45 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
int _currentTab = 0; int _currentTab = 0;
int _detailStyle = 0; // 0: 紧凑, 1: 沉浸式 int _detailStyle = 0; // 0: 紧凑, 1: 沉浸式
// 播放 Tab 数据
bool _playLoading = false;
String? _playError;
List<String> _playSources = [];
List<List<({String name, String url})>> _playEpisodes = [];
Set<String> _playedEpisodes = {};
int _currentSource = 0;
int? _currentEpisode;
// 播放器状态
bool _isPlaying = false;
Player? _player;
VideoController? _videoController;
bool _playerCreating = false;
bool _isFullscreen = false;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
WidgetsBinding.instance.addObserver(this);
_load(); _load();
} }
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
_player?.dispose();
SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
super.dispose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.paused) {
_player?.pause();
}
}
Future<void> _load() async { Future<void> _load() async {
final token = UserPrefs().movieSearchToken; final token = UserPrefs().movieSearchToken;
try { try {
@@ -105,6 +142,127 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
}); });
} }
Future<void> _loadPlayInfo() async {
if (_playSources.isNotEmpty || _playLoading) return;
final token = UserPrefs().movieSearchToken;
setState(() {
_playLoading = true;
_playError = null;
});
try {
final url =
'${ServerConfig.vipBaseUrl}/api/movie/detail/plus?vodId=${widget.vodId}&token=$token';
final resp =
await http.get(Uri.parse(url)).timeout(const Duration(seconds: 10));
if (!mounted) return;
if (resp.statusCode == 200) {
final json_ = json.decode(resp.body);
if (json_['code'] == 0 && json_['data'] != null) {
final data = json_['data'] as Map<String, dynamic>;
final sources = (data['vod_play_from'] ?? '')
.toString()
.split(r'$$$')
.where((s) => s.trim().isNotEmpty)
.toList();
final urlGroups = (data['vod_play_url'] ?? '')
.toString()
.split(r'$$$');
final episodes = <List<({String name, String url})>>[];
for (var i = 0; i < sources.length; i++) {
final group =
i < urlGroups.length ? urlGroups[i] : '';
final eps = <({String name, String url})>[];
for (final raw in group.split('#')) {
final parts = raw.split('\$');
if (parts.length >= 2 && parts[1].trim().isNotEmpty) {
eps.add((name: parts[0].trim(), url: parts[1].trim()));
}
}
episodes.add(eps);
}
if (mounted) {
setState(() {
_playSources = sources;
_playEpisodes = episodes;
_playedEpisodes = UserPrefs().getPlayedEpisodes(widget.vodId).toSet();
_playLoading = false;
});
}
return;
}
}
if (mounted) {
setState(() {
_playError = '加载失败';
_playLoading = false;
});
}
} catch (_) {
if (mounted) {
setState(() {
_playError = '网络错误';
_playLoading = false;
});
}
}
}
Future<void> _startPlay(String url, int episodeIndex) async {
if (_playerCreating) return;
setState(() {
_playerCreating = true;
});
try {
if (_player == null) {
_player = Player();
_videoController = VideoController(_player!);
}
await _player!.open(Media(url));
if (!mounted) return;
final epName = _playEpisodes[_currentSource][episodeIndex].name;
await UserPrefs().addPlayedEpisode(widget.vodId, epName);
setState(() {
_isPlaying = true;
_currentEpisode = episodeIndex;
_playedEpisodes.add(epName);
_playerCreating = false;
});
} catch (_) {
if (mounted) {
ToastUtil.show(context, '播放失败');
setState(() {
_playerCreating = false;
});
}
}
}
void _closePlayer() {
_player?.pause();
setState(() {
_isPlaying = false;
});
}
Future<void> _enterFullscreen() async {
await SystemChrome.setPreferredOrientations([
DeviceOrientation.landscapeLeft,
DeviceOrientation.landscapeRight,
]);
await SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky);
setState(() {
_isFullscreen = true;
});
}
Future<void> _exitFullscreen() async {
await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
await SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
setState(() {
_isFullscreen = false;
});
}
void _checkLocal() { void _checkLocal() {
final name = _data?['vod_name'] ?? ''; final name = _data?['vod_name'] ?? '';
if (name.toString().isEmpty) return; if (name.toString().isEmpty) return;
@@ -278,11 +436,89 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
); );
} }
// ── 播放器区域 ──────────────────────────────────────────
Widget _buildPlayerArea(ColorScheme colors) {
return Column(children: [
SafeArea(
bottom: false,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
child: Row(children: [
GestureDetector(
onTap: () => Navigator.pop(context),
child: Container(
width: 36,
height: 36,
decoration: BoxDecoration(
color: colors.surfaceContainerHigh, shape: BoxShape.circle),
child: Icon(Icons.arrow_back,
size: 20, color: colors.onSurface),
),
),
const Spacer(),
GestureDetector(
onTap: _closePlayer,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 7),
decoration: BoxDecoration(
color: colors.surfaceContainerHigh,
borderRadius: BorderRadius.circular(18)),
child: Text('关闭播放器',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
color: colors.onSurface)),
),
),
]),
),
),
AspectRatio(
aspectRatio: 16 / 9,
child: _videoController != null
? MaterialVideoControlsTheme(
normal: const MaterialVideoControlsThemeData(
seekBarThumbColor: Color(0xFFFFFFFF),
seekBarPositionColor: Color(0xFFFFFFFF),
),
fullscreen: const MaterialVideoControlsThemeData(),
child: Video(
controller: _videoController!,
controls: MaterialVideoControls,
onEnterFullscreen: _enterFullscreen,
onExitFullscreen: _exitFullscreen,
),
)
: Container(color: Colors.black),
),
]);
}
Widget _buildFullscreenPlayer(ColorScheme colors) {
return Scaffold(
backgroundColor: Colors.black,
body: _videoController != null
? MaterialVideoControlsTheme(
normal: const MaterialVideoControlsThemeData(),
fullscreen: const MaterialVideoControlsThemeData(),
child: Video(
controller: _videoController!,
controls: MaterialVideoControls,
onEnterFullscreen: _enterFullscreen,
onExitFullscreen: _exitFullscreen,
),
)
: const SizedBox.shrink(),
);
}
// ── Build ────────────────────────────────────────────── // ── Build ──────────────────────────────────────────────
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme; final colors = Theme.of(context).colorScheme;
if (_isFullscreen && _isPlaying) return _buildFullscreenPlayer(colors);
return Scaffold( return Scaffold(
backgroundColor: colors.surface, backgroundColor: colors.surface,
floatingActionButton: floatingActionButton:
@@ -347,111 +583,113 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
return Scaffold( return Scaffold(
backgroundColor: colors.surface, backgroundColor: colors.surface,
body: Column(children: [ body: Column(children: [
// 头部:返回按钮 + 海报信息 // 头部:返回按钮 + 海报信息(播放时替换为播放器区域)
SafeArea( _isPlaying
bottom: false, ? _buildPlayerArea(colors)
child: Column(children: [ : SafeArea(
Padding( bottom: false,
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), child: Column(children: [
child: Row(children: [ Padding(
GestureDetector( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
onTap: () => Navigator.pop(context), child: Row(children: [
child: Container( GestureDetector(
width: 36, onTap: () => Navigator.pop(context),
height: 36, child: Container(
decoration: BoxDecoration( width: 36,
color: colors.surfaceContainerHigh, height: 36,
shape: BoxShape.circle), decoration: BoxDecoration(
child: Icon(Icons.arrow_back, color: colors.surfaceContainerHigh,
size: 20, color: colors.onSurface)), shape: BoxShape.circle),
), child: Icon(Icons.arrow_back,
const Spacer(), size: 20, color: colors.onSurface)),
GestureDetector(
onTap: () => setState(() => _detailStyle = _detailStyle == 0 ? 1 : 0),
child: Container(
width: 36,
height: 36,
decoration: BoxDecoration(
color: colors.surfaceContainerHigh,
shape: BoxShape.circle),
child: Icon(
_detailStyle == 0
? Icons.crop_landscape_rounded
: Icons.grid_view_rounded,
size: 18,
color: colors.onSurface)),
),
]),
),
Padding(
padding: const EdgeInsets.fromLTRB(16, 4, 16, 16),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ClipRRect(
borderRadius: BorderRadius.circular(10),
child: SizedBox(
width: 120,
height: 170,
child: pic.toString().isNotEmpty
? Image.network(pic,
fit: BoxFit.cover,
errorBuilder: (_, __, ___) =>
_posterPlaceholder(colors))
: _posterPlaceholder(colors),
), ),
), const Spacer(),
const SizedBox(width: 16), GestureDetector(
Expanded( onTap: () => setState(() => _detailStyle = _detailStyle == 0 ? 1 : 0),
child: Column( child: Container(
crossAxisAlignment: CrossAxisAlignment.start, width: 36,
children: [ height: 36,
Text(name, decoration: BoxDecoration(
style: TextStyle( color: colors.surfaceContainerHigh,
fontSize: 18, shape: BoxShape.circle),
fontWeight: FontWeight.w700, child: Icon(
color: colors.onSurface)), _detailStyle == 0
const SizedBox(height: 8), ? Icons.crop_landscape_rounded
if (score.toString().isNotEmpty && score != '0.0') ...[ : Icons.grid_view_rounded,
Row(children: [ size: 18,
Icon(Icons.star_rounded, size: 16, color: const Color(0xFFF59E0B)), color: colors.onSurface)),
const SizedBox(width: 3), ),
Text('$score', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: colors.onSurface)), ]),
Text(' /10', style: TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.3))), ),
]), Padding(
const SizedBox(height: 2), padding: const EdgeInsets.fromLTRB(16, 4, 16, 16),
Text('评分来源于网络资源收集,并非官方评分', style: TextStyle(fontSize: 10, color: colors.onSurface.withValues(alpha: 0.25))), child: Row(
const SizedBox(height: 8), crossAxisAlignment: CrossAxisAlignment.start,
], children: [
_endTag(isEnd), ClipRRect(
if (metaParts.isNotEmpty) ...[ borderRadius: BorderRadius.circular(10),
const SizedBox(height: 8), child: SizedBox(
Text(metaParts, width: 120,
style: TextStyle( height: 170,
fontSize: 12, child: pic.toString().isNotEmpty
color: colors.onSurface ? Image.network(pic,
.withValues(alpha: 0.5))), fit: BoxFit.cover,
], errorBuilder: (_, __, ___) =>
if (typeParts.isNotEmpty) ...[ _posterPlaceholder(colors))
const SizedBox(height: 3), : _posterPlaceholder(colors),
Text(typeParts, ),
style: TextStyle( ),
fontSize: 11, const SizedBox(width: 16),
color: colors.onSurface Expanded(
.withValues(alpha: 0.4)), child: Column(
maxLines: 1, crossAxisAlignment: CrossAxisAlignment.start,
overflow: TextOverflow.ellipsis), children: [
], Text(name,
if (_localMovie != null) ...[ style: TextStyle(
const SizedBox(height: 10), fontSize: 18,
_buildLocalStatus(colors), fontWeight: FontWeight.w700,
], color: colors.onSurface)),
]), const SizedBox(height: 8),
), if (score.toString().isNotEmpty && score != '0.0') ...[
]), Row(children: [
), Icon(Icons.star_rounded, size: 16, color: const Color(0xFFF59E0B)),
]), const SizedBox(width: 3),
), Text('$score', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: colors.onSurface)),
Text(' /10', style: TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.3))),
]),
const SizedBox(height: 2),
Text('评分来源于网络资源收集,并非官方评分', style: TextStyle(fontSize: 10, color: colors.onSurface.withValues(alpha: 0.25))),
const SizedBox(height: 8),
],
_endTag(isEnd),
if (metaParts.isNotEmpty) ...[
const SizedBox(height: 8),
Text(metaParts,
style: TextStyle(
fontSize: 12,
color: colors.onSurface
.withValues(alpha: 0.5))),
],
if (typeParts.isNotEmpty) ...[
const SizedBox(height: 3),
Text(typeParts,
style: TextStyle(
fontSize: 11,
color: colors.onSurface
.withValues(alpha: 0.4)),
maxLines: 1,
overflow: TextOverflow.ellipsis),
],
if (_localMovie != null) ...[
const SizedBox(height: 10),
_buildLocalStatus(colors),
],
]),
),
]),
),
]),
),
// Tab 栏 // Tab 栏
Container( Container(
decoration: BoxDecoration( decoration: BoxDecoration(
@@ -461,11 +699,16 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
child: Row(children: [ child: Row(children: [
_buildTabButton('概要', 0), _buildTabButton('概要', 0),
_buildTabButton('演职人员', 1), _buildTabButton('演职人员', 1),
if (UserPrefs().playbackUnlocked) _buildTabButton('在线播放', 2),
]), ]),
), ),
// Tab 内容 // Tab 内容
Expanded( Expanded(
child: _currentTab == 0 ? _buildOverview(colors) : _buildStaffTab(colors), child: _currentTab == 0
? _buildOverview(colors)
: _currentTab == 1
? _buildStaffTab(colors)
: _buildPlayTab(colors),
), ),
]), ]),
); );
@@ -489,95 +732,97 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
return Scaffold( return Scaffold(
backgroundColor: colors.surface, backgroundColor: colors.surface,
body: Column(children: [ body: Column(children: [
// 沉浸式头部 // 沉浸式头部(播放时替换为播放器区域)
Stack(children: [ _isPlaying
SizedBox( ? _buildPlayerArea(colors)
width: double.infinity, : Stack(children: [
height: 320, SizedBox(
child: pic.toString().isNotEmpty width: double.infinity,
? Image.network(pic, fit: BoxFit.cover, height: 320,
errorBuilder: (_, __, ___) => Container(color: colors.surfaceContainerHighest)) child: pic.toString().isNotEmpty
: Container(color: colors.surfaceContainerHighest, ? Image.network(pic, fit: BoxFit.cover,
child: Icon(Icons.movie_outlined, size: 64, color: colors.onSurface.withValues(alpha: 0.1))), errorBuilder: (_, __, ___) => Container(color: colors.surfaceContainerHighest))
), : Container(color: colors.surfaceContainerHighest,
Positioned.fill( child: Icon(Icons.movie_outlined, size: 64, color: colors.onSurface.withValues(alpha: 0.1))),
child: DecoratedBox(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Colors.transparent, Colors.black.withValues(alpha: 0.8)],
stops: const [0.35, 1.0],
), ),
), Positioned.fill(
), child: DecoratedBox(
),
SafeArea(
bottom: false,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
child: Row(children: [
GestureDetector(
onTap: () => Navigator.pop(context),
child: Container(
width: 36, height: 36,
decoration: BoxDecoration(color: Colors.black.withValues(alpha: 0.3), shape: BoxShape.circle),
child: const Icon(Icons.arrow_back, size: 20, color: Colors.white)),
),
const Spacer(),
GestureDetector(
onTap: () => setState(() => _detailStyle = 0),
child: Container(
width: 36, height: 36,
decoration: BoxDecoration(color: Colors.black.withValues(alpha: 0.3), shape: BoxShape.circle),
child: const Icon(Icons.grid_view_rounded, size: 18, color: Colors.white)),
),
]),
),
),
Positioned(
left: 16, right: 16, bottom: 18,
child: Column(crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [
Text(name, maxLines: 2, overflow: TextOverflow.ellipsis,
style: const TextStyle(fontSize: 22, fontWeight: FontWeight.w700, color: Colors.white)),
const SizedBox(height: 8),
Row(children: [
if (score.toString().isNotEmpty && score != '0.0') ...[
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.black.withValues(alpha: 0.3), gradient: LinearGradient(
borderRadius: BorderRadius.circular(6), begin: Alignment.topCenter,
), end: Alignment.bottomCenter,
child: Row( colors: [Colors.transparent, Colors.black.withValues(alpha: 0.8)],
mainAxisSize: MainAxisSize.min, stops: const [0.35, 1.0],
children: [ ),
Icon(Icons.star_rounded, size: 18, color: Colors.amber.shade400),
const SizedBox(width: 3),
Text('$score', style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w700, color: Colors.white)),
],
), ),
), ),
const SizedBox(width: 10), ),
], SafeArea(
_endTag(isEnd), bottom: false,
if (metaParts.isNotEmpty) ...[ child: Padding(
const SizedBox(width: 8), padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
Text(metaParts, style: TextStyle(fontSize: 12, color: Colors.white.withValues(alpha: 0.7))), child: Row(children: [
], GestureDetector(
onTap: () => Navigator.pop(context),
child: Container(
width: 36, height: 36,
decoration: BoxDecoration(color: Colors.black.withValues(alpha: 0.3), shape: BoxShape.circle),
child: const Icon(Icons.arrow_back, size: 20, color: Colors.white)),
),
const Spacer(),
GestureDetector(
onTap: () => setState(() => _detailStyle = 0),
child: Container(
width: 36, height: 36,
decoration: BoxDecoration(color: Colors.black.withValues(alpha: 0.3), shape: BoxShape.circle),
child: const Icon(Icons.grid_view_rounded, size: 18, color: Colors.white)),
),
]),
),
),
Positioned(
left: 16, right: 16, bottom: 18,
child: Column(crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [
Text(name, maxLines: 2, overflow: TextOverflow.ellipsis,
style: const TextStyle(fontSize: 22, fontWeight: FontWeight.w700, color: Colors.white)),
const SizedBox(height: 8),
Row(children: [
if (score.toString().isNotEmpty && score != '0.0') ...[
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Colors.black.withValues(alpha: 0.3),
borderRadius: BorderRadius.circular(6),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.star_rounded, size: 18, color: Colors.amber.shade400),
const SizedBox(width: 3),
Text('$score', style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w700, color: Colors.white)),
],
),
),
const SizedBox(width: 10),
],
_endTag(isEnd),
if (metaParts.isNotEmpty) ...[
const SizedBox(width: 8),
Text(metaParts, style: TextStyle(fontSize: 12, color: Colors.white.withValues(alpha: 0.7))),
],
]),
if (typeParts.isNotEmpty) ...[
const SizedBox(height: 4),
Text(typeParts, maxLines: 1, overflow: TextOverflow.ellipsis,
style: TextStyle(fontSize: 12, color: Colors.white.withValues(alpha: 0.5))),
],
if (_localMovie != null) ...[
const SizedBox(height: 8),
_buildLocalStatus(colors),
],
]),
),
]), ]),
if (typeParts.isNotEmpty) ...[
const SizedBox(height: 4),
Text(typeParts, maxLines: 1, overflow: TextOverflow.ellipsis,
style: TextStyle(fontSize: 12, color: Colors.white.withValues(alpha: 0.5))),
],
if (_localMovie != null) ...[
const SizedBox(height: 8),
_buildLocalStatus(colors),
],
]),
),
]),
// Tab 栏 // Tab 栏
Container( Container(
decoration: BoxDecoration( decoration: BoxDecoration(
@@ -586,11 +831,16 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
child: Row(children: [ child: Row(children: [
_buildTabButton('概要', 0), _buildTabButton('概要', 0),
_buildTabButton('演职人员', 1), _buildTabButton('演职人员', 1),
if (UserPrefs().playbackUnlocked) _buildTabButton('在线播放', 2),
]), ]),
), ),
// Tab 内容 // Tab 内容
Expanded( Expanded(
child: _currentTab == 0 ? _buildOverview(colors) : _buildStaffTab(colors), child: _currentTab == 0
? _buildOverview(colors)
: _currentTab == 1
? _buildStaffTab(colors)
: _buildPlayTab(colors),
), ),
]), ]),
); );
@@ -657,7 +907,15 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
final colors = Theme.of(context).colorScheme; final colors = Theme.of(context).colorScheme;
final selected = _currentTab == index; final selected = _currentTab == index;
return GestureDetector( return GestureDetector(
onTap: () => setState(() => _currentTab = index), onTap: () {
setState(() => _currentTab = index);
if (index == 2 &&
UserPrefs().playbackUnlocked &&
_playSources.isEmpty &&
!_playLoading) {
_loadPlayInfo();
}
},
child: Container( child: Container(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 11), padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 11),
decoration: BoxDecoration( decoration: BoxDecoration(
@@ -832,6 +1090,164 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
]); ]);
} }
// ── 播放 Tab ──────────────────────────────────────────
Widget _buildPlayTab(ColorScheme colors) {
if (!UserPrefs().playbackUnlocked) {
return Center(
child: Padding(
padding: const EdgeInsets.all(32),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.lock_outline,
size: 40, color: colors.onSurface.withValues(alpha: 0.2)),
const SizedBox(height: 12),
Text('播放功能未解锁',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: colors.onSurface.withValues(alpha: 0.5))),
const SizedBox(height: 6),
Text('请在增强搜索中输入验证码以解锁7天有效',
style: TextStyle(
fontSize: 12,
color: colors.onSurface.withValues(alpha: 0.35))),
],
),
),
);
}
if (_playLoading) {
return Center(
child: SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
color: colors.primary,
),
),
);
}
if (_playError != null) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.error_outline,
size: 40, color: colors.onSurface.withValues(alpha: 0.2)),
const SizedBox(height: 12),
Text(_playError!,
style: TextStyle(
fontSize: 13,
color: colors.onSurface.withValues(alpha: 0.4))),
const SizedBox(height: 12),
TextButton(
onPressed: _loadPlayInfo,
child: Text('重试', style: TextStyle(color: colors.primary)),
),
],
),
);
}
if (_playSources.isEmpty || _playEpisodes.isEmpty) {
return Center(
child: Text('暂无播放资源',
style: TextStyle(
fontSize: 13, color: colors.onSurface.withValues(alpha: 0.35))),
);
}
final episodes = _playEpisodes[_currentSource];
return Column(children: [
// 播放源选择器
SizedBox(
height: 44,
child: ListView.builder(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
itemCount: _playSources.length,
itemBuilder: (_, i) {
final selected = _currentSource == i;
return GestureDetector(
onTap: () {
setState(() {
_currentSource = i;
_currentEpisode = null;
});
},
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 6),
margin: const EdgeInsets.only(right: 8),
decoration: BoxDecoration(
color: selected
? colors.primaryContainer
: colors.surfaceContainerHighest,
borderRadius: BorderRadius.circular(8),
),
alignment: Alignment.center,
child: Text(_playSources[i],
style: TextStyle(
fontSize: 12,
fontWeight: selected ? FontWeight.w600 : FontWeight.w400,
color: selected
? colors.onPrimaryContainer
: colors.onSurface.withValues(alpha: 0.6),
)),
),
);
},
),
),
const SizedBox(height: 8),
// 剧集网格
Expanded(
child: GridView.builder(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 40),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 5,
crossAxisSpacing: 6,
mainAxisSpacing: 6,
childAspectRatio: 1.4,
),
itemCount: episodes.length,
itemBuilder: (_, i) {
final ep = episodes[i];
final isCurrent = _currentEpisode == i && _isPlaying;
final isPlayed = _playedEpisodes.contains(ep.name);
return GestureDetector(
onTap: () => _startPlay(ep.url, i),
child: Container(
decoration: BoxDecoration(
color: isCurrent
? colors.primary
: isPlayed
? colors.surfaceContainerHigh
: colors.surfaceContainerHighest,
borderRadius: BorderRadius.circular(8),
),
alignment: Alignment.center,
child: Text(ep.name,
style: TextStyle(
fontSize: 12,
fontWeight: isCurrent ? FontWeight.w600 : FontWeight.w400,
color: isCurrent
? colors.onPrimary
: isPlayed
? colors.onSurface.withValues(alpha: 0.5)
: colors.onSurface,
),
maxLines: 1,
overflow: TextOverflow.ellipsis),
),
);
},
),
),
]);
}
// ── 演职人员 Tab ────────────────────────────────────────── // ── 演职人员 Tab ──────────────────────────────────────────
Widget _buildStaffTab(ColorScheme colors) { Widget _buildStaffTab(ColorScheme colors) {

View File

@@ -99,6 +99,7 @@ class _OnlineSearchPageBodyState extends State<OnlineSearchPageBody> {
final q = _searchController.text.trim(); final q = _searchController.text.trim();
if (q.isEmpty) return; if (q.isEmpty) return;
_focusNode.unfocus(); _focusNode.unfocus();
_tryUnlockPlayback(q);
setState(() { setState(() {
_query = q; _query = q;
_hasSearched = true; _hasSearched = true;
@@ -123,6 +124,73 @@ class _OnlineSearchPageBodyState extends State<OnlineSearchPageBody> {
} }
} }
Future<void> _tryUnlockPlayback(String query) async {
try {
final url = '${ServerConfig.baseUrl}/api/enhance-video';
final resp =
await http.get(Uri.parse(url)).timeout(const Duration(seconds: 10));
if (!mounted) return;
if (resp.statusCode == 200) {
final json_ = json.decode(resp.body);
if (json_['code'] == 0 && json_['data'] == query) {
await UserPrefs().setPlaybackUnlockTime(DateTime.now().toIso8601String());
if (!mounted) return;
_showUnlockDialog();
}
}
} catch (_) {}
}
void _showUnlockDialog() {
final colors = Theme.of(context).colorScheme;
showDialog(
context: context,
builder: (ctx) => AlertDialog(
backgroundColor: colors.surface,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.check_circle_rounded,
size: 52,
color: const Color(0xFF16A34A),
),
const SizedBox(height: 14),
Text(
'播放功能已解锁',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: colors.onSurface,
),
),
const SizedBox(height: 6),
Text(
'有效期 7 天,可前往影视详情页观看',
style: TextStyle(
fontSize: 12,
color: colors.onSurface.withValues(alpha: 0.5),
),
textAlign: TextAlign.center,
),
],
),
actionsAlignment: MainAxisAlignment.center,
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx),
child: Text('知道了',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: colors.primary)),
),
],
),
);
}
Future<void> _searchMovies(String keyword, int page) async { Future<void> _searchMovies(String keyword, int page) async {
final token = UserPrefs().movieSearchToken; final token = UserPrefs().movieSearchToken;
if (token.isEmpty) { if (token.isEmpty) {

View File

@@ -312,6 +312,37 @@ class UserPrefs {
int get lastSearchTab => prefs.getInt('lastSearchTab') ?? 0; int get lastSearchTab => prefs.getInt('lastSearchTab') ?? 0;
Future<bool> setLastSearchTab(int value) => prefs.setInt('lastSearchTab', value); Future<bool> setLastSearchTab(int value) => prefs.setInt('lastSearchTab', value);
// ========== 播放解锁 ==========
/// 播放解锁时间ISO 字符串),解锁后 7 天有效
String? get playbackUnlockTime => prefs.getString('playbackUnlockTime');
Future<bool> setPlaybackUnlockTime(String value) => prefs.setString('playbackUnlockTime', value);
/// 当前是否已解锁播放7 天内有效)
bool get playbackUnlocked {
final t = playbackUnlockTime;
if (t == null) return false;
final dt = DateTime.tryParse(t);
if (dt == null) return false;
return DateTime.now().difference(dt).inDays < 7;
}
/// 影视已播放剧集记录key 为 "playback_played_<vodId>"value 为已播放 episode 名称 JSON 数组
List<String> getPlayedEpisodes(int vodId) {
final key = 'playback_played_$vodId';
return prefs.getStringList(key) ?? [];
}
Future<bool> addPlayedEpisode(int vodId, String episodeName) async {
final key = 'playback_played_$vodId';
final list = prefs.getStringList(key) ?? [];
if (!list.contains(episodeName)) {
list.add(episodeName);
return prefs.setStringList(key, list);
}
return false;
}
// ========== 版本更新 ========== // ========== 版本更新 ==========
/// 已忽略的版本号(不再提示更新) /// 已忽略的版本号(不再提示更新)

View File

@@ -8,8 +8,10 @@
#include <dynamic_color/dynamic_color_plugin.h> #include <dynamic_color/dynamic_color_plugin.h>
#include <file_selector_linux/file_selector_plugin.h> #include <file_selector_linux/file_selector_plugin.h>
#include <media_kit_video/media_kit_video_plugin.h>
#include <screen_retriever_linux/screen_retriever_linux_plugin.h> #include <screen_retriever_linux/screen_retriever_linux_plugin.h>
#include <url_launcher_linux/url_launcher_plugin.h> #include <url_launcher_linux/url_launcher_plugin.h>
#include <volume_controller/volume_controller_plugin.h>
#include <window_manager/window_manager_plugin.h> #include <window_manager/window_manager_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) { void fl_register_plugins(FlPluginRegistry* registry) {
@@ -19,12 +21,18 @@ void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) file_selector_linux_registrar = g_autoptr(FlPluginRegistrar) file_selector_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin"); fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin");
file_selector_plugin_register_with_registrar(file_selector_linux_registrar); file_selector_plugin_register_with_registrar(file_selector_linux_registrar);
g_autoptr(FlPluginRegistrar) media_kit_video_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "MediaKitVideoPlugin");
media_kit_video_plugin_register_with_registrar(media_kit_video_registrar);
g_autoptr(FlPluginRegistrar) screen_retriever_linux_registrar = g_autoptr(FlPluginRegistrar) screen_retriever_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "ScreenRetrieverLinuxPlugin"); fl_plugin_registry_get_registrar_for_plugin(registry, "ScreenRetrieverLinuxPlugin");
screen_retriever_linux_plugin_register_with_registrar(screen_retriever_linux_registrar); screen_retriever_linux_plugin_register_with_registrar(screen_retriever_linux_registrar);
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
g_autoptr(FlPluginRegistrar) volume_controller_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "VolumeControllerPlugin");
volume_controller_plugin_register_with_registrar(volume_controller_registrar);
g_autoptr(FlPluginRegistrar) window_manager_registrar = g_autoptr(FlPluginRegistrar) window_manager_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "WindowManagerPlugin"); fl_plugin_registry_get_registrar_for_plugin(registry, "WindowManagerPlugin");
window_manager_plugin_register_with_registrar(window_manager_registrar); window_manager_plugin_register_with_registrar(window_manager_registrar);

View File

@@ -5,8 +5,10 @@
list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_PLUGIN_LIST
dynamic_color dynamic_color
file_selector_linux file_selector_linux
media_kit_video
screen_retriever_linux screen_retriever_linux
url_launcher_linux url_launcher_linux
volume_controller
window_manager window_manager
) )

View File

@@ -10,12 +10,15 @@ import dynamic_color
import file_picker import file_picker
import file_selector_macos import file_selector_macos
import flutter_inappwebview_macos import flutter_inappwebview_macos
import media_kit_video
import package_info_plus import package_info_plus
import screen_retriever_macos import screen_retriever_macos
import share_plus import share_plus
import shared_preferences_foundation import shared_preferences_foundation
import sqflite_darwin import sqflite_darwin
import url_launcher_macos import url_launcher_macos
import volume_controller
import wakelock_plus
import window_manager import window_manager
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
@@ -24,11 +27,14 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin")) FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin")) InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin"))
MediaKitVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitVideoPlugin"))
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
ScreenRetrieverMacosPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverMacosPlugin")) ScreenRetrieverMacosPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverMacosPlugin"))
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
VolumeControllerPlugin.register(with: registry.registrar(forPlugin: "VolumeControllerPlugin"))
WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin"))
WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin")) WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin"))
} }

View File

@@ -97,6 +97,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.7" version: "3.0.7"
dbus:
dependency: transitive
description:
name: dbus
sha256: "0ce9b0a839e6dee59a37a623d2fc26a35bbbe6404213e419b0d6411023d62645"
url: "https://pub.dev"
source: hosted
version: "0.7.14"
device_info_plus: device_info_plus:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -565,6 +573,30 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.13.0" 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: "direct main"
description:
name: media_kit_libs_android_video
sha256: "3f6274e5ab2de512c286a25c327288601ee445ed8ac319e0ef0b66148bd8f76c"
url: "https://pub.dev"
source: hosted
version: "1.3.8"
media_kit_video:
dependency: "direct main"
description:
name: media_kit_video
sha256: "813858c3fe84eb46679eb698695f60665e2bfbef757766fac4d2e683f926e15a"
url: "https://pub.dev"
source: hosted
version: "1.3.1"
meta: meta:
dependency: transitive dependency: transitive
description: description:
@@ -789,6 +821,30 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.6.0" version: "0.6.0"
safe_local_storage:
dependency: transitive
description:
name: safe_local_storage
sha256: "494b982d5edb71030650ea463d939670e91b232b588323dc75229d2c5f23e7b7"
url: "https://pub.dev"
source: hosted
version: "2.0.6"
screen_brightness_android:
dependency: transitive
description:
name: screen_brightness_android
sha256: "2008ad8e9527cc968f7a4de1ec58b476d495b3c612a149dbd6550c4f046da147"
url: "https://pub.dev"
source: hosted
version: "2.1.6"
screen_brightness_platform_interface:
dependency: transitive
description:
name: screen_brightness_platform_interface
sha256: "2de60c0ba569b898950029cc1f7e9dd72bda44a22beb5054aac331cb6fce2ff2"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
screen_retriever: screen_retriever:
dependency: transitive dependency: transitive
description: description:
@@ -1034,6 +1090,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.4.0" version: "1.4.0"
universal_platform:
dependency: transitive
description:
name: universal_platform
sha256: "64e16458a0ea9b99260ceb5467a214c1f298d647c659af1bff6d3bf82536b1ec"
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: url_launcher:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -1146,6 +1218,30 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "15.2.0" version: "15.2.0"
volume_controller:
dependency: transitive
description:
name: volume_controller
sha256: bed7c57b1da19d60a58e2ed598b6824754cd9af1b728b26fb966a8745c72629c
url: "https://pub.dev"
source: hosted
version: "3.5.0"
wakelock_plus:
dependency: transitive
description:
name: wakelock_plus
sha256: "61713aa82b7f85c21c9f4cd0a148abd75f38a74ec645fcb1e446f882c82fd09b"
url: "https://pub.dev"
source: hosted
version: "1.3.3"
wakelock_plus_platform_interface:
dependency: transitive
description:
name: wakelock_plus_platform_interface
sha256: "0618d1799f0b28bcf98255b4ee8313e6fc4d38589dc4ee5fe5840d57d1aff6da"
url: "https://pub.dev"
source: hosted
version: "1.6.0"
web: web:
dependency: transitive dependency: transitive
description: description:
@@ -1204,4 +1300,4 @@ packages:
version: "3.1.3" version: "3.1.3"
sdks: sdks:
dart: ">=3.11.0 <4.0.0" dart: ">=3.11.0 <4.0.0"
flutter: ">=3.38.4" flutter: ">=3.41.0"

View File

@@ -41,6 +41,9 @@ dependencies:
flutter_svg: ^2.3.0 flutter_svg: ^2.3.0
lpinyin: ^2.0.3 lpinyin: ^2.0.3
scrollable_positioned_list: ^0.3.8 scrollable_positioned_list: ^0.3.8
media_kit: ^1.1.11
media_kit_video: ^1.2.5
media_kit_libs_android_video: ^1.3.6
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:

View File

@@ -9,10 +9,12 @@
#include <dynamic_color/dynamic_color_plugin_c_api.h> #include <dynamic_color/dynamic_color_plugin_c_api.h>
#include <file_selector_windows/file_selector_windows.h> #include <file_selector_windows/file_selector_windows.h>
#include <flutter_inappwebview_windows/flutter_inappwebview_windows_plugin_c_api.h> #include <flutter_inappwebview_windows/flutter_inappwebview_windows_plugin_c_api.h>
#include <media_kit_video/media_kit_video_plugin_c_api.h>
#include <permission_handler_windows/permission_handler_windows_plugin.h> #include <permission_handler_windows/permission_handler_windows_plugin.h>
#include <screen_retriever_windows/screen_retriever_windows_plugin_c_api.h> #include <screen_retriever_windows/screen_retriever_windows_plugin_c_api.h>
#include <share_plus/share_plus_windows_plugin_c_api.h> #include <share_plus/share_plus_windows_plugin_c_api.h>
#include <url_launcher_windows/url_launcher_windows.h> #include <url_launcher_windows/url_launcher_windows.h>
#include <volume_controller/volume_controller_plugin_c_api.h>
#include <window_manager/window_manager_plugin.h> #include <window_manager/window_manager_plugin.h>
void RegisterPlugins(flutter::PluginRegistry* registry) { void RegisterPlugins(flutter::PluginRegistry* registry) {
@@ -22,6 +24,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
registry->GetRegistrarForPlugin("FileSelectorWindows")); registry->GetRegistrarForPlugin("FileSelectorWindows"));
FlutterInappwebviewWindowsPluginCApiRegisterWithRegistrar( FlutterInappwebviewWindowsPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FlutterInappwebviewWindowsPluginCApi")); registry->GetRegistrarForPlugin("FlutterInappwebviewWindowsPluginCApi"));
MediaKitVideoPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("MediaKitVideoPluginCApi"));
PermissionHandlerWindowsPluginRegisterWithRegistrar( PermissionHandlerWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin")); registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin"));
ScreenRetrieverWindowsPluginCApiRegisterWithRegistrar( ScreenRetrieverWindowsPluginCApiRegisterWithRegistrar(
@@ -30,6 +34,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi")); registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi"));
UrlLauncherWindowsRegisterWithRegistrar( UrlLauncherWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("UrlLauncherWindows")); registry->GetRegistrarForPlugin("UrlLauncherWindows"));
VolumeControllerPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("VolumeControllerPluginCApi"));
WindowManagerPluginRegisterWithRegistrar( WindowManagerPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("WindowManagerPlugin")); registry->GetRegistrarForPlugin("WindowManagerPlugin"));
} }

View File

@@ -6,10 +6,12 @@ list(APPEND FLUTTER_PLUGIN_LIST
dynamic_color dynamic_color
file_selector_windows file_selector_windows
flutter_inappwebview_windows flutter_inappwebview_windows
media_kit_video
permission_handler_windows permission_handler_windows
screen_retriever_windows screen_retriever_windows
share_plus share_plus
url_launcher_windows url_launcher_windows
volume_controller
window_manager window_manager
) )