diff --git a/ui/lion/hooks/useColor.ts b/ui/lion/hooks/useColor.ts
deleted file mode 100644
index dd10643ea..000000000
--- a/ui/lion/hooks/useColor.ts
+++ /dev/null
@@ -1,205 +0,0 @@
-import { ref } from "vue";
-
-interface HSL {
- h: number;
- s: number;
- l: number;
-}
-
-const mainThemeColorMap = new Map(
- Object.entries({
- default: "#483D3D",
- deepBlue: "#1A212C",
- darkGary: "#303237"
- })
-);
-
-const currentMainColoc = ref("#303237");
-
-export const useColor = () => {
- const setCurrentMainColor = (color: string) => {
- const themeColor = mainThemeColorMap.get(color);
-
- if (themeColor) {
- currentMainColoc.value = themeColor;
- } else {
- currentMainColoc.value = "#483D3D";
- }
- };
-
- /**
- * 将十六进制颜色转换为HSL颜色
- * @param hex 十六进制颜色
- * @returns HSL颜色
- */
- const hexToHSL = (hex: string): HSL => {
- let hexValue = hex.replace(/^#/, "");
-
- if (hexValue.length === 3) {
- hexValue = hexValue
- .split("")
- .map((char) => char + char)
- .join("");
- }
-
- // 解析RGB值
- const r = Number.parseInt(hexValue.substring(0, 2), 16) / 255;
- const g = Number.parseInt(hexValue.substring(2, 4), 16) / 255;
- const b = Number.parseInt(hexValue.substring(4, 6), 16) / 255;
-
- // 计算HSL值
- const max = Math.max(r, g, b);
- const min = Math.min(r, g, b);
- let h = 0;
- let s = 0;
- const l = (max + min) / 2;
-
- if (max !== min) {
- const d = max - min;
- s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
-
- switch (max) {
- case r:
- h = (g - b) / d + (g < b ? 6 : 0);
- break;
- case g:
- h = (b - r) / d + 2;
- break;
- case b:
- h = (r - g) / d + 4;
- break;
- }
-
- h /= 6;
- }
-
- // 转换为标准HSL格式
- return {
- h: Math.round(h * 360),
- s: Math.round(s * 100),
- l: Math.round(l * 100)
- };
- };
-
- /**
- * 将HSL颜色转换为十六进制颜色
- * @param h 色相
- * @param s 饱和度
- * @param l 亮度
- * @returns 十六进制颜色
- */
- const hslToHex = (h: number, s: number, l: number) => {
- h /= 360;
- s /= 100;
- l /= 100;
-
- let r, g, b;
-
- if (s === 0) {
- // 如果饱和度为0,则为灰色
- r = g = b = l;
- } else {
- const hue2rgb = (p: number, q: number, t: number): number => {
- if (t < 0) t += 1;
- if (t > 1) t -= 1;
- if (t < 1 / 6) return p + (q - p) * 6 * t;
- if (t < 1 / 2) return q;
- if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
- return p;
- };
-
- const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
- const p = 2 * l - q;
-
- r = hue2rgb(p, q, h + 1 / 3);
- g = hue2rgb(p, q, h);
- b = hue2rgb(p, q, h - 1 / 3);
- }
-
- // 转换为十六进制
- const toHex = (x: number): string => {
- const hex = Math.round(x * 255).toString(16);
- return hex.length === 1 ? `0${hex}` : hex;
- };
-
- return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
- };
-
- /**
- * 将颜色转换为rgba格式
- * @param alphaValue 透明度值
- * @param color 颜色
- * @returns rgba格式颜色
- */
- const alpha = (alphaValue: number, color?: string) => {
- // 如果没有提供颜色,使用当前主题颜色
- const actualColor = color || currentMainColoc.value;
- // 确保透明度值在0-1之间
- const alpha = Math.max(0, Math.min(1, alphaValue));
-
- // 移除#号并处理缩写形式
- let hex = actualColor.replace(/^#/, "");
-
- if (hex.length === 3) {
- hex = hex
- .split("")
- .map((char) => char + char)
- .join("");
- }
-
- // 解析RGB值
- const r = Number.parseInt(hex.substring(0, 2), 16);
- const g = Number.parseInt(hex.substring(2, 4), 16);
- const b = Number.parseInt(hex.substring(4, 6), 16);
-
- // 返回rgba格式
- return `rgba(${r}, ${g}, ${b}, ${alpha})`;
- };
-
- /**
- * 将颜色变亮
- * @param amount 调整的亮度值
- * @param color 要处理的颜色,默认使用当前主题色
- * @param alphaValue 可选透明度,传入后返回 rgba 字符串
- * @returns 调亮后的十六进制颜色,或带透明度的 rgba 字符串
- */
- const lighten = (amount: number, color?: string, alphaValue?: number) => {
- const actualColor = color || currentMainColoc.value;
- const hsl = hexToHSL(actualColor);
- const hexColor = hslToHex(hsl.h, hsl.s, Math.min(100, hsl.l + amount));
-
- if (alphaValue !== undefined) {
- return alpha(alphaValue, hexColor);
- }
-
- return hexColor;
- };
-
- /**
- * 将颜色变暗
- * @param amount 调整的亮度值
- * @param color 要处理的颜色,默认使用当前主题色
- * @param alphaValue 可选透明度,传入后返回 rgba 字符串
- * @returns 调暗后的十六进制颜色,或带透明度的 rgba 字符串
- */
- const darken = (amount: number, color?: string, alphaValue?: number) => {
- const actualColor = color || currentMainColoc.value;
- const hsl = hexToHSL(actualColor);
- const hexColor = hslToHex(hsl.h, hsl.s, Math.max(0, hsl.l - amount));
-
- // 如果提供了透明度参数,应用透明度
- if (alphaValue !== undefined) {
- return alpha(alphaValue, hexColor);
- }
-
- return hexColor;
- };
-
- return {
- darken,
- lighten,
- alpha,
- setCurrentMainColor,
- currentMainColor: currentMainColoc
- };
-};
diff --git a/ui/lion/hooks/useGuacamoleClient.ts b/ui/lion/hooks/useGuacamoleClient.ts
index c36f2957c..c148e1662 100644
--- a/ui/lion/hooks/useGuacamoleClient.ts
+++ b/ui/lion/hooks/useGuacamoleClient.ts
@@ -3,18 +3,16 @@ import type { LionUploadCustomRequestOptions } from "@/lion/types/upload";
import { useDebounceFn } from "@vueuse/core";
import * as Guacamole from "guacamole-common-js-jumpserver/dist/guacamole-common";
-import { nextTick, ref, toValue } from "vue";
-import { LanguageCode } from "@/lion/locales";
+import { computed, nextTick, ref, toValue } from "vue";
import { LUNA_MESSAGE_TYPE } from "@/lion/types/postmessage.type";
import { withLionUrl } from "@/lion/utils/base";
-import { readClipboardText } from "@/lion/utils/clipboard";
+import { readClipboardText, writeClipboardBlob, writeClipboardText } from "@/lion/utils/clipboard";
+import { LanguageCode } from "@/lion/utils/config";
import { lunaCommunicator } from "@/lion/utils/lunaBus";
-import { ConvertGuacamoleError, ErrorStatusCodes } from "@/lion/utils/status";
+import { ConvertGuacamoleError as convertGuacamoleError, ErrorStatusCodes } from "@/lion/utils/status";
-const supportImages: any[] = [];
-const pendingTests: any[] = [];
-const testImages: any = {
+const testImages: Record
= {
/**
* Test JPEG image, encoded as base64.
*/
@@ -37,8 +35,9 @@ const testImages: any = {
* Test WebP image, encoded as base64.
*/
"image/webp": "UklGRhoAAABXRUJQVlA4TA0AAAAvAAAAEAcQERGIiP4HAA=="
-}; // 测试单个图片格式
-async function testImageFormat(mimeType: string, base64Data: any): Promise {
+};
+
+async function testImageFormat(mimeType: string, base64Data: string): Promise {
return new Promise((resolve) => {
const image = new Image();
@@ -58,54 +57,19 @@ async function testImageFormat(mimeType: string, base64Data: any): Promise {
- const imageTest = new Promise((resolve) => {
- const image = new Image();
-
- // Set up handlers before setting src to avoid race conditions
- image.onload = () => {
- // Image format is supported if successfully decoded with correct dimensions
- if (image.width === 1 && image.height === 1) {
- supportImages.push(mimeType);
- }
- resolve();
- };
-
- // Handle errors separately for better debugging
- image.onerror = () => {
- console.debug(`Format ${mimeType} not supported`);
- resolve(); // Still resolve to continue testing other formats
- };
-
- // Set source to trigger loading
- image.src = `data:${mimeType};base64,${base64Data}`;
- });
-
- pendingTests.push(imageTest);
-});
const FileType = {
NORMAL: "NORMAL",
DIRECTORY: "DIRECTORY"
};
export async function getSupportedImages(): Promise {
- // 清空之前的结果
- supportImages.length = 0;
-
- // 并行测试所有图片格式
- const testPromises = Object.entries(testImages).map(async ([mimeType, base64Data]) => {
- const isSupported = await testImageFormat(mimeType, base64Data);
- if (isSupported) {
- supportImages.push(mimeType);
- }
- return { mimeType, isSupported };
- });
-
- // 等待所有测试完成
- await Promise.all(testPromises);
-
- return [...supportImages]; // 返回副本
+ const results = await Promise.all(
+ Object.entries(testImages).map(async ([mimeType, base64Data]) => ({
+ mimeType,
+ isSupported: await testImageFormat(mimeType, base64Data)
+ }))
+ );
+ return results.filter((result) => result.isSupported).map((result) => result.mimeType);
}
export async function getSupportedGuacVideos(): Promise {
return Guacamole.VideoPlayer.getSupportedTypes();
@@ -132,15 +96,22 @@ export async function getSupportedGuacMimeTypes(): Promise {
return connectString;
}
+let supportedMimeTypesPromise: Promise> | null = null;
+
export async function getSupportedMimeTypes(): Promise> {
- const supportImages = await getSupportedImages();
- const supportVideos = await getSupportedGuacVideos();
- const supportAudios = await getSupportedGuacAudios();
- return {
- GUAC_IMAGE: supportImages,
- GUAC_VIDEO: supportVideos,
- GUAC_AUDIO: supportAudios
- };
+ if (!supportedMimeTypesPromise) {
+ supportedMimeTypesPromise = Promise.all([getSupportedImages(), getSupportedGuacVideos(), getSupportedGuacAudios()])
+ .then(([supportImages, supportVideos, supportAudios]) => ({
+ GUAC_IMAGE: supportImages,
+ GUAC_VIDEO: supportVideos,
+ GUAC_AUDIO: supportAudios
+ }))
+ .catch((error) => {
+ supportedMimeTypesPromise = null;
+ throw error;
+ });
+ }
+ return await supportedMimeTypesPromise;
}
const sanitizeFilename = (filename: string) => {
@@ -160,7 +131,25 @@ interface GuacamoleFile {
is_dir?: boolean;
}
-export function useGuacamoleClient(t: any, endpointUrl?: MaybeRefOrGetter) {
+interface ClipboardPolicyItem {
+ enabled?: boolean;
+ text_limit?: number;
+ file_size_limit?: number;
+}
+
+interface ClipboardPolicy {
+ copy?: ClipboardPolicyItem;
+ paste?: ClipboardPolicyItem;
+}
+
+const BYTES_PER_MEGABYTE = 1024 * 1024;
+const getTextLength = (text: string) => Array.from(text).length;
+
+export function useGuacamoleClient(
+ t: any,
+ endpointUrl?: MaybeRefOrGetter,
+ requestAuth?: MaybeRefOrGetter<{ ticket?: string; token?: string }>
+) {
const toast = useToast();
const { addErrorToast } = useErrorToast();
const message = {
@@ -187,14 +176,17 @@ export function useGuacamoleClient(t: any, endpointUrl?: MaybeRefOrGetter(null);
const pixelDensity = 1;
const sink = new Guacamole.InputSink();
const keyboard = new Guacamole.Keyboard();
+ const pressedKeys = ref>(new Set());
const isRemoteApp = ref(false);
const isHttpProtocol = ref(false);
const remoteClipboardText = ref("");
+ const clipboardPasteTextLimit = computed(() => getClipboardTextLimit("paste"));
let connectGeneration = 0;
+ let inputCleanup: (() => void) | null = null;
+ let keyboardListening = false;
const currentFolderFiles = ref([]);
const current_files = ref({});
const currentFolder = ref(null);
@@ -202,17 +194,45 @@ export function useGuacamoleClient(t: any, endpointUrl?: MaybeRefOrGetter(null);
- const getBaseApiUrl = () => withLionUrl("/api", toValue(endpointUrl) || window.location.origin);
+ const getApiUrl = (path: string) => {
+ const url = new URL(withLionUrl(`/api${path}`, toValue(endpointUrl) || window.location.origin));
+ const auth = requestAuth ? toValue(requestAuth) : undefined;
+ if (auth?.ticket) url.searchParams.set("ticket", auth.ticket);
+ if (auth?.token) url.searchParams.set("token", auth.token);
+ return url.toString();
+ };
function disconnectGuaclient() {
connectGeneration += 1;
- if (guaClient.value) {
- guaClient.value.disconnect();
+ if (warningIntervalId.value !== null) {
+ window.clearInterval(warningIntervalId.value);
+ warningIntervalId.value = null;
}
- guaDisplay.value?.getElement()?.remove();
+ inputCleanup?.();
+ inputCleanup = null;
+ keyboard.reset();
+ pressedKeys.value.clear();
+ const client = guaClient.value;
+ const tunnel = guaTunnel.value;
+ const display = guaDisplay.value;
guaClient.value = null;
guaTunnel.value = null;
guaDisplay.value = null;
+ if (client) {
+ client.onstatechange = null;
+ client.onerror = null;
+ client.onclipboard = null;
+ client.onfile = null;
+ client.onfilesystem = null;
+ client.disconnect();
+ }
+ if (tunnel) {
+ tunnel.onerror = null;
+ tunnel.oninstruction = null;
+ }
+ if (display) display.onresize = null;
+ display?.getElement()?.remove();
+ loading.value = false;
}
function connectToGuacamole(
@@ -233,11 +253,11 @@ export function useGuacamoleClient(t: any, endpointUrl?: MaybeRefOrGetter {
+ loading.value = false;
message.error(t("WebSocketError"));
};
tunnel.onuuid = (uuid: string) => {
tunnel.uuid = uuid;
- console.log("WebSocket UUID:", uuid);
};
const oninstruction = tunnel.oninstruction;
@@ -258,10 +278,7 @@ export function useGuacamoleClient(t: any, endpointUrl?: MaybeRefOrGetter {
- console.log("Guacamole display resized:", resizeEvent);
- updateScale();
- };
+ display.onresize = updateScale;
display.showCursor(false);
guaDisplay.value = display;
guaClient.value = client;
@@ -286,12 +303,69 @@ export function useGuacamoleClient(t: any, endpointUrl?: MaybeRefOrGetter {
if (generation !== connectGeneration) return;
- console.log("Connecting to Guacamole with params:", queryParams.toString());
client.connect(queryParams.toString());
});
}
+ function getClipboardPolicyItem(direction: "copy" | "paste"): ClipboardPolicyItem | null {
+ const policy = action_permission.value?.clipboard_policy as ClipboardPolicy | undefined;
+ return policy?.[direction] || null;
+ }
+
+ function getClipboardTextLimit(direction: "copy" | "paste") {
+ const limit = Number(getClipboardPolicyItem(direction)?.text_limit || 0);
+ return Number.isFinite(limit) && limit > 0 ? limit : 0;
+ }
+
+ function getClipboardFileSizeLimit(direction: "copy" | "paste") {
+ const limit = Number(getClipboardPolicyItem(direction)?.file_size_limit || 0);
+ return Number.isFinite(limit) && limit > 0 ? limit : 0;
+ }
+
+ function canUseClipboardDirection(direction: "copy" | "paste") {
+ return Boolean(direction === "copy" ? action_permission.value?.enable_copy : action_permission.value?.enable_paste);
+ }
+
+ function isClipboardDirectionDeniedByPolicy(direction: "copy" | "paste") {
+ return getClipboardPolicyItem(direction)?.enabled === false;
+ }
+
+ function showClipboardPermissionWarning(direction: "copy" | "paste") {
+ if (isClipboardDirectionDeniedByPolicy(direction)) {
+ message.warning(t(direction === "copy" ? "ClipboardCopyDeniedByPolicy" : "ClipboardPasteDeniedByPolicy"));
+ return;
+ }
+ message.warning(`${t(direction === "copy" ? "Copy" : "Paste")} ${t("NoPermission")}`);
+ }
+
+ function validateClipboardText(direction: "copy" | "paste", text: string) {
+ if (!canUseClipboardDirection(direction)) {
+ showClipboardPermissionWarning(direction);
+ return false;
+ }
+ const limit = getClipboardTextLimit(direction);
+ if (limit > 0 && getTextLength(text) > limit) {
+ message.warning(`${t(direction === "copy" ? "Copy" : "Paste")} ${t("ClipboardTextLimitExceeded")}: ${limit}`);
+ return false;
+ }
+ return true;
+ }
+
+ function validateClipboardBlob(direction: "copy" | "paste", size: number) {
+ if (!canUseClipboardDirection(direction)) {
+ showClipboardPermissionWarning(direction);
+ return false;
+ }
+ const limit = getClipboardFileSizeLimit(direction);
+ if (limit > 0 && size > limit * BYTES_PER_MEGABYTE) {
+ message.warning(`${t(direction === "copy" ? "Copy" : "Paste")} ${t("ClipboardFileSizeLimitExceeded")}: ${limit}`);
+ return false;
+ }
+ return true;
+ }
+
function sendTextToRemote(text: string) {
+ if (!validateClipboardText("paste", text)) return;
const data = {
type: "text/plain",
data: text
@@ -319,11 +393,12 @@ export function useGuacamoleClient(t: any, endpointUrl?: MaybeRefOrGetter {
- const text = await readClipboardText();
- if (!text || !text.trim()) {
- return;
+ try {
+ const text = await readClipboardText();
+ if (text?.trim()) sendTextToRemote(text);
+ } catch (error) {
+ console.debug("Unable to read local clipboard", error);
}
- sendTextToRemote(text);
}, 300);
const registerMouseAndKeyboardHanlder = () => {
@@ -331,10 +406,10 @@ export function useGuacamoleClient(t: any, endpointUrl?: MaybeRefOrGetter {
document.body.focus();
+ display.showCursor(false);
nextTick(() => {
sink.focus();
});
@@ -353,6 +429,24 @@ export function useGuacamoleClient(t: any, endpointUrl?: MaybeRefOrGetter {
+ displayEl.removeEventListener("mouseenter", handleMouseEnter);
+ displayEl.removeEventListener("mouseleave", handleMouseLeave);
+ keyboard.reset();
+ keyboard.onkeydown = null;
+ keyboard.onkeyup = null;
+ if (mouse) {
+ mouse.onmousedown = null;
+ mouse.onmouseup = null;
+ mouse.onmousemove = null;
+ mouse.onmouseout = null;
+ }
+ if (touchScreen) {
+ touchScreen.onmousedown = null;
+ touchScreen.onmousemove = null;
+ touchScreen.onmouseup = null;
+ }
+ };
};
const resizeGuaScale = useDebounceFn((width: number, height: number) => {
@@ -363,7 +457,6 @@ export function useGuacamoleClient(t: any, endpointUrl?: MaybeRefOrGetter {
if (guaClient.value && guaDisplay.value) {
- console.log("Sending resize to Guacamole client:", width, height);
guaClient.value.sendSize(width, height);
}
};
@@ -380,15 +473,20 @@ export function useGuacamoleClient(t: any, endpointUrl?: MaybeRefOrGetter {
message.warning(warningMsg);
}, 1000 * 31);
@@ -481,7 +579,6 @@ export function useGuacamoleClient(t: any, endpointUrl?: MaybeRefOrGetter>(new Set());
const isBlockedCombination = (keysym: number): boolean => {
if (!isRemoteApp.value) {
return false;
@@ -517,7 +614,10 @@ export function useGuacamoleClient(t: any, endpointUrl?: MaybeRefOrGetter {
if (isBlockedCombination(keysym)) {
@@ -542,12 +642,12 @@ export function useGuacamoleClient(t: any, endpointUrl?: MaybeRefOrGetter {
@@ -573,6 +673,7 @@ export function useGuacamoleClient(t: any, endpointUrl?: MaybeRefOrGetter {
sendScaledMouseState(client, mouseState);
@@ -623,6 +724,7 @@ export function useGuacamoleClient(t: any, endpointUrl?: MaybeRefOrGetter {
- console.log("Audio stream closed");
- requestAudioStream(client); // 重新请求音频流
+ if (guaClient.value === client) requestAudioStream(client);
}; // 重新请求音频流
}
@@ -769,9 +867,11 @@ export function useGuacamoleClient(t: any, endpointUrl?: MaybeRefOrGetter {
- if (file.percentage && file.percentage < 97) {
- file.percentage += 1;
- } else {
- if (fakeProcessInterval.value) {
- clearInterval(fakeProcessInterval.value);
- fakeProcessInterval.value = null;
- }
- }
- }, 1000 * 2);
const progressCallback = (e: any) => {
- console.log("Upload progress:", e.loaded, "/", e.total, "for file:", file.name);
- options.file.percentage = (e.loaded / e.total) * 100 - 40;
+ if (e.lengthComputable && e.total > 0) {
+ options.file.percentage = Math.min(99, (e.loaded / e.total) * 100);
+ }
};
try {
@@ -958,12 +1044,6 @@ export function useGuacamoleClient(t: any, endpointUrl?: MaybeRefOrGetter {
- console.log("clipboard received from remote: ", data);
+ if (!validateClipboardText("copy", data)) return;
remoteClipboardText.value = data;
- if (navigator.clipboard) {
- await navigator.clipboard.writeText(data);
+ try {
+ await writeClipboardText(data);
+ } catch (error) {
+ console.debug("Unable to write local clipboard", error);
}
};
} else {
// Otherwise read the clipboard data as a Blob
reader = new Guacamole.BlobReader(stream, mimetype);
- reader.onprogress = (text: any) => {
- console.log("clipboard blob text received from remote: ", text);
- };
- reader.onend = () => {
+ reader.onend = async () => {
const blob = reader.getBlob();
- console.log("clipboard blob received from remote: ", blob);
- navigator.clipboard.write(blob);
+ if (!validateClipboardBlob("copy", blob.size)) return;
+ try {
+ await writeClipboardBlob(blob);
+ } catch (error) {
+ console.debug("Unable to write binary clipboard data", error);
+ }
};
}
}
@@ -1126,6 +1209,7 @@ export function useGuacamoleClient(t: any, endpointUrl?: MaybeRefOrGetter("create_koko_connect_ticket", {
+ baseUrl,
+ tokenId
+ });
+ if (!result.ticket) throw new Error("Koko did not return a Lion connect ticket");
+ return String(result.ticket);
+}
diff --git a/ui/lion/hooks/useLionEndpoint.ts b/ui/lion/hooks/useLionEndpoint.ts
new file mode 100644
index 000000000..1744a0ecd
--- /dev/null
+++ b/ui/lion/hooks/useLionEndpoint.ts
@@ -0,0 +1,17 @@
+import type { MaybeRefOrGetter } from "vue";
+import { computed, toValue } from "vue";
+
+import { useUserInfoStore } from "~/store/modules/userInfo";
+import { isTauriRuntime } from "~/utils/runtime";
+
+export function useLionEndpoint(explicitEndpoint?: MaybeRefOrGetter) {
+ const userInfoStore = useUserInfoStore();
+
+ return computed(() => {
+ const explicit = String(toValue(explicitEndpoint) || "").trim();
+ if (explicit) return explicit;
+ if (!import.meta.client) return "";
+ if (isTauriRuntime() && userInfoStore.currentSite) return userInfoStore.currentSite;
+ return window.location.origin;
+ });
+}
diff --git a/ui/lion/locales/index.ts b/ui/lion/locales/index.ts
index c58890664..35575e58b 100644
--- a/ui/lion/locales/index.ts
+++ b/ui/lion/locales/index.ts
@@ -1,3 +1,28 @@
+import { apiRequest } from "@/composables/useApiRequest";
+import { LanguageCode } from "@/lion/utils/config";
+
export { default as date } from "./date";
export { message } from "./modules";
-export { LanguageCode } from "@/lion/utils/config";
+export { LanguageCode };
+
+type LionTranslations = Record>;
+
+const normalizedLangCode = LanguageCode.toLowerCase();
+let remoteTranslationsPromise: Promise | null = null;
+
+export const loadRemoteTranslations = () => {
+ if (!remoteTranslationsPromise) {
+ remoteTranslationsPromise = apiRequest({
+ method: "GET",
+ path: "/api/v1/settings/i18n/lion/",
+ query: {
+ lang: normalizedLangCode,
+ flat: 0
+ }
+ }).catch((error) => {
+ remoteTranslationsPromise = null;
+ throw error;
+ });
+ }
+ return remoteTranslationsPromise;
+};
diff --git a/ui/lion/locales/modules/en.json b/ui/lion/locales/modules/en.json
index 0967ef424..840beffdc 100644
--- a/ui/lion/locales/modules/en.json
+++ b/ui/lion/locales/modules/en.json
@@ -1 +1,15 @@
-{}
+{
+ "ClipboardTextLimitExceeded": "clipboard text exceeds the limit",
+ "ClipboardFileSizeLimitExceeded": "clipboard file exceeds the limit",
+ "ClipboardCopyDeniedByPolicy": "Clipboard control policy prevents copying",
+ "ClipboardPasteDeniedByPolicy": "Clipboard control policy prevents pasting",
+ "Copy": "Copy",
+ "Paste": "Paste",
+ "NoPermission": "permission denied",
+ "LoadMore": "Load more",
+ "Uploading": "Uploading",
+ "Waiting": "Waiting",
+ "Forward": "Forward",
+ "ZoomIn": "Zoom in",
+ "ZoomOut": "Zoom out"
+}
diff --git a/ui/lion/locales/modules/ja.json b/ui/lion/locales/modules/ja.json
index 0967ef424..0b51ce444 100644
--- a/ui/lion/locales/modules/ja.json
+++ b/ui/lion/locales/modules/ja.json
@@ -1 +1,15 @@
-{}
+{
+ "ClipboardTextLimitExceeded": "クリップボードのテキストが制限を超えています",
+ "ClipboardFileSizeLimitExceeded": "クリップボードのファイルが制限を超えています",
+ "ClipboardCopyDeniedByPolicy": "クリップボード制御ルールによりコピーできません",
+ "ClipboardPasteDeniedByPolicy": "クリップボード制御ルールにより貼り付けできません",
+ "Copy": "コピー",
+ "Paste": "貼り付け",
+ "NoPermission": "権限がありません",
+ "LoadMore": "さらに読み込む",
+ "Uploading": "アップロード中",
+ "Waiting": "待機中",
+ "Forward": "進む",
+ "ZoomIn": "拡大",
+ "ZoomOut": "縮小"
+}
diff --git a/ui/lion/locales/modules/zh.json b/ui/lion/locales/modules/zh.json
index 0967ef424..60204f101 100644
--- a/ui/lion/locales/modules/zh.json
+++ b/ui/lion/locales/modules/zh.json
@@ -1 +1,15 @@
-{}
+{
+ "ClipboardTextLimitExceeded": "剪贴板文本超过限制",
+ "ClipboardFileSizeLimitExceeded": "剪贴板文件超过限制",
+ "ClipboardCopyDeniedByPolicy": "触发剪贴板控制规则,无法复制",
+ "ClipboardPasteDeniedByPolicy": "触发剪贴板控制规则,无法粘贴",
+ "Copy": "复制",
+ "Paste": "粘贴",
+ "NoPermission": "无权限",
+ "LoadMore": "加载更多",
+ "Uploading": "上传中",
+ "Waiting": "等待中",
+ "Forward": "前进",
+ "ZoomIn": "放大",
+ "ZoomOut": "缩小"
+}
diff --git a/ui/lion/locales/modules/zh_Hant.json b/ui/lion/locales/modules/zh_Hant.json
index 0967ef424..c96b33c5e 100644
--- a/ui/lion/locales/modules/zh_Hant.json
+++ b/ui/lion/locales/modules/zh_Hant.json
@@ -1 +1,15 @@
-{}
+{
+ "ClipboardTextLimitExceeded": "剪貼簿文字超過限制",
+ "ClipboardFileSizeLimitExceeded": "剪貼簿檔案超過限制",
+ "ClipboardCopyDeniedByPolicy": "觸發剪貼簿控制規則,無法複製",
+ "ClipboardPasteDeniedByPolicy": "觸發剪貼簿控制規則,無法貼上",
+ "Copy": "複製",
+ "Paste": "貼上",
+ "NoPermission": "無權限",
+ "LoadMore": "載入更多",
+ "Uploading": "上傳中",
+ "Waiting": "等待中",
+ "Forward": "前進",
+ "ZoomIn": "放大",
+ "ZoomOut": "縮小"
+}
diff --git a/ui/lion/styles/base.css b/ui/lion/styles/base.css
index ec70c1fe4..15f1d5dc1 100644
--- a/ui/lion/styles/base.css
+++ b/ui/lion/styles/base.css
@@ -1,50 +1,6 @@
-@import "tailwindcss";
@import "./keyboard.css";
-@font-face {
- font-family: "Open Sans";
- src: url("./fonts/OpenSans-Regular.ttf");
- font-weight: normal;
- font-style: normal;
-}
-
-@font-face {
- font-family: "Open Sans";
- src: url("./fonts/OpenSans-Bold.ttf");
- font-weight: bold;
- font-style: normal;
-}
-
-@font-face {
- font-family: "Open Sans";
- src: url("./fonts/OpenSans-Light.ttf");
- font-weight: 300;
- font-style: normal;
-}
-
-@font-face {
- font-family: "Open Sans";
- src: url("./fonts/OpenSans-Italic.ttf");
- font-weight: 300;
- font-style: italic;
-}
-
-body {
- height: 100%;
- /* -moz-osx-font-smoothing: grayscale; */
- -webkit-font-smoothing: auto;
- background-color: #000000;
- font-family: "open sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
- font-size: 13px;
- line-height: 1.428;
-}
-
-::-webkit-scrollbar-track {
- box-shadow: inset 0 0 2px rgba(0, 0, 0, 0.3);
- background-color: #0a0a0a;
-}
-
-::-webkit-scrollbar-thumb {
- background-color: #494141;
- border-radius: 6px;
+.lion-surface {
+ color: var(--app-text-primary);
+ background: var(--workspace-surface-background);
}
diff --git a/ui/lion/styles/keyboard.css b/ui/lion/styles/keyboard.css
index 917d33563..450b1fb8d 100644
--- a/ui/lion/styles/keyboard.css
+++ b/ui/lion/styles/keyboard.css
@@ -29,25 +29,19 @@
top: 0;
bottom: 0;
- background: #444;
+ background: var(--app-surface-header);
- border: 0.125em solid #666;
+ border: 0.125em solid var(--app-border-strong);
-moz-border-radius: 0.25em;
-webkit-border-radius: 0.25em;
-khtml-border-radius: 0.25em;
border-radius: 0.25em;
- color: white;
+ color: var(--app-text-primary);
font-size: 40%;
font-weight: lighter;
text-align: center;
white-space: pre;
-
- text-shadow:
- 1px 1px 0 rgba(0, 0, 0, 0.25),
- 1px -1px 0 rgba(0, 0, 0, 0.25),
- -1px 1px 0 rgba(0, 0, 0, 0.25),
- -1px -1px 0 rgba(0, 0, 0, 0.25);
}
.guac-keyboard .guac-keyboard-key:hover {
@@ -55,8 +49,8 @@
}
.guac-keyboard .guac-keyboard-key.highlight {
- background: #666;
- border-color: #666;
+ background: var(--app-state-hover-strong);
+ border-color: var(--app-border-strong);
}
/* Align some keys to the left */
@@ -97,13 +91,15 @@
/* Active latin */
.guac-keyboard.guac-keyboard-modifier-lat .guac-keyboard-key-latin {
- background: #882;
- border-color: #dd4;
+ color: var(--app-accent-foreground);
+ background: var(--theme-accent);
+ border-color: color-mix(in srgb, var(--theme-accent) 72%, var(--app-border-strong));
}
.guac-keyboard .guac-keyboard-key.guac-keyboard-pressed {
- background: #822;
- border-color: #d44;
+ color: var(--app-text-inverse);
+ background: var(--color-bg-error);
+ border-color: var(--color-border-error);
}
.guac-keyboard .guac-keyboard-group {
diff --git a/ui/lion/utils/clipboard.ts b/ui/lion/utils/clipboard.ts
index 98fab704d..8dd9a0472 100644
--- a/ui/lion/utils/clipboard.ts
+++ b/ui/lion/utils/clipboard.ts
@@ -1,12 +1 @@
-export async function readClipboardText(): Promise {
- try {
- if (navigator.clipboard && navigator.clipboard.readText) {
- return await navigator.clipboard.readText();
- }
- console.log("navigator.clipboard api not found");
- return "";
- } catch (err) {
- console.error("Failed to read clipboard:", err);
- return "";
- }
-}
+export { readClipboardText, writeClipboardBlob, writeClipboardText } from "~/utils/clipboard";
diff --git a/ui/lion/utils/config.ts b/ui/lion/utils/config.ts
index 66a1e115b..f192857e4 100644
--- a/ui/lion/utils/config.ts
+++ b/ui/lion/utils/config.ts
@@ -1,5 +1,3 @@
-export { BASE_URL, BASE_WS_URL } from "./base";
-
const readCookie = (name: string) => {
if (!import.meta.client) return "";
const match = document.cookie.match(new RegExp(`(?:^|; )${name}=([^;]+)`));
@@ -11,161 +9,3 @@ const browserLang = import.meta.client
: "en";
export const LanguageCode = readCookie("django_language") || readCookie("lang") || browserLang || "en";
-export const ThemeCode = import.meta.client ? localStorage.getItem("themeType") || "default" : "default";
-
-export const MaxTimeout = 30 * 1000;
-
-export const MAX_TRANSFER_SIZE = 1024 * 1024 * 500;
-
-export const defaultTheme = {
- background: "#121414",
- foreground: "#ffffff",
- black: "#2e3436",
- red: "#cc0000",
- green: "#4e9a06",
- yellow: "#c4a000",
- blue: "#3465a4",
- magenta: "#75507b",
- cyan: "#06989a",
- white: "#d3d7cf",
- brightBlack: "#555753",
- brightRed: "#ef2929",
- brightGreen: "#8ae234",
- brightYellow: "#fce94f",
- brightBlue: "#729fcf",
- brightMagenta: "#ad7fa8",
- brightCyan: "#34e2e2",
- brightWhite: "#eeeeec"
-};
-
-// 图片类型的
-export const FILE_SUFFIX_IMAGE = ["jpg", "jpeg", "png", "gif", "bmp", "webp", "ico", "svg", "heic", "heif"];
-// 音频类型的
-export const FILE_SUFFIX_AUDIO = ["mp3", "wav", "ogg", "m4a", "aac", "flac", "m4b", "m4p", "m4b", "m4p", "m4b", "m4p"];
-// 视频类型的
-export const FILE_SUFFIX_VIDEO = [
- "mp4",
- "avi",
- "mov",
- "wmv",
- "flv",
- "mpeg",
- "mpg",
- "m4v",
- "mkv",
- "webm",
- "vob",
- "m2ts",
- "mts",
- "ts",
- "m2t",
- "m2ts",
- "mts",
- "ts",
- "m2t",
- "m2ts"
-];
-// 压缩包类型的
-export const FILE_SUFFIX_COMPRESSION = [
- "zip",
- "rar",
- "7z",
- "tar",
- "gz",
- "bz2",
- "iso",
- "dmg",
- "pkg",
- "deb",
- "rpm",
- "msi",
- "exe",
- "app",
- "dmg",
- "pkg",
- "deb",
- "rpm",
- "msi",
- "exe",
- "app"
-];
-// 文档类型的
-export const FILE_SUFFIX_DOCUMENT = [
- "doc",
- "docx",
- "xls",
- "xlsx",
- "ppt",
- "pptx",
- "pdf",
- "txt",
- "md",
- "csv",
- "json",
- "xml",
- "yaml",
- "yml",
- "toml",
- "ini",
- "conf",
- "cfg",
- "config",
- "log",
- "yml",
- "toml",
- "ini",
- "conf",
- "cfg",
- "config",
- "log",
- "lock",
- "sock"
-];
-// 代码类型的
-export const FILE_SUFFIX_CODE = [
- "js",
- "ts",
- "py",
- "java",
- "c",
- "cpp",
- "h",
- "hpp",
- "css",
- "html",
- "php",
- "ruby",
- "go",
- "rust",
- "swift",
- "kotlin",
- "dart",
- "scala",
- "haskell",
- "erlang",
- "elixir",
- "ocaml",
- "erlang",
- "elixir",
- "ocaml",
- "erlang",
- "elixir",
- "ocaml",
- "erlang",
- "elixir",
- "ocaml"
-];
-// 安装包类型的
-export const FILE_SUFFIX_INSTALL = ["deb", "rpm", "msi", "exe", "app", "dmg", "pkg", "deb", "rpm", "msi", "exe", "app"];
-// 数据库类型
-export const FILE_SUFFIX_DATABASE = [
- "mysql",
- "oracle",
- "postgresql",
- "sqlserver",
- "mongodb",
- "redis",
- "memcached",
- "sqlite",
- "mariadb"
-];
diff --git a/ui/lion/utils/lunaBus.ts b/ui/lion/utils/lunaBus.ts
index d064ad777..e281ff6a9 100644
--- a/ui/lion/utils/lunaBus.ts
+++ b/ui/lion/utils/lunaBus.ts
@@ -19,8 +19,8 @@ const allEventTypes = Object.keys(LUNA_MESSAGE_TYPE) as LunaEventType[];
class LunaCommunicator {
private mitt: Emitter;
private lunaId: string = "";
- private targetOrigin: string = "*";
- private protocol: string = "";
+ private targetOrigin: string = "";
+ private messageHandler: ((event: MessageEvent) => void) | null = null;
constructor() {
this.mitt = mitt();
@@ -28,36 +28,38 @@ class LunaCommunicator {
}
private setupMessageListener() {
- window.addEventListener("message", (event: MessageEvent) => {
- const message: LunaMessage = event.data;
+ if (typeof window === "undefined") return;
+ this.messageHandler = (event: MessageEvent) => {
+ if (event.source !== window.parent) return;
+ if (!event.data || typeof event.data !== "object" || typeof event.data.name !== "string") return;
+
+ const message = event.data as LunaMessage;
switch (message.name) {
case LUNA_MESSAGE_TYPE.PING:
+ if (typeof message.id !== "string") return;
this.lunaId = message.id;
- this.targetOrigin = event.origin;
- this.protocol = message.protocol;
+ this.targetOrigin = event.origin === "null" ? "*" : event.origin;
this.sendLuna(LUNA_MESSAGE_TYPE.PONG, "");
- console.log(
- `LunaCommunicator initialized with ID: ${this.lunaId}, Origin: ${this.targetOrigin}, Protocol: ${this.protocol}`
- );
break;
default:
+ if (!this.lunaId || (this.targetOrigin !== "*" && event.origin !== this.targetOrigin)) return;
// 处理其他类型的消息
if (allEventTypes.includes(message.name as LunaEventType)) {
const eventType = message.name as keyof T;
const data = message as T[keyof T];
this.mitt.emit(eventType, data);
} else {
- console.warn(`Unhandled message type: ${message.name}`, message);
+ console.warn(`Unhandled Luna message type: ${message.name}`);
}
}
- });
+ };
+ window.addEventListener("message", this.messageHandler);
}
// 发送消息到目标窗口
public sendLuna(name: K, data: T[K]) {
- if (!this.lunaId || !this.targetOrigin) {
- console.warn("Target window not set");
- }
+ if (typeof window === "undefined") return;
+ if (!this.lunaId || !this.targetOrigin || window.parent === window) return;
window.parent.postMessage({ name, id: this.lunaId, data }, this.targetOrigin);
}
@@ -84,6 +86,10 @@ class LunaCommunicator {
// 销毁实例
public destroy() {
this.mitt.all.clear();
+ if (this.messageHandler && typeof window !== "undefined") {
+ window.removeEventListener("message", this.messageHandler);
+ this.messageHandler = null;
+ }
}
// 获取所有事件类型
diff --git a/ui/lion/views/ConnectView.vue b/ui/lion/views/ConnectView.vue
index 098e9c786..921451243 100644
--- a/ui/lion/views/ConnectView.vue
+++ b/ui/lion/views/ConnectView.vue
@@ -13,9 +13,10 @@ import Osk from "@/lion/components/Osk.vue";
import OtherOption from "@/lion/components/OtherOption.vue";
import SessionShare from "@/lion/components/SessionShare/index.vue";
import { useGuacamoleClient } from "@/lion/hooks/useGuacamoleClient";
+import { createLionConnectTicket } from "@/lion/hooks/useLionConnectTicket";
+import { useLionEndpoint } from "@/lion/hooks/useLionEndpoint";
import { LUNA_MESSAGE_TYPE } from "@/lion/types/postmessage.type";
import { withLionWsUrl } from "@/lion/utils/base";
-import { readClipboardText } from "@/lion/utils/clipboard";
import { getCurrentConnectParams } from "@/lion/utils/common";
import { lunaCommunicator } from "@/lion/utils/lunaBus";
import { ErrorStatusCodes } from "@/lion/utils/status";
@@ -24,12 +25,18 @@ const toast = useToast();
const { addErrorToast } = useErrorToast();
const { t } = useI18n();
const containerRef = ref(null);
+const displayRef = ref(null);
const sessionContext = inject(connectorSessionKey, ref(null));
-const endpointUrl = computed(() => unref(sessionContext)?.endpointUrl || window.location.origin);
+const endpointUrl = useLionEndpoint(() => unref(sessionContext)?.endpointUrl);
+const activeToken = ref("");
+const activeTicket = ref("");
+let ticketCreatedAt = 0;
+let ticketRefreshPromise: Promise | null = null;
const {
guaDisplay,
connectToGuacamole,
+ connectStatus,
onlineUsersMap,
disconnectGuaclient,
sendTextToRemote,
@@ -47,16 +54,17 @@ const {
currentFolder,
currentFolderFiles,
hasClipboardPermission,
+ debouncedSendClipboardToRemote,
fileFsLoading,
currentGuacFsObject,
enableShare,
action_permission,
remoteClipboardText,
+ clipboardPasteTextLimit,
sendInputActive
-} = useGuacamoleClient(t, endpointUrl);
+} = useGuacamoleClient(t, endpointUrl, () => ({ ticket: activeTicket.value, token: activeToken.value }));
const drawShow = ref(false);
-const connectStatus = ref("Connecting");
const autoFit = ref(true);
const resolveContainerSize = () => {
@@ -94,8 +102,15 @@ const uploadingFiles = ref>([]);
const isUploading = ref(false);
const displayUploadingFiles = ref>([]);
const showOsk = ref(false);
+let uploadSequence = 0;
+
+const createUploadId = () => {
+ uploadSequence += 1;
+ return globalThis.crypto?.randomUUID?.() || `lion-drop-${Date.now()}-${uploadSequence}`;
+};
function getKeyboardLayout() {
+ if (!import.meta.client) return "en-us-qwerty";
const lunaSetting = localStorage.getItem("LunaSetting");
if (lunaSetting) {
const setting = JSON.parse(lunaSetting);
@@ -110,7 +125,37 @@ const keyboardLayout = ref(getKeyboardLayout());
const currentTab = ref("general");
const shouldEnableScroll = ref(false);
-const handleUploadFile = (options: LionUploadCustomRequestOptions, folder: any) => {
+const refreshConnectTicket = async () => {
+ if (!activeToken.value) return activeTicket.value;
+ if (activeTicket.value && Date.now() - ticketCreatedAt < 25 * 60 * 1000) return activeTicket.value;
+ const previousTicket = activeTicket.value;
+ const previousTicketAge = Date.now() - ticketCreatedAt;
+ if (!ticketRefreshPromise) {
+ ticketRefreshPromise = createLionConnectTicket(endpointUrl.value, activeToken.value).finally(() => {
+ ticketRefreshPromise = null;
+ });
+ }
+ try {
+ activeTicket.value = await ticketRefreshPromise;
+ ticketCreatedAt = Date.now();
+ return activeTicket.value;
+ } catch (error) {
+ if (previousTicket && previousTicketAge < 30 * 60 * 1000) return previousTicket;
+ throw error;
+ }
+};
+
+const handleUploadFile = async (options: LionUploadCustomRequestOptions, folder: any) => {
+ if (action_permission.value && !action_permission.value.enable_upload) {
+ toast.add({ title: `${t("UploadFile")} ${t("NoPermission")}`, color: "warning" });
+ return;
+ }
+ try {
+ await refreshConnectTicket();
+ } catch (error) {
+ addErrorToast({ title: error instanceof Error ? error.message : String(error) });
+ return;
+ }
const item = { uploadOptions: options, folder: folder || currentFolder.value };
displayUploadingFiles.value.push(options.file);
uploadingFiles.value.push(item);
@@ -128,7 +173,10 @@ const handleRemoveFile = (file: LionUploadFileInfo) => {
toast.add({ title: t("FileUploadingWarning"), color: "warning" });
return;
}
- displayUploadingFiles.value = displayUploadingFiles.value.filter((f) => f.name !== file.name);
+ if (file.status === "pending") {
+ uploadingFiles.value = uploadingFiles.value.filter((item) => item.uploadOptions.file.id !== file.id);
+ }
+ displayUploadingFiles.value = displayUploadingFiles.value.filter((item) => item.id !== file.id);
};
async function processUploadQueue() {
@@ -138,6 +186,7 @@ async function processUploadQueue() {
const { uploadOptions, folder } = uploadItem;
try {
+ await refreshConnectTicket();
uploadOptions.file.status = "uploading";
await uploadFile(uploadOptions, folder);
uploadOptions.file.status = "finished";
@@ -150,8 +199,6 @@ async function processUploadQueue() {
msg = `${t("FileUploadError")}: ${uploadOptions.file.name}`;
}
addErrorToast({ title: msg });
- } finally {
- setTimeout(handleRemoveFile, 5000, uploadOptions.file);
}
}
isUploading.value = false;
@@ -164,12 +211,13 @@ const fileDrop = (event: DragEvent) => {
if (!files?.length) return;
Array.from(files).forEach((fileObj) => {
+ const id = createUploadId();
handleUploadFile(
{
file: {
- id: `batch-id-${fileObj.name}`,
+ id,
name: fileObj.name,
- batchId: `batch-id-${fileObj.name}`,
+ batchId: id,
percentage: 0,
type: fileObj.type,
status: "pending",
@@ -181,60 +229,97 @@ const fileDrop = (event: DragEvent) => {
});
};
-const debouncedSendClipboardToRemote = useDebounceFn(async () => {
- const text = await readClipboardText();
- if (!text?.trim()) return;
- sendTextToRemote(text);
-}, 300);
+const getBrowserTimezone = () => {
+ try {
+ return Intl.DateTimeFormat().resolvedOptions().timeZone || "";
+ } catch (error) {
+ console.debug("Unable to detect browser timezone", error);
+ return "";
+ }
+};
+
+const preventDefault = (event: Event) => {
+ event.stopPropagation();
+ event.preventDefault();
+};
+
+let displayElement: HTMLElement | null = null;
+let disposed = false;
-const resolveConnectConfig = () => {
+const handleLunaOpen = () => {
+ nextTick(() => {
+ drawShow.value = !drawShow.value;
+ });
+};
+
+const handleLunaInputActive = () => {
+ nextTick(() => sendInputActive());
+};
+
+const resolveConnectConfig = async () => {
const ctx = unref(sessionContext);
if (ctx?.tokenId) {
return {
- // ponytail: lion 走 Guacamole connect 参数 TOKEN_ID,不用 koko 的 ?token= WS 查询串
+ // Lion 同时保留 TOKEN_ID 协议参数和 Koko 票据绑定所需的 token 参数。
ws: withLionWsUrl("/ws/connect/", ctx.endpointUrl),
- token: ctx.tokenId
+ token: ctx.tokenId,
+ ticket: ctx.ticket || (await createLionConnectTicket(ctx.endpointUrl, ctx.tokenId))
};
}
const params = getCurrentConnectParams();
+ const token = params.data.token || params.data.TOKEN_ID || "";
return {
- ws: params.ws || "",
- token: params.data.token || ""
+ ws: withLionWsUrl("/ws/connect/", endpointUrl.value),
+ token,
+ ticket: await createLionConnectTicket(endpointUrl.value, token)
};
};
onMounted(async () => {
loading.value = true;
await nextTick();
-
- lunaCommunicator.onLuna(LUNA_MESSAGE_TYPE.OPEN, () => {
- nextTick(() => {
- drawShow.value = !drawShow.value;
- });
- });
- lunaCommunicator.onLuna(LUNA_MESSAGE_TYPE.INPUT_ACTIVE, () => {
- nextTick(() => sendInputActive());
- });
-
- const { ws, token } = resolveConnectConfig();
+ if (disposed) return;
+
+ lunaCommunicator.onLuna(LUNA_MESSAGE_TYPE.OPEN, handleLunaOpen);
+ lunaCommunicator.onLuna(LUNA_MESSAGE_TYPE.INPUT_ACTIVE, handleLunaInputActive);
+
+ let connectConfig: Awaited>;
+ try {
+ connectConfig = await resolveConnectConfig();
+ } catch (error) {
+ if (disposed) return;
+ loading.value = false;
+ addErrorToast({ title: error instanceof Error ? error.message : String(error) });
+ return;
+ }
+ if (disposed) return;
+ const { ws, token, ticket } = connectConfig;
+ activeToken.value = token;
+ activeTicket.value = ticket;
+ ticketCreatedAt = ticket ? Date.now() : 0;
const { width, height } = resolveContainerSize();
connectToGuacamole(
ws,
{
- TOKEN_ID: encodeURIComponent(token),
- GUAC_KEYBOARD: keyboardLayout.value
+ TOKEN_ID: token,
+ token,
+ ...(ticket ? { ticket } : {}),
+ GUAC_KEYBOARD: keyboardLayout.value,
+ GUAC_TIMEZONE: getBrowserTimezone()
},
width,
height,
true
);
- const displayEl = document.getElementById("display");
+ const displayEl = displayRef.value;
if (!displayEl) {
- console.error("Display element not found");
+ loading.value = false;
+ disconnectGuaclient();
return;
}
+ displayElement = displayEl;
displayEl.appendChild(guaDisplay.value.getElement());
if (containerRef.value) {
@@ -243,33 +328,27 @@ onMounted(async () => {
debouncedResize();
}
- displayEl.addEventListener(
- "dragenter",
- (e) => {
- e.stopPropagation();
- e.preventDefault();
- },
- false
- );
- displayEl.addEventListener(
- "dragover",
- (e) => {
- e.stopPropagation();
- e.preventDefault();
- },
- false
- );
+ displayEl.addEventListener("dragenter", preventDefault, false);
+ displayEl.addEventListener("dragover", preventDefault, false);
displayEl.addEventListener("drop", fileDrop, false);
+ displayEl.addEventListener("contextmenu", preventDefault, false);
registerMouseAndKeyboardHanlder();
window.addEventListener("focus", debouncedSendClipboardToRemote);
});
onUnmounted(() => {
+ disposed = true;
resizeObserver?.disconnect();
resizeObserver = null;
+ displayElement?.removeEventListener("dragenter", preventDefault, false);
+ displayElement?.removeEventListener("dragover", preventDefault, false);
+ displayElement?.removeEventListener("drop", fileDrop, false);
+ displayElement?.removeEventListener("contextmenu", preventDefault, false);
+ displayElement = null;
disconnectGuaclient();
- lunaCommunicator.offLuna(LUNA_MESSAGE_TYPE.OPEN);
+ lunaCommunicator.offLuna(LUNA_MESSAGE_TYPE.OPEN, handleLunaOpen);
+ lunaCommunicator.offLuna(LUNA_MESSAGE_TYPE.INPUT_ACTIVE, handleLunaInputActive);
lunaCommunicator.sendLuna(LUNA_MESSAGE_TYPE.CLOSE, "");
window.removeEventListener("focus", debouncedSendClipboardToRemote);
});
@@ -279,26 +358,23 @@ const ClipBoardTextChange = (text: string) => {
sendTextToRemote(text);
};
-document.addEventListener(
- "contextmenu",
- (e: MouseEvent) => {
- e.preventDefault();
- e.stopPropagation();
- },
- false
-);
-
const handleScreenKeyboard = (name: string, keysym: any) => {
if (name === "keydown") sendKeyEvent(1, keysym);
else if (name === "keyup") sendKeyEvent(0, keysym);
};
-const handleDownloadFile = (file: GuacamoleFile) => {
+const handleDownloadFile = async (file: { name: string; streamName?: GuacamoleFile["streamName"] }) => {
if (!file?.streamName) return;
if (action_permission.value && !action_permission.value.enable_download) {
toast.add({ title: t("FileDownloadDenied"), color: "warning" });
return;
}
+ try {
+ await refreshConnectTicket();
+ } catch (error) {
+ addErrorToast({ title: error instanceof Error ? error.message : String(error) });
+ return;
+ }
currentGuacFsObject.value.requestInputStream(file.streamName, (stream: any, mimetype: any) => {
clientFileReceived(stream, mimetype, file.name);
});
@@ -354,7 +430,7 @@ const drawerTabs = computed(() => {