feat: take screenshot (#11591)

* feat: take screenshot

Signed-off-by: fufesou <linlong1266@gmail.com>

* screenshot, vram temp switch capturer

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix: misspelling

Signed-off-by: fufesou <linlong1266@gmail.com>

* screenshot, taking

Signed-off-by: fufesou <linlong1266@gmail.com>

* screenshot, rgba stride

Signed-off-by: fufesou <linlong1266@gmail.com>

* Bumps 1.4.0

Signed-off-by: fufesou <linlong1266@gmail.com>

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
This commit is contained in:
fufesou
2025-04-30 17:23:35 +08:00
committed by GitHub
parent 2864e1984a
commit c626c2414d
82 changed files with 948 additions and 96 deletions

View File

@@ -1,3 +1,4 @@
import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart';
@@ -15,7 +16,7 @@ bool isEditOsPassword = false;
class TTextMenu {
final Widget child;
final VoidCallback onPressed;
final VoidCallback? onPressed;
Widget? trailingIcon;
bool divider;
TTextMenu(
@@ -294,6 +295,41 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
),
onPressed: () => ffi.recordingModel.toggle()));
}
// to-do:
// 1. Web desktop
// 2. Mobile, copy the image to the clipboard
if (isDesktop) {
final isScreenshotSupported = bind.sessionGetCommonSync(
sessionId: sessionId, key: 'is_screenshot_supported', param: '');
if ('true' == isScreenshotSupported) {
v.add(TTextMenu(
child: Text(ffi.ffiModel.timerScreenshot != null
? '${translate('Taking screenshot')} ...'
: translate('Take screenshot')),
onPressed: ffi.ffiModel.timerScreenshot != null
? null
: () {
if (pi.currentDisplay == kAllDisplayValue) {
msgBox(
sessionId,
'custom-nook-nocancel-hasclose-info',
'Take screenshot',
'screenshot-merged-screen-not-supported-tip',
'',
ffi.dialogManager);
} else {
bind.sessionTakeScreenshot(
sessionId: sessionId, display: pi.currentDisplay);
ffi.ffiModel.timerScreenshot =
Timer(Duration(seconds: 30), () {
ffi.ffiModel.timerScreenshot = null;
});
}
},
));
}
}
// fingerprint
if (!(isDesktop || isWebDesktop)) {
v.add(TTextMenu(

View File

@@ -220,7 +220,8 @@ const double kDefaultQuality = 50;
const double kMaxQuality = 100;
const double kMaxMoreQuality = 2000;
const String kKeyPrinterIncommingJobAction = 'printer-incomming-job-action';
// incomming (should be incoming) is kept, because change it will break the previous setting.
const String kKeyPrinterIncomingJobAction = 'printer-incomming-job-action';
const String kValuePrinterIncomingJobDismiss = 'dismiss';
const String kValuePrinterIncomingJobDefault = '';
const String kValuePrinterIncomingJobSelected = 'selected';

View File

@@ -1908,7 +1908,7 @@ class __PrinterState extends State<_Printer> {
final scrollController = ScrollController();
return ListView(controller: scrollController, children: [
outgoing(context),
incomming(context),
incoming(context),
]).marginOnly(bottom: _kListViewBottomMargin);
}
@@ -1995,15 +1995,15 @@ class __PrinterState extends State<_Printer> {
return _Card(title: 'Outgoing Print Jobs', children: children);
}
Widget incomming(BuildContext context) {
Widget incoming(BuildContext context) {
onRadioChanged(String value) async {
await bind.mainSetLocalOption(
key: kKeyPrinterIncommingJobAction, value: value);
key: kKeyPrinterIncomingJobAction, value: value);
setState(() {});
}
PrinterOptions printerOptions = PrinterOptions.load();
return _Card(title: 'Incomming Print Jobs', children: [
return _Card(title: 'Incoming Print Jobs', children: [
_Radio(context,
value: kValuePrinterIncomingJobDismiss,
groupValue: printerOptions.action,

View File

@@ -695,9 +695,9 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
);
if (index != null) {
if (index < mobileActionMenus.length) {
mobileActionMenus[index].onPressed.call();
mobileActionMenus[index].onPressed?.call();
} else if (index < mobileActionMenus.length + more.length) {
menus[index - mobileActionMenus.length].onPressed.call();
menus[index - mobileActionMenus.length].onPressed?.call();
}
}
}();
@@ -770,7 +770,7 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
elevation: 8,
);
if (index != null && index < menus.length) {
menus[index].onPressed.call();
menus[index].onPressed?.call();
}
});
}
@@ -1267,7 +1267,7 @@ void showOptions(
title: resolution.child,
onTap: () {
close();
resolution.onPressed();
resolution.onPressed?.call();
},
));
}
@@ -1279,7 +1279,7 @@ void showOptions(
title: virtualDisplayMenu.child,
onTap: () {
close();
virtualDisplayMenu.onPressed();
virtualDisplayMenu.onPressed?.call();
},
));
}

View File

@@ -243,7 +243,7 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
Widget build(BuildContext context) {
Provider.of<FfiModel>(context);
final outgoingOnly = bind.isOutgoingOnly();
final incommingOnly = bind.isIncomingOnly();
final incomingOnly = bind.isIncomingOnly();
final customClientSection = CustomSettingsSection(
child: Column(
children: [
@@ -728,7 +728,7 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
});
},
),
if (!incommingOnly)
if (!incomingOnly)
SettingsTile.switchTile(
title:
Text(translate('Automatically record outgoing sessions')),

View File

@@ -478,9 +478,9 @@ class _ViewCameraPageState extends State<ViewCameraPage>
);
if (index != null) {
if (index < mobileActionMenus.length) {
mobileActionMenus[index].onPressed.call();
mobileActionMenus[index].onPressed?.call();
} else if (index < mobileActionMenus.length + more.length) {
menus[index - mobileActionMenus.length].onPressed.call();
menus[index - mobileActionMenus.length].onPressed?.call();
}
}
}();
@@ -553,7 +553,7 @@ class _ViewCameraPageState extends State<ViewCameraPage>
elevation: 8,
);
if (index != null && index < menus.length) {
menus[index].onPressed.call();
menus[index].onPressed?.call();
}
});
}

View File

@@ -34,6 +34,7 @@ import 'package:flutter_svg/flutter_svg.dart';
import 'package:get/get.dart';
import 'package:uuid/uuid.dart';
import 'package:window_manager/window_manager.dart';
import 'package:file_picker/file_picker.dart';
import '../common.dart';
import '../utils/image.dart' as img;
@@ -119,6 +120,8 @@ class FfiModel with ChangeNotifier {
RxBool waitForFirstImage = true.obs;
bool isRefreshing = false;
Timer? timerScreenshot;
Rect? get rect => _rect;
bool get isOriginalResolutionSet =>
_pi.tryGetDisplayIfNotAllDisplay()?.isOriginalResolutionSet ?? false;
@@ -216,6 +219,7 @@ class FfiModel with ChangeNotifier {
_timer = null;
clearPermissions();
waitForImageTimer?.cancel();
timerScreenshot?.cancel();
}
setConnectionType(String peerId, bool secure, bool direct) {
@@ -414,12 +418,82 @@ class FfiModel with ChangeNotifier {
}
} else if (name == "printer_request") {
_handlePrinterRequest(evt, sessionId, peerId);
} else if (name == 'screenshot') {
_handleScreenshot(evt, sessionId, peerId);
} else {
debugPrint('Event is not handled in the fixed branch: $name');
}
};
}
_handleScreenshot(
Map<String, dynamic> evt, SessionID sessionId, String peerId) {
timerScreenshot?.cancel();
timerScreenshot = null;
final msg = evt['msg'] ?? '';
final msgBoxType = 'custom-nook-nocancel-hasclose';
final msgBoxTitle = 'Take screenshot';
final dialogManager = parent.target!.dialogManager;
if (msg.isNotEmpty) {
msgBox(sessionId, msgBoxType, msgBoxTitle, msg, '', dialogManager);
} else {
final msgBoxText = 'screenshot-action-tip';
close() {
dialogManager.dismissAll();
}
saveAs() {
close();
Future.delayed(Duration.zero, () async {
final ts = DateTime.now().millisecondsSinceEpoch ~/ 1000;
String? outputFile = await FilePicker.platform.saveFile(
dialogTitle: '${translate('Save as')}...',
fileName: 'screenshot_$ts.png',
allowedExtensions: ['png'],
type: FileType.custom,
);
if (outputFile == null) {
bind.sessionHandleScreenshot(sessionId: sessionId, action: '2');
} else {
final res = await bind.sessionHandleScreenshot(
sessionId: sessionId, action: '0:$outputFile');
if (res.isNotEmpty) {
msgBox(sessionId, 'custom-nook-nocancel-hasclose-error',
'Take screenshot', res, '', dialogManager);
}
}
});
}
copyToClipboard() {
bind.sessionHandleScreenshot(sessionId: sessionId, action: '1');
close();
}
cancel() {
bind.sessionHandleScreenshot(sessionId: sessionId, action: '2');
close();
}
final List<Widget> buttons = [
dialogButton('${translate('Save as')}...', onPressed: saveAs),
dialogButton('Copy to clipboard', onPressed: copyToClipboard),
dialogButton('Cancel', onPressed: cancel),
];
dialogManager.dismissAll();
dialogManager.show(
(setState, close, context) => CustomAlertDialog(
title: null,
content: SelectionArea(
child: msgboxContent(msgBoxType, msgBoxTitle, msgBoxText)),
actions: buttons,
),
tag: '$msgBoxType-$msgBoxTitle-$msgBoxTitle',
);
}
}
_handlePrinterRequest(
Map<String, dynamic> evt, SessionID sessionId, String peerId) {
final id = evt['id'];
@@ -451,7 +525,7 @@ class FfiModel with ChangeNotifier {
if (saveSettings.value || dontShowAgain.value) {
bind.mainSetLocalOption(key: kKeyPrinterSelected, value: printerName);
bind.mainSetLocalOption(
key: kKeyPrinterIncommingJobAction,
key: kKeyPrinterIncomingJobAction,
value: defaultOrSelectedGroupValue.value);
}
if (dontShowAgain.value) {
@@ -463,7 +537,7 @@ class FfiModel with ChangeNotifier {
onCancel() {
if (dontShowAgain.value) {
bind.mainSetLocalOption(
key: kKeyPrinterIncommingJobAction,
key: kKeyPrinterIncomingJobAction,
value: kValuePrinterIncomingJobDismiss);
}
close();

View File

@@ -13,7 +13,7 @@ class PrinterOptions {
required this.printerName});
static PrinterOptions load() {
var action = bind.mainGetLocalOption(key: kKeyPrinterIncommingJobAction);
var action = bind.mainGetLocalOption(key: kKeyPrinterIncomingJobAction);
if (![
kValuePrinterIncomingJobDismiss,
kValuePrinterIncomingJobDefault,
@@ -28,7 +28,7 @@ class PrinterOptions {
if (action == kValuePrinterIncomingJobSelected) {
action = kValuePrinterIncomingJobDefault;
bind.mainSetLocalOption(
key: kKeyPrinterIncommingJobAction,
key: kKeyPrinterIncomingJobAction,
value: kValuePrinterIncomingJobDefault);
if (printerNames.isEmpty) {
selectedPrinterName = '';

View File

@@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# 1.1.9-1 works for android, but for ios it becomes 1.1.91, need to set it to 1.1.9-a.1 for iOS, will get 1.1.9.1, but iOS store not allow 4 numbers
version: 1.3.9+57
version: 1.4.0+58
environment:
sdk: '^3.1.0'