Skip to content

Commit 18bc0b2

Browse files
author
liuchuancong
committed
fix(添加小窗播放)
1 parent 973ac36 commit 18bc0b2

9 files changed

Lines changed: 178 additions & 35 deletions

File tree

lib/common/global/initialized.dart

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import 'dart:io';
2+
import 'dart:ffi';
23
import 'dart:developer';
34
import 'package:get/get.dart';
5+
import 'package:win32/win32.dart';
46
import 'package:pure_live/common/index.dart';
57
import 'package:pure_live/plugins/global.dart';
68
import 'package:hive_ce_flutter/hive_flutter.dart';
@@ -37,21 +39,37 @@ class AppInitializer {
3739
// 👇 从启动参数获取实例 ID
3840
String instanceId = getInstanceIdFromArgs(args);
3941

40-
if (PlatformUtils.isDesktop) {
41-
await DesktopManager.initialize();
42-
} else if (PlatformUtils.isMobile) {
43-
await MobileManager.initialize();
44-
}
45-
46-
PrefUtil.prefs = await SharedPreferences.getInstance();
47-
4842
// 👇 每个实例使用独立 Hive 路径
4943
final appDir = await getApplicationDocumentsDirectory();
5044
String path = '${appDir.path}${Platform.pathSeparator}pure_live${Platform.pathSeparator}$instanceId';
5145
if (instanceId.isEmpty) {
5246
path = '${appDir.path}${Platform.pathSeparator}pure_live';
5347
}
48+
if (PlatformUtils.isDesktopNotMac) {
49+
// Hive 默认锁文件通常叫 'LOCK',但我们可以自己维护一个实例锁,更加稳定
50+
final lockFile = File('$path${Platform.pathSeparator}app_instance.lock');
51+
52+
try {
53+
if (!lockFile.parent.existsSync()) lockFile.parent.createSync(recursive: true);
54+
final raf = lockFile.openSync(mode: FileMode.write);
55+
raf.lockSync();
56+
} catch (e) {
57+
log("检测到实例 [$instanceId] 文件夹已被锁定,正在唤醒已有窗口...");
58+
final hwnd = FindWindow(nullptr, TEXT('纯粹直播'));
59+
if (hwnd != 0) {
60+
if (IsIconic(hwnd) != 0) ShowWindow(hwnd, SW_RESTORE);
61+
SetForegroundWindow(hwnd);
62+
}
63+
exit(0);
64+
}
65+
}
66+
if (PlatformUtils.isDesktop) {
67+
await DesktopManager.initialize();
68+
} else if (PlatformUtils.isMobile) {
69+
await MobileManager.initialize();
70+
}
5471

72+
PrefUtil.prefs = await SharedPreferences.getInstance();
5573
try {
5674
await Hive.initFlutter(path);
5775
await HivePrefUtil.init();
@@ -70,13 +88,11 @@ class AppInitializer {
7088
initService();
7189

7290
if (PlatformUtils.isDesktopNotMac) {
73-
if (instanceId == 'default') {
74-
if (!await FlutterSingleInstance().isFirstInstance()) {
75-
log("Default instance is already running");
76-
exit(0);
77-
}
78-
await _setupLaunchAtStartup();
91+
if (!await FlutterSingleInstance().isFirstInstance()) {
92+
log("Default instance is already running");
93+
exit(0);
7994
}
95+
await _setupLaunchAtStartup();
8096
}
8197
_isInitialized = true;
8298
}

lib/common/services/settings_service.dart

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ class SettingsService extends GetxController {
7575
final videoOutputDriver = (HivePrefUtil.getString('videoOutputDriver') ?? "gpu").obs;
7676
final audioOutputDriver = (HivePrefUtil.getString('audioOutputDriver') ?? "auto").obs;
7777
final videoHardwareDecoder = (HivePrefUtil.getString('videoHardwareDecoder') ?? "auto").obs;
78-
78+
final floatPlay = (HivePrefUtil.getBool('floatPlay') ?? true).obs;
7979
// ==============================
8080
// 🍪 平台 Cookie
8181
// ==============================
@@ -237,6 +237,10 @@ class SettingsService extends GetxController {
237237
FlutterExitApp.exitApp();
238238
});
239239

240+
floatPlay.listen((value) {
241+
HivePrefUtil.setBool('floatPlay', value);
242+
});
243+
240244
videoFitIndex.listen((value) {
241245
HivePrefUtil.setInt('videoFitIndex', value);
242246
});
@@ -660,6 +664,7 @@ class SettingsService extends GetxController {
660664
? double.parse(json['danmakuFontBorder'].toString())
661665
: 4.0;
662666
danmakuOpacity.value = json['danmakuOpacity'] != null ? double.parse(json['danmakuOpacity'].toString()) : 1.0;
667+
floatPlay.value = json['floatPlay'] ?? true;
663668
enableCodec.value = json['enableCodec'] ?? true;
664669
playerCompatMode.value = json['playerCompatMode'] ?? false;
665670
bilibiliCookie.value = json['bilibiliCookie'] ?? '';
@@ -725,6 +730,7 @@ class SettingsService extends GetxController {
725730
json['danmakuFontSize'] = danmakuFontSize.value;
726731
json['danmakuFontBorder'] = danmakuFontBorder.value;
727732
json['danmakuOpacity'] = danmakuOpacity.value;
733+
json['floatPlay'] = floatPlay.value;
728734
json['videoPlayerIndex'] = videoPlayerIndex.value;
729735
json['enableCodec'] = enableCodec.value;
730736
json['playerCompatMode'] = playerCompatMode.value;

lib/common/widgets/floating_icon.dart

Whitespace-only changes.

lib/modules/live_play/live_play_controller.dart

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,6 @@ class LivePlayController extends StateController with GetSingleTickerProviderSta
7272

7373
@override
7474
void onClose() {
75-
success.value = false;
76-
SwitchableGlobalPlayer().stop();
7775
tabController.dispose();
7876
if (Platform.isAndroid) {
7977
BackButtonInterceptor.removeByName("live_play_page");
@@ -83,8 +81,6 @@ class LivePlayController extends StateController with GetSingleTickerProviderSta
8381

8482
@override
8583
void dispose() {
86-
success.value = false;
87-
SwitchableGlobalPlayer().stop();
8884
tabController.dispose();
8985
if (Platform.isAndroid) {
9086
BackButtonInterceptor.removeByName("live_play_page");
@@ -141,23 +137,18 @@ class LivePlayController extends StateController with GetSingleTickerProviderSta
141137
}
142138

143139
bool myInterceptor(bool stopDefaultButtonEvent, RouteInfo info) {
144-
// 1. 如果是全屏,退出全屏
145140
if (videoController.value!.isFullscreen.value) {
146141
setNormalScreen();
147142
videoController.value!.exitFullScreen();
148-
return true; // 拦截,不让页面关闭
143+
return true;
149144
}
150145

151-
// 2. 如果显示设置面板,隐藏它
152146
if (videoController.value!.showSettting.value) {
153147
videoController.value!.showSettting.toggle();
154-
return true; // 拦截,不让页面关闭
148+
return true;
155149
}
156-
157-
// 3. 所有特殊状态都处理完了,现在决定是否允许退出页面
158-
// 如果你想直接退出页面:
159150
success.value = false;
160-
return false; // 不拦截,让系统执行默认的返回(关闭页面)
151+
return false;
161152
}
162153

163154
Future<LiveRoom> onInitPlayerState({

lib/modules/settings/settings_page.dart

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,15 @@ class SettingsPage extends GetView<SettingsService> {
8686
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (context) => const BackupPage())),
8787
),
8888
SectionTitle(title: S.of(context).video),
89+
Obx(
90+
() => SwitchListTile(
91+
title: Text('退出小窗播放'),
92+
subtitle: Text("应用退出时是否关闭小窗播放"),
93+
value: controller.floatPlay.value,
94+
activeThumbColor: Theme.of(context).colorScheme.primary,
95+
onChanged: (bool value) => controller.floatPlay.value = value,
96+
),
97+
),
8998
if (Platform.isAndroid)
9099
Obx(
91100
() => ListTile(

lib/player/switchable_global_player.dart

Lines changed: 91 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import 'package:rxdart/rxdart.dart';
88
import 'unified_player_interface.dart';
99
import 'package:floating/floating.dart';
1010
import 'package:pure_live/common/index.dart';
11+
import 'package:pure_live/routes/app_navigation.dart';
12+
import 'package:flutter_floating/flutter_floating.dart';
1113
import 'package:pure_live/common/global/platform_utils.dart';
1214

1315
enum PlayerEngine { mediaKit, fijk }
@@ -28,7 +30,8 @@ class SwitchableGlobalPlayer {
2830
late Floating floating;
2931
bool playerHasInit = false;
3032
bool hasSetVolume = false;
31-
33+
static const String _floatTag = "global_video_player";
34+
final isFloating = false.obs;
3235
// 依赖
3336
final SettingsService settings = Get.find<SettingsService>();
3437

@@ -55,6 +58,8 @@ class SwitchableGlobalPlayer {
5558
Stream<int?> get width => _currentPlayer?.width ?? Stream.value(null);
5659
Stream<int?> get height => _currentPlayer?.height ?? Stream.value(null);
5760

61+
// 全局floating
62+
late LiveRoom currentFloatRoom;
5863
Future<void> init(PlayerEngine engine) async {
5964
if (_currentPlayer != null) return;
6065
_currentPlayer = _createPlayer(engine);
@@ -130,6 +135,88 @@ class SwitchableGlobalPlayer {
130135
}
131136
}
132137

138+
/// 构建悬浮窗关闭按钮
139+
Widget _buildCloseButton() {
140+
return GestureDetector(
141+
onTap: () {
142+
stop();
143+
closeAppFloating();
144+
},
145+
child: Container(
146+
padding: const EdgeInsets.all(4),
147+
decoration: BoxDecoration(
148+
color: Colors.black.withAlpha(128),
149+
shape: BoxShape.circle,
150+
border: Border.all(color: Colors.white24, width: 0.5),
151+
),
152+
child: const Icon(Icons.close, color: Colors.white, size: 16),
153+
),
154+
);
155+
}
156+
157+
void showAppFloating(LiveRoom room) {
158+
floatingManager.disposeFloating(_floatTag);
159+
double baseWidth = Platform.isWindows ? 300.0 : 200.0;
160+
double ratio = isVerticalVideo.value ? (9 / 16) : (16 / 9);
161+
double floatHeight = baseWidth / ratio;
162+
floatingManager.createFloating(
163+
_floatTag,
164+
FloatingOverlay(
165+
Container(
166+
width: baseWidth,
167+
height: floatHeight,
168+
clipBehavior: Clip.antiAlias,
169+
decoration: BoxDecoration(
170+
borderRadius: BorderRadius.circular(12),
171+
color: Colors.black,
172+
boxShadow: [
173+
BoxShadow(
174+
color: Colors.black.withAlpha(100), // 加深一点阴影
175+
blurRadius: 15,
176+
spreadRadius: 2,
177+
offset: const Offset(0, 4),
178+
),
179+
],
180+
),
181+
child: Stack(
182+
children: [
183+
getVideoWidget(null),
184+
Positioned.fill(
185+
child: GestureDetector(
186+
behavior: HitTestBehavior.opaque,
187+
onTap: () {
188+
// 点击悬浮窗回到页面的逻辑
189+
closeAppFloating();
190+
AppNavigator.toLiveRoomDetail(liveRoom: currentFloatRoom);
191+
},
192+
child: const SizedBox.expand(),
193+
),
194+
),
195+
196+
// 3. 顶层:关闭按钮
197+
Positioned(right: 8, top: 8, child: _buildCloseButton()),
198+
],
199+
),
200+
),
201+
right: 50,
202+
top: 100,
203+
slideType: FloatingEdgeType.onRightAndTop,
204+
params: FloatingParams(isSnapToEdge: false, snapToEdgeSpace: 10, dragOpacity: 0.8),
205+
),
206+
);
207+
208+
floatingManager.getFloating(_floatTag).open(Get.context!);
209+
currentFloatRoom = room;
210+
isFloating.value = true;
211+
}
212+
213+
/// 关闭并销毁悬浮播放器
214+
void closeAppFloating() {
215+
if (!isFloating.value) return;
216+
floatingManager.disposeFloating(_floatTag);
217+
isFloating.value = false;
218+
}
219+
133220
Future<void> setVolume(double volume) async {
134221
final clamped = volume.clamp(0.0, 1.0);
135222
currentVolume.value = clamped;
@@ -168,6 +255,7 @@ class SwitchableGlobalPlayer {
168255

169256
Widget getVideoWidget(Widget? child) {
170257
return Obx(() {
258+
final bool isFloatContent = isFloating.value && child == null;
171259
if (!isInitialized.value) {
172260
return Material(
173261
child: Stack(
@@ -200,7 +288,7 @@ class SwitchableGlobalPlayer {
200288
children: [
201289
Container(color: Colors.black),
202290
_currentPlayer?.getVideoWidget(settings.videoFitIndex.value, child) ?? const SizedBox(),
203-
child ?? const SizedBox(),
291+
if (!isFloatContent) child ?? const SizedBox(),
204292
],
205293
),
206294
resizeToAvoidBottomInset: true,
@@ -238,7 +326,7 @@ class SwitchableGlobalPlayer {
238326
children: [
239327
Container(color: Colors.black),
240328
_currentPlayer?.getVideoWidget(settings.videoFitIndex.value, child) ?? const SizedBox(),
241-
child ?? const SizedBox(),
329+
if (!isFloatContent) child ?? const SizedBox(),
242330
],
243331
),
244332
resizeToAvoidBottomInset: true,

lib/routes/app_navigation.dart

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,18 +42,34 @@ class BackButtonObserver extends RouteObserver<PageRoute<dynamic>> {
4242
@override
4343
void didPop(Route route, Route? previousRoute) {
4444
super.didPop(route, previousRoute);
45-
// 处理路由弹出事件
4645
if (route.settings.name == RoutePath.kLivePlay) {
4746
try {
48-
SwitchableGlobalPlayer().stop();
49-
Get.find<LivePlayController>().success.value = false;
47+
final livePlayController = Get.find<LivePlayController>();
48+
livePlayController.success.value = false;
49+
final settings = Get.find<SettingsService>();
50+
if (settings.floatPlay.value) {
51+
Future.delayed(Duration(milliseconds: 200), () {
52+
SwitchableGlobalPlayer().showAppFloating(livePlayController.detail.value!);
53+
});
54+
log("BackButtonObserver showAppFloating");
55+
} else {
56+
SwitchableGlobalPlayer().stop();
57+
}
5058
if (PlatformUtils.isMobile) {
5159
WindowService().doExitFullScreen();
5260
}
53-
Get.find<LivePlayController>().onDelete();
5461
} catch (e) {
55-
log(e.toString());
62+
log("BackButtonObserver Error: ${e.toString()}");
5663
}
5764
}
5865
}
66+
67+
@override
68+
void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) {
69+
super.didPush(route, previousRoute);
70+
if (route.settings.name == RoutePath.kLivePlay) {
71+
log("BackButtonObserver enter LivePlay");
72+
SwitchableGlobalPlayer().closeAppFloating();
73+
}
74+
}
5975
}

pubspec.lock

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,14 @@ packages:
345345
url: "https://pub.flutter-io.cn"
346346
source: hosted
347347
version: "1.0.2"
348+
cupertino_icons:
349+
dependency: transitive
350+
description:
351+
name: cupertino_icons
352+
sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6
353+
url: "https://pub.flutter-io.cn"
354+
source: hosted
355+
version: "1.0.8"
348356
dart_quickjs:
349357
dependency: "direct main"
350358
description:
@@ -576,6 +584,14 @@ packages:
576584
url: "https://pub.flutter-io.cn"
577585
source: hosted
578586
version: "2.0.0"
587+
flutter_floating:
588+
dependency: "direct main"
589+
description:
590+
name: flutter_floating
591+
sha256: "9887a378b4f48ed7a29f67562e8da2205d3c39f62baf9d75e376c0a190ab32ce"
592+
url: "https://pub.flutter-io.cn"
593+
source: hosted
594+
version: "2.0.1"
579595
flutter_highlight:
580596
dependency: transitive
581597
description:

pubspec.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ dependencies:
120120
markdown_widget: ^2.3.2+8
121121
google_fonts: ^6.3.3
122122
back_button_interceptor: ^8.0.4
123+
flutter_floating: ^2.0.1
123124
win32: ^5.15.0
124125
dependency_overrides:
125126
media_kit:

0 commit comments

Comments
 (0)