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);
}