From f04fa01fe1d81520fff17a634d1eca9ffe981000 Mon Sep 17 00:00:00 2001 From: iqubik Date: Sat, 4 Apr 2026 22:25:40 +0300 Subject: [PATCH] fix copy btn and copy page btn --- client/src/pages/SubscriptionsPage.tsx | 5 +-- client/src/utils/copyToClipboard.ts | 25 +++++++++++++++ .../client/templates/subscription.template.ts | 32 +++++++++++++------ 3 files changed, 50 insertions(+), 12 deletions(-) create mode 100644 client/src/utils/copyToClipboard.ts diff --git a/client/src/pages/SubscriptionsPage.tsx b/client/src/pages/SubscriptionsPage.tsx index 69f7f53..944a9af 100644 --- a/client/src/pages/SubscriptionsPage.tsx +++ b/client/src/pages/SubscriptionsPage.tsx @@ -13,6 +13,7 @@ import { } from '@mui/material'; import { Delete, Add, Link as LinkIcon, OpenInNew, ContentCopy, Dns, Router, Edit, MoreVert, Remove, Refresh } from '@mui/icons-material'; import api from '../api'; +import { copyToClipboard } from '../utils/copyToClipboard'; import { Logger } from '../utils/logger'; interface Subscription { @@ -345,7 +346,7 @@ export default function SubscriptionsPage() { }; const handleCopyLink = async (uuid: string, tunnelId: string | number) => { - await navigator.clipboard.writeText(getSubscriptionUrl(uuid, tunnelId)); + await copyToClipboard(getSubscriptionUrl(uuid, tunnelId)); setSnackbar({ open: true, type: 'success', message: 'Ссылка на подписку скопирована' }); }; @@ -602,7 +603,7 @@ export default function SubscriptionsPage() { /> - + diff --git a/client/src/utils/copyToClipboard.ts b/client/src/utils/copyToClipboard.ts new file mode 100644 index 0000000..0e01159 --- /dev/null +++ b/client/src/utils/copyToClipboard.ts @@ -0,0 +1,25 @@ +/** + * Универсальная функция копирования текста в буфер обмена. + * Работает и в secure (HTTPS/localhost), и в insecure (HTTP по IP) контекстах. + * navigator.clipboard недоступен при не-HTTPS/не-localhost соединениях. + */ +export async function copyToClipboard(text: string): Promise { + if (navigator.clipboard && navigator.clipboard.writeText) { + await navigator.clipboard.writeText(text); + } else { + // Fallback для HTTP (не-localhost) — использует execCommand + const textarea = document.createElement('textarea'); + textarea.value = text; + textarea.style.position = 'fixed'; + textarea.style.left = '-9999px'; + textarea.style.top = '-9999px'; + document.body.appendChild(textarea); + textarea.focus(); + textarea.select(); + try { + document.execCommand('copy'); + } finally { + document.body.removeChild(textarea); + } + } +} diff --git a/server/src/client/templates/subscription.template.ts b/server/src/client/templates/subscription.template.ts index f8fffb4..152ab42 100644 --- a/server/src/client/templates/subscription.template.ts +++ b/server/src/client/templates/subscription.template.ts @@ -259,16 +259,28 @@ export function generateSubscriptionHtmlWithQr( function copyLink() { const link = document.getElementById('link-text').innerText; - navigator.clipboard.writeText(link).then(() => { - const btn = document.querySelector('.action-btn'); - const originalText = btn.innerHTML; - btn.innerHTML = 'Скопировано!'; - btn.style.backgroundColor = 'var(--button-success)'; - setTimeout(() => { - btn.innerHTML = originalText; - btn.style.backgroundColor = 'var(--button-bg)'; - }, 2000); - }); + let copied = false; + if (navigator.clipboard && navigator.clipboard.writeText) { + navigator.clipboard.writeText(link).then(() => { copied = true; }); + } + if (!copied) { + const ta = document.createElement('textarea'); + ta.value = link; + ta.style.position = 'fixed'; + ta.style.left = '-9999px'; + document.body.appendChild(ta); + ta.select(); + document.execCommand('copy'); + document.body.removeChild(ta); + } + const btn = document.querySelector('.action-btn'); + const originalText = btn.innerHTML; + btn.innerHTML = 'Скопировано!'; + btn.style.backgroundColor = 'var(--button-success)'; + setTimeout(() => { + btn.innerHTML = originalText; + btn.style.backgroundColor = 'var(--button-bg)'; + }, 2000); }