画中画窗口

This commit is contained in:
wanghongenpin
2024-01-10 13:02:41 +08:00
parent 6f2966db1d
commit b8accd4302
26 changed files with 512 additions and 148 deletions

View File

@@ -28,8 +28,8 @@ class AppConfiguration {
//是否显示更新内容公告
bool upgradeNoticeV7 = true;
/// 是否启用小窗口
bool smallWindow = false;
/// 是否启用画中画
bool pipEnabled = true;
///
bool headerExpanded = true;
@@ -116,7 +116,7 @@ class AppConfiguration {
_theme = ThemeModel(mode: mode, useMaterial3: config['useMaterial3'] ?? true);
upgradeNoticeV7 = config['upgradeNoticeV7'] ?? true;
_language = config['language'] == null ? null : Locale.fromSubtags(languageCode: config['language']);
smallWindow = config['smallWindow'] ?? Platform.isAndroid;
pipEnabled = config['pipEnabled'] ?? true;
headerExpanded = config['headerExpanded'] ?? true;
iosVpnBackgroundAudioEnable = config['iosVpnBackgroundAudioEnable'];
} catch (e) {
@@ -142,7 +142,7 @@ class AppConfiguration {
'useMaterial3': _theme.useMaterial3,
'upgradeNoticeV7': upgradeNoticeV7,
"language": _language?.languageCode,
'smallWindow': smallWindow,
'pipEnabled': pipEnabled,
"headerExpanded": headerExpanded,
"iosVpnBackgroundAudioEnable": iosVpnBackgroundAudioEnable == false ? null : iosVpnBackgroundAudioEnable
};

View File

@@ -206,6 +206,7 @@ class _DesktopHomePagePageState extends State<DesktopHomePage> implements EventL
'4. 请求编辑URL参数支持表单编辑\n'
'5. 增加高级重放;\n'
'6. 域名过滤支持批量导出&编辑;\n'
'7. IOS支持画中画模式\n'
: 'TipsBy default, HTTPS packet capture will not be enabled. Please install the certificate before enabling HTTPS packet capture。\n'
'Click HTTPS Capture packets(Lock icon)Choose to install the root certificate and follow the prompts to proceed。\n\n'
'1. Increase multilingual support\n'
@@ -213,7 +214,8 @@ class _DesktopHomePagePageState extends State<DesktopHomePage> implements EventL
'3. Details page Headers Expanded Config\n'
'4. Request Edit URL parameter support for form editing\n'
'5. Support advanced replay\n'
'6. Domain name filtering supports batch export&editing\n',
'6. Domain name filtering supports batch export&editing\n'
'7. iOS Supports picture in picture mode;\n',
style: const TextStyle(fontSize: 14)));
});
}

View File

@@ -90,7 +90,8 @@ class SearchModel {
var entries = option == Option.requestHeader ? request.headers.entries : response?.headers.entries ?? [];
for (var entry in entries) {
if (entry.value.any((element) => element.contains(keyword))) {
if (entry.key.toLowerCase() == keyword.toLowerCase() ||
entry.value.any((element) => element.contains(keyword))) {
return true;
}
}

View File

@@ -148,7 +148,7 @@ class SearchConditionsState extends State<SearchConditions> {
Widget options(String title, Option option) {
bool isCN = localizations.localeName == 'zh';
return Container(
constraints: BoxConstraints(maxWidth: isCN ? 100 : 150, minWidth: 100, maxHeight: 33),
constraints: BoxConstraints(maxWidth: isCN ? 100 : 152, minWidth: 100, maxHeight: 33),
child: Row(children: [
Text(title, style: const TextStyle(fontSize: 12)),
Checkbox(

View File

@@ -194,7 +194,6 @@ class _RequestRuleListState extends State<RequestRuleList> {
rules = widget.requestRewrites.rules;
}
@override
Widget build(BuildContext context) {
return GestureDetector(
@@ -488,8 +487,12 @@ class _RuleAddDialogState extends State<RuleAddDialog> {
text: localizations.useGuide,
style: const TextStyle(color: Colors.blue, fontSize: 14),
recognizer: TapGestureRecognizer()
..onTap = () => DesktopMultiWindow.invokeMethod(0, "launchUrl",
'https://gitee.com/wanghongenpin/network-proxy-flutter/wikis/%E8%AF%B7%E6%B1%82%E9%87%8D%E5%86%99'))),
..onTap = () => DesktopMultiWindow.invokeMethod(
0,
"launchUrl",
isCN
? 'https://gitee.com/wanghongenpin/network-proxy-flutter/wikis/%E8%AF%B7%E6%B1%82%E9%87%8D%E5%86%99'
: 'https://github.com/wanghongenpin/network_proxy_flutter/wiki/Request-Rewrite'))),
]),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10.0)),
content: Container(

View File

@@ -4,13 +4,13 @@ import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:flutter_toastr/flutter_toastr.dart';
import 'package:network_proxy/native/app_lifecycle.dart';
import 'package:network_proxy/network/bin/server.dart';
import 'package:network_proxy/utils/lang.dart';
import 'package:network_proxy/utils/platform.dart';
import 'package:window_manager/window_manager.dart';
class SocketLaunch extends StatefulWidget {
static bool started = false;
static ValueNotifier<ValueWrap<bool>> startStatus = ValueNotifier(ValueWrap());
final ProxyServer proxyServer;
final int size;
@@ -30,20 +30,18 @@ class SocketLaunch extends StatefulWidget {
this.serverLaunch = true});
@override
State<StatefulWidget> createState() {
return _SocketLaunchState();
}
State<StatefulWidget> createState() => _SocketLaunchState();
}
class _SocketLaunchState extends State<SocketLaunch> with WindowListener, WidgetsBindingObserver {
AppLocalizations get localizations => AppLocalizations.of(context)!;
bool started = false;
@override
void initState() {
super.initState();
windowManager.addListener(this);
WidgetsBinding.instance.addObserver(this);
AppLifecycleBinding.ensureInitialized();
//启动代理服务器
if (widget.startup) {
start();
@@ -51,6 +49,11 @@ class _SocketLaunchState extends State<SocketLaunch> with WindowListener, Widget
if (Platforms.isDesktop()) {
windowManager.setPreventClose(true);
}
SocketLaunch.startStatus.addListener(() {
setState(() {
started = SocketLaunch.startStatus.value.get() ?? started;
});
});
}
@override
@@ -68,7 +71,7 @@ class _SocketLaunchState extends State<SocketLaunch> with WindowListener, Widget
Future<void> appExit() async {
await widget.proxyServer.stop();
SocketLaunch.started = false;
started = false;
await windowManager.destroy();
exit(0);
}
@@ -85,22 +88,22 @@ class _SocketLaunchState extends State<SocketLaunch> with WindowListener, Widget
print('AppLifecycleState.detached');
widget.onStop?.call();
widget.proxyServer.stop();
SocketLaunch.started = false;
started = false;
}
}
@override
Widget build(BuildContext context) {
return IconButton(
tooltip: SocketLaunch.started ? localizations.stop : localizations.start,
icon: Icon(SocketLaunch.started ? Icons.stop : Icons.play_arrow_sharp,
color: SocketLaunch.started ? Colors.red : Colors.green, size: widget.size.toDouble()),
tooltip: started ? localizations.stop : localizations.start,
icon: Icon(started ? Icons.stop : Icons.play_arrow_sharp,
color: started ? Colors.red : Colors.green, size: widget.size.toDouble()),
onPressed: () async {
if (SocketLaunch.started) {
if (started) {
if (!widget.serverLaunch) {
setState(() {
widget.onStop?.call();
SocketLaunch.started = !SocketLaunch.started;
started = !started;
});
return;
}
@@ -108,7 +111,7 @@ class _SocketLaunchState extends State<SocketLaunch> with WindowListener, Widget
widget.proxyServer.stop().then((value) {
widget.onStop?.call();
setState(() {
SocketLaunch.started = !SocketLaunch.started;
started = !started;
});
});
} else {
@@ -122,14 +125,14 @@ class _SocketLaunchState extends State<SocketLaunch> with WindowListener, Widget
if (!widget.serverLaunch) {
await widget.onStart?.call();
setState(() {
SocketLaunch.started = true;
started = true;
});
return;
}
widget.proxyServer.start().then((value) {
setState(() {
SocketLaunch.started = true;
started = true;
});
widget.onStart?.call();
}).catchError((e) {

View File

@@ -14,8 +14,8 @@ import 'package:network_proxy/ui/component/toolbox.dart';
import 'package:network_proxy/ui/component/utils.dart';
import 'package:network_proxy/ui/component/widgets.dart';
import 'package:network_proxy/ui/configuration.dart';
import 'package:network_proxy/ui/mobile/about.dart';
import 'package:network_proxy/ui/mobile/connect_remote.dart';
import 'package:network_proxy/ui/mobile/widgets/about.dart';
import 'package:network_proxy/ui/mobile/widgets/connect_remote.dart';
import 'package:network_proxy/ui/mobile/request/favorite.dart';
import 'package:network_proxy/ui/mobile/request/history.dart';
import 'package:network_proxy/ui/mobile/setting/app_whitelist.dart';
@@ -126,15 +126,13 @@ class SettingMenu extends StatelessWidget {
onTap: () => _language(context),
),
MobileThemeSetting(appConfiguration: appConfiguration),
Platform.isIOS
? const SizedBox()
: ListTile(
ListTile(
title: Text(localizations.windowMode),
subtitle: Text(localizations.windowModeSubTitle, style: const TextStyle(fontSize: 12)),
trailing: SwitchWidget(
value: appConfiguration.smallWindow,
value: appConfiguration.pipEnabled,
onChanged: (value) {
appConfiguration.smallWindow = value;
appConfiguration.pipEnabled = value;
appConfiguration.flushConfig();
})),
ListTile(

View File

@@ -15,14 +15,14 @@ import 'package:network_proxy/network/handler.dart';
import 'package:network_proxy/network/http/http.dart';
import 'package:network_proxy/network/http/websocket.dart';
import 'package:network_proxy/network/http_client.dart';
import 'package:network_proxy/ui/component/utils.dart';
import 'package:network_proxy/ui/configuration.dart';
import 'package:network_proxy/ui/content/panel.dart';
import 'package:network_proxy/ui/launch/launch.dart';
import 'package:network_proxy/ui/mobile/connect_remote.dart';
import 'package:network_proxy/ui/mobile/menu.dart';
import 'package:network_proxy/ui/mobile/request/list.dart';
import 'package:network_proxy/ui/mobile/request/search.dart';
import 'package:network_proxy/ui/mobile/widgets/connect_remote.dart';
import 'package:network_proxy/ui/mobile/widgets/pip.dart';
import 'package:network_proxy/utils/ip.dart';
class MobileHomePage extends StatefulWidget {
@@ -53,16 +53,16 @@ class MobileHomeState extends State<MobileHomePage> implements EventListener, Li
@override
void onUserLeaveHint() async {
if (Vpn.isVpnStarted && !pictureInPictureNotifier.value) {
if (desktop.value.connect || !Platform.isAndroid || !(await (AppConfiguration.instance)).smallWindow) {
if (desktop.value.connect || !Platform.isAndroid || !(await (AppConfiguration.instance)).pipEnabled) {
return;
}
PictureInPicture.enterPictureInPictureMode();
PictureInPicture.enterPictureInPictureMode(Platform.isAndroid ? await localIp() : "127.0.0.1", proxyServer.port);
}
}
@override
onPictureInPictureModeChanged(bool isInPictureInPictureMode) {
onPictureInPictureModeChanged(bool isInPictureInPictureMode) async {
if (isInPictureInPictureMode && !pictureInPictureNotifier.value) {
while (Navigator.canPop(context)) {
Navigator.pop(context);
@@ -81,6 +81,7 @@ class MobileHomeState extends State<MobileHomePage> implements EventListener, Li
@override
void onRequest(Channel channel, HttpRequest request) {
PictureInPicture.addData(request.requestUrl);
requestStateKey.currentState!.add(channel, request);
}
@@ -154,18 +155,21 @@ class MobileHomeState extends State<MobileHomePage> implements EventListener, Li
}
return Scaffold(
appBar: appBar(),
drawer: DrawerWidget(proxyServer: proxyServer),
floatingActionButton: _floatingActionButton(),
body: ValueListenableBuilder(
valueListenable: desktop,
builder: (context, value, _) {
return Column(children: [
value.connect ? remoteConnect(value) : const SizedBox(),
Expanded(child: RequestListWidget(key: requestStateKey, proxyServer: proxyServer))
]);
}),
);
floatingActionButton:
widget.appConfiguration.pipEnabled ? PictureInPictureWindow(proxyServer) : const SizedBox(),
body: Scaffold(
appBar: appBar(),
drawer: DrawerWidget(proxyServer: proxyServer),
floatingActionButton: _launchActionButton(),
body: ValueListenableBuilder(
valueListenable: desktop,
builder: (context, value, _) {
return Column(children: [
value.connect ? remoteConnect(value) : const SizedBox(),
Expanded(child: RequestListWidget(key: requestStateKey, proxyServer: proxyServer))
]);
}),
));
}));
}
@@ -177,36 +181,25 @@ class MobileHomeState extends State<MobileHomePage> implements EventListener, Li
onPressed: () => requestStateKey.currentState?.clean()),
const SizedBox(width: 2),
MoreMenu(proxyServer: proxyServer, desktop: desktop),
const SizedBox(width: 10)
const SizedBox(width: 10),
]);
}
FloatingActionButton _floatingActionButton() {
FloatingActionButton _launchActionButton() {
return FloatingActionButton(
onPressed: null,
child: Center(
child: futureWidget(localIp(), (data) {
SocketLaunch.started = Vpn.isVpnStarted;
return SocketLaunch(
proxyServer: proxyServer,
size: 36,
startup: false,
serverLaunch: false,
onStart: () async {
// if (Platform.isIOS && widget.appConfiguration.iosVpnBackgroundAudioEnable == null) {
// widget.appConfiguration.iosVpnBackgroundAudioEnable = false;
// await showConfirmDialog(context, content: localizations.iosVpnBackgroundAudio, onConfirm: () {
// widget.appConfiguration.iosVpnBackgroundAudioEnable = true;
// widget.appConfiguration.flushConfig();
// });
// }
Vpn.startVpn(Platform.isAndroid ? data : "127.0.0.1", proxyServer.port,
backgroundAudioEnable: widget.appConfiguration.iosVpnBackgroundAudioEnable,
appList: proxyServer.configuration.appWhitelist);
},
onStop: () => Vpn.stopVpn());
})),
child: SocketLaunch(
proxyServer: proxyServer,
size: 36,
startup: Vpn.isVpnStarted,
serverLaunch: false,
onStart: () async {
Vpn.startVpn(Platform.isAndroid ? await localIp() : "127.0.0.1", proxyServer.port,
backgroundAudioEnable: widget.appConfiguration.iosVpnBackgroundAudioEnable,
appList: proxyServer.configuration.appWhitelist);
},
onStop: () => Vpn.stopVpn())),
);
}
@@ -215,19 +208,21 @@ class MobileHomeState extends State<MobileHomePage> implements EventListener, Li
String content = isCN
? '提示默认不会开启HTTPS抓包请安装证书后再开启HTTPS抓包。\n\n'
'1. 增加多语言支持;\n'
'2. 请求重写支持文件选择\n'
'3. 抓包详情页面Headers默认展开配置\n'
'4. 请求编辑URL参数支持表单编辑\n'
'5. 增加高级重放\n'
'6. 域名过滤支持批量导出&编辑\n'
'1. 支持画中画模式,可在设置中关闭\n'
'2. 增加多语言支持\n'
'3. 请求重写支持文件选择\n'
'4. 抓包详情页面Headers默认展开配置\n'
'5. 请求编辑URL参数支持表单编辑\n'
'6. 增加高级重放\n'
'7. 域名过滤支持批量导出&编辑;\n'
: 'TipsBy default, HTTPS packet capture will not be enabled. Please install the certificate before enabling HTTPS packet capture。\n\n'
'1. Increase multilingual support\n'
'2. Request Rewrite support file selection\n'
'3. Details page Headers Expanded Config\n'
'4. Request Edit URL parameter support for form editing\n'
'5. Support advanced replay\n'
'6. Domain name filtering supports batch export&editing\n';
'1. Supports picture in picture mode, which can be turned off in settings;\n'
'2. Increase multilingual support\n'
'3. Request Rewrite support file selection\n'
'4. Details page Headers Expanded Config\n'
'5. Request Edit URL parameter support for form editing\n'
'6. Support advanced replay\n'
'7. Domain name filtering supports batch export&editing\n';
showAlertDialog(isCN ? '更新内容V1.0.7' : "Update content V1.0.7", content, () {
widget.appConfiguration.upgradeNoticeV7 = false;
widget.appConfiguration.flushConfig();

View File

@@ -40,7 +40,7 @@ class RequestListState extends State<RequestListWidget> {
void initState() {
super.initState();
if (widget.list != null) {
container.addAll(widget.list!);
container = widget.list!;
}
}

View File

@@ -444,8 +444,9 @@ class _RewriteRuleState extends State<RewriteRule> {
text: localizations.useGuide,
style: const TextStyle(color: Colors.blue, fontSize: 14),
recognizer: TapGestureRecognizer()
..onTap = () => launchUrl(Uri.parse(
'https://gitee.com/wanghongenpin/network-proxy-flutter/wikis/%E8%AF%B7%E6%B1%82%E9%87%8D%E5%86%99')))),
..onTap = () => launchUrl(Uri.parse(isCN
? 'https://gitee.com/wanghongenpin/network-proxy-flutter/wikis/%E8%AF%B7%E6%B1%82%E9%87%8D%E5%86%99'
: 'https://github.com/wanghongenpin/network_proxy_flutter/wiki/Request-Rewrite')))),
]),
actions: [
TextButton(

View File

@@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:network_proxy/network/bin/server.dart';
import 'package:network_proxy/network/util/crts.dart';
import 'package:network_proxy/ui/mobile/setting/video_player.dart';
import 'package:url_launcher/url_launcher.dart';
class MobileSslWidget extends StatefulWidget {
@@ -54,27 +55,39 @@ class _MobileSslState extends State<MobileSslWidget> {
}
Widget ios() {
return ExpansionTile(
title: Text(localizations.profileDownload),
initiallyExpanded: true,
childrenPadding: const EdgeInsets.only(left: 20),
expandedAlignment: Alignment.topLeft,
expandedCrossAxisAlignment: CrossAxisAlignment.start,
shape: const Border(),
children: [
TextButton(onPressed: () => _downloadCert(), child: Text("1. ${localizations.downloadRootCa}")),
TextButton(onPressed: () {}, child: Text("2. ${localizations.installRootCa} -> ${localizations.trustCa}")),
TextButton(onPressed: () {}, child: Text("2.1 ${localizations.installCaDescribe}")),
Padding(
padding: const EdgeInsets.only(left: 15),
child: Image.network("https://foruda.gitee.com/images/1689346516243774963/c56bc546_1073801.png",
height: 400)),
TextButton(onPressed: () {}, child: Text("2.2 ${localizations.trustCaDescribe}")),
Padding(
padding: const EdgeInsets.only(left: 15),
child: Image.network("https://foruda.gitee.com/images/1689346614916658100/fd9b9e41_1073801.png",
height: 270)),
]);
return Column(children: [
if (localizations.localeName != 'zh')
ExpansionTile(
title: Text(localizations.useGuide),
shape: const Border(),
maintainState: true,
children: [
Container(
height: 350, padding: const EdgeInsets.only(left: 15, right: 15), child: const VideoPlayerScreen())
],
),
ExpansionTile(
title: Text(localizations.installRootCa),
initiallyExpanded: true,
childrenPadding: const EdgeInsets.only(left: 20),
expandedAlignment: Alignment.topLeft,
expandedCrossAxisAlignment: CrossAxisAlignment.start,
shape: const Border(),
children: [
TextButton(onPressed: () => _downloadCert(), child: Text("1. ${localizations.downloadRootCa}")),
TextButton(onPressed: () {}, child: Text("2. ${localizations.installRootCa} -> ${localizations.trustCa}")),
TextButton(onPressed: () {}, child: Text("2.1 ${localizations.installCaDescribe}")),
Padding(
padding: const EdgeInsets.only(left: 15),
child: Image.network("https://foruda.gitee.com/images/1689346516243774963/c56bc546_1073801.png",
height: 400)),
TextButton(onPressed: () {}, child: Text("2.2 ${localizations.trustCaDescribe}")),
Padding(
padding: const EdgeInsets.only(left: 15),
child: Image.network("https://foruda.gitee.com/images/1689346614916658100/fd9b9e41_1073801.png",
height: 270)),
])
]);
}
Widget android() {

View File

@@ -0,0 +1,167 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:video_player/video_player.dart';
void main() => runApp(const VideoPlayerApp());
class VideoPlayerApp extends StatelessWidget {
const VideoPlayerApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
title: 'Video Player Demo',
home: VideoPlayerScreen(),
);
}
}
class VideoPlayerScreen extends StatefulWidget {
const VideoPlayerScreen({super.key});
@override
State<VideoPlayerScreen> createState() => _VideoPlayerScreenState();
}
class _VideoPlayerScreenState extends State<VideoPlayerScreen> {
late VideoPlayerController _controller;
late Future<void> _initializeVideoPlayerFuture;
@override
void initState() {
super.initState();
_controller = VideoPlayerController.network(
'https://github.com/wanghongenpin/network_proxy_flutter/assets/24794200/38bc5a83-999f-4af2-9d74-863532a81cef',
videoPlayerOptions: VideoPlayerOptions(mixWithOthers: true, allowBackgroundPlayback: true),
);
_initializeVideoPlayerFuture = _controller.initialize();
_initializeVideoPlayerFuture.whenComplete(() {
final MediaQueryData data = MediaQuery.of(context);
EdgeInsets paddingSafeArea = data.padding;
double widthScreen = data.size.width;
_controller.setPictureInPictureOverlayRect(
rect: Rect.fromLTWH(0, paddingSafeArea.top, widthScreen, 9 * widthScreen / 16));
// _controller.setAutomaticallyStartPictureInPicture(enableStartPictureInPictureAutomaticallyFromInline: true);
});
_controller.addListener(() {
setState(() {});
});
}
@override
void dispose() {
// Ensure disposing of the VideoPlayerController to free up resources.
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return FutureBuilder(
future: _initializeVideoPlayerFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
return AspectRatio(
aspectRatio: _controller.value.aspectRatio,
// Use the VideoPlayer widget to display the video.
child: Stack(alignment: Alignment.bottomCenter, children: [
VideoPlayer(_controller),
_ControlsOverlay(controller: _controller),
VideoProgressIndicator(
_controller,
allowScrubbing: true,
),
]),
);
} else {
// If the VideoPlayerController is still initializing, show a
// loading spinner.
return const Center(
child: CircularProgressIndicator(),
);
}
},
);
}
}
class _ControlsOverlay extends StatelessWidget {
const _ControlsOverlay({required this.controller});
static const List<double> _playbackRates = <double>[
0.25,
0.5,
1.0,
1.5,
2.0,
];
final VideoPlayerController controller;
@override
Widget build(BuildContext context) {
return Stack(
children: <Widget>[
AnimatedSwitcher(
duration: const Duration(milliseconds: 50),
reverseDuration: const Duration(milliseconds: 200),
child: controller.value.isPlaying
? const SizedBox.shrink()
: Container(
color: Colors.black26,
child: const Center(
child: Icon(
Icons.play_arrow,
color: Colors.white,
size: 80.0,
semanticLabel: 'Play',
),
),
),
),
GestureDetector(
onTap: () {
controller.value.isPlaying ? controller.pause() : controller.play();
},
),
Align(
alignment: Alignment.topRight,
child: IconButton(
onPressed: () async {
controller.startPictureInPicture();
},
icon: const Icon(Icons.picture_in_picture),
)),
Align(
alignment: Alignment.bottomRight,
child: PopupMenuButton<double>(
initialValue: controller.value.playbackSpeed,
tooltip: 'Playback speed',
onSelected: (double speed) {
controller.setPlaybackSpeed(speed);
},
itemBuilder: (BuildContext context) {
return <PopupMenuItem<double>>[
for (final double speed in _playbackRates)
PopupMenuItem<double>(
value: speed,
child: Text('${speed}x'),
)
];
},
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 15,
horizontal: 16,
),
child: Text('${controller.value.playbackSpeed}x', style: const TextStyle(color: Colors.white)),
),
),
),
],
);
}
}

View File

@@ -0,0 +1,60 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:network_proxy/native/pip.dart';
import 'package:network_proxy/network/bin/server.dart';
import 'package:network_proxy/utils/ip.dart';
class PictureInPictureWindow extends StatefulWidget {
final ProxyServer proxyServer;
const PictureInPictureWindow(
this.proxyServer, {
super.key,
});
@override
State<PictureInPictureWindow> createState() => _PictureInPictureState();
}
class _PictureInPictureState extends State<PictureInPictureWindow> {
static double xPosition = -1;
static double yPosition = -1;
static Size? size;
AppLocalizations get localizations => AppLocalizations.of(context)!;
@override
Widget build(BuildContext context) {
size ??= MediaQuery.of(context).size;
if (xPosition == -1) {
xPosition = size!.width * 0.9;
yPosition = size!.height * 0.35;
}
return Stack(children: [
Positioned(
top: yPosition,
left: xPosition,
child: GestureDetector(
onPanUpdate: (tapInfo) {
if (xPosition + tapInfo.delta.dx < 0) return;
if (yPosition + tapInfo.delta.dy < 0) return;
setState(() {
xPosition += tapInfo.delta.dx;
yPosition += tapInfo.delta.dy;
});
},
child: IconButton(
tooltip: localizations.windowMode,
onPressed: () async {
PictureInPicture.enterPictureInPictureMode(
Platform.isAndroid ? await localIp() : "127.0.0.1", widget.proxyServer.port);
},
icon: const Icon(Icons.picture_in_picture))),
)
]);
}
}