提交qiming-noVNC
This commit is contained in:
252
qiming-noVNC/app/clipboard.js
Normal file
252
qiming-noVNC/app/clipboard.js
Normal file
@@ -0,0 +1,252 @@
|
||||
/*
|
||||
* noVNC: HTML5 VNC client
|
||||
* Clipboard management module
|
||||
*/
|
||||
|
||||
import * as Log from '../core/util/logging.js';
|
||||
import { l10n } from './localization.js';
|
||||
|
||||
const _ = l10n.get.bind(l10n);
|
||||
|
||||
class ClipboardManager {
|
||||
constructor() {
|
||||
this.rfb = null;
|
||||
this.autoSync = false;
|
||||
this.permissionGranted = false;
|
||||
this.lastLocalText = null;
|
||||
this.syncFocusHandler = null;
|
||||
|
||||
// Callbacks for UI updates
|
||||
this.onStatusChange = null; // (status, message) => void
|
||||
}
|
||||
|
||||
setRFB(rfb) {
|
||||
this.rfb = rfb;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize clipboard state based on permissions
|
||||
* 根据权限状态初始化剪贴板自动同步
|
||||
*
|
||||
* 注意:默认开启自动同步,因为:
|
||||
* 1. 这是更好的用户体验
|
||||
* 2. Clipboard API 在用户手势(如点击、焦点切换)时才会真正读取
|
||||
* 3. 如果权限被拒绝,会在实际操作时处理
|
||||
*/
|
||||
async init() {
|
||||
Log.Info('[Clipboard] Initializing...');
|
||||
|
||||
// 检查 Clipboard API 是否可用
|
||||
if (!navigator.clipboard) {
|
||||
Log.Warn('[Clipboard] Clipboard API not supported');
|
||||
this._updateStatus('error', _('Clipboard API not supported'));
|
||||
return;
|
||||
}
|
||||
|
||||
// 尝试查询权限状态(某些浏览器可能不支持)
|
||||
let permissionState = 'unknown';
|
||||
|
||||
if (navigator.permissions && navigator.permissions.query) {
|
||||
try {
|
||||
// 注意:某些浏览器不支持 "clipboard-read" 权限查询
|
||||
const result = await navigator.permissions.query({ name: "clipboard-read" });
|
||||
permissionState = result.state;
|
||||
Log.Info('[Clipboard] Permission query result: ' + permissionState);
|
||||
} catch (err) {
|
||||
// 权限查询不支持(如 Firefox),这是正常的
|
||||
Log.Debug('[Clipboard] Permission query not supported: ' + err.message);
|
||||
permissionState = 'unknown';
|
||||
}
|
||||
}
|
||||
|
||||
// 根据权限状态决定行为
|
||||
if (permissionState === 'denied') {
|
||||
// 权限明确被拒绝
|
||||
Log.Info('[Clipboard] Permission denied, disabling auto sync');
|
||||
this.autoSync = false;
|
||||
this._updateStatus('inactive', '');
|
||||
} else {
|
||||
// granted / prompt / unknown 都默认开启
|
||||
// 实际权限检查会在用户手势时进行
|
||||
Log.Info('[Clipboard] Enabling auto sync (permission: ' + permissionState + ')');
|
||||
this.autoSync = true;
|
||||
this._startSync(false); // 不立即执行,等待用户手势
|
||||
this._updateStatus('active', _('Auto sync enabled'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle auto-sync state
|
||||
* @param {boolean} enable
|
||||
* @returns {Promise<boolean>} resulting state
|
||||
*/
|
||||
async toggleAutoSync(enable) {
|
||||
if (enable) {
|
||||
const hasPermission = await this._requestPermission();
|
||||
if (!hasPermission) {
|
||||
this._updateStatus('error', _('Clipboard permission denied'));
|
||||
return false;
|
||||
}
|
||||
|
||||
this.autoSync = true;
|
||||
this._startSync();
|
||||
this._updateStatus('active', _('Auto sync enabled'));
|
||||
Log.Info('[Clipboard] Auto sync enabled');
|
||||
} else {
|
||||
this.autoSync = false;
|
||||
this._stopSync();
|
||||
this._updateStatus('inactive', '');
|
||||
Log.Info('[Clipboard] Auto sync disabled');
|
||||
}
|
||||
return this.autoSync;
|
||||
}
|
||||
|
||||
/**
|
||||
* Receive text from remote and sync to local if enabled
|
||||
* @param {string} text
|
||||
*/
|
||||
handleRemoteText(text) {
|
||||
if (this.autoSync) {
|
||||
this.writeLocal(text);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read from local clipboard manually
|
||||
*/
|
||||
async readLocal() {
|
||||
try {
|
||||
if (!navigator.clipboard || !navigator.clipboard.readText) {
|
||||
this._updateStatus('error', _('Clipboard API not supported'));
|
||||
return null;
|
||||
}
|
||||
|
||||
const text = await navigator.clipboard.readText();
|
||||
this.lastLocalText = text;
|
||||
Log.Debug('[Clipboard] Read from local: ' + text.substr(0, 40) + '...');
|
||||
|
||||
this._updateStatus('success', _('Read from local clipboard'));
|
||||
this._resetStatus();
|
||||
|
||||
return text;
|
||||
} catch (err) {
|
||||
Log.Warn('[Clipboard] Failed to read local: ' + err.message);
|
||||
this._updateStatus('error', _('Failed to read clipboard'));
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write to local clipboard
|
||||
* @param {string} text
|
||||
*/
|
||||
async writeLocal(text) {
|
||||
try {
|
||||
if (!navigator.clipboard || !navigator.clipboard.writeText) {
|
||||
this._updateStatus('error', _('Clipboard API not supported'));
|
||||
return;
|
||||
}
|
||||
|
||||
await navigator.clipboard.writeText(text);
|
||||
this.lastLocalText = text;
|
||||
Log.Debug('[Clipboard] Wrote to local: ' + text.substr(0, 40) + '...');
|
||||
|
||||
this._updateStatus('success', _('Copied to local clipboard'));
|
||||
this._resetStatus();
|
||||
} catch (err) {
|
||||
Log.Warn('[Clipboard] Failed to write local: ' + err.message);
|
||||
this._updateStatus('error', _('Failed to write clipboard'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send text to remote
|
||||
* @param {string} text
|
||||
*/
|
||||
sendToRemote(text) {
|
||||
if (this.rfb) {
|
||||
this.rfb.clipboardPasteFrom(text);
|
||||
Log.Debug('[Clipboard] Sent to remote: ' + text.substr(0, 40) + '...');
|
||||
}
|
||||
}
|
||||
|
||||
// Private methods
|
||||
|
||||
async _requestPermission() {
|
||||
try {
|
||||
if (navigator.clipboard && navigator.clipboard.readText) {
|
||||
await navigator.clipboard.readText();
|
||||
this.permissionGranted = true;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
} catch (err) {
|
||||
if (err.name === 'NotAllowedError') {
|
||||
return false;
|
||||
}
|
||||
// Other errors (like empty clipboard) usually mean we have permission
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
_startSync(runImmediately = true) {
|
||||
this.syncFocusHandler = async () => {
|
||||
if (!this.autoSync || !this.rfb) return;
|
||||
|
||||
try {
|
||||
// Focus event is a user gesture, so readText should work/prompt
|
||||
const text = await navigator.clipboard.readText();
|
||||
if (text && text !== this.lastLocalText) {
|
||||
this.lastLocalText = text;
|
||||
this.sendToRemote(text);
|
||||
}
|
||||
} catch (err) {
|
||||
// If it fails (e.g. denied), we might want to turn off autoSync?
|
||||
// For now, just log and maybe show status
|
||||
Log.Warn('[Clipboard] Auto-sync read failed: ' + err);
|
||||
if (err.name === 'NotAllowedError') {
|
||||
this._updateStatus('error', _('Clipboard permission denied'));
|
||||
this.autoSync = false; // Disable if denied
|
||||
// We might want to notify UI to uncheck box
|
||||
// But we don't have a direct way to sync back to checkbox unless we use callback
|
||||
// Current arch: init sets checkbox. toggle sets checkbox.
|
||||
// If we change state here, checkbox might desync.
|
||||
// We can use onStatusChange to signal "error"? Or add onStateChange?
|
||||
// For now, keep simple.
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('focus', this.syncFocusHandler);
|
||||
if (runImmediately) {
|
||||
this.syncFocusHandler(); // Initial sync
|
||||
}
|
||||
}
|
||||
|
||||
_stopSync() {
|
||||
if (this.syncFocusHandler) {
|
||||
window.removeEventListener('focus', this.syncFocusHandler);
|
||||
this.syncFocusHandler = null;
|
||||
}
|
||||
this.lastLocalText = null;
|
||||
}
|
||||
|
||||
_updateStatus(status, message) {
|
||||
if (this.onStatusChange) {
|
||||
this.onStatusChange(status, message);
|
||||
}
|
||||
}
|
||||
|
||||
_resetStatus() {
|
||||
setTimeout(() => {
|
||||
if (this.autoSync) {
|
||||
this._updateStatus('active', _('Auto sync enabled'));
|
||||
} else {
|
||||
this._updateStatus('inactive', '');
|
||||
}
|
||||
}, 2000);
|
||||
}
|
||||
}
|
||||
|
||||
const clipboardManager = new ClipboardManager();
|
||||
export default clipboardManager;
|
||||
Reference in New Issue
Block a user