diff --git a/README.md b/README.md index fca2166..da255d7 100644 --- a/README.md +++ b/README.md @@ -341,7 +341,7 @@ services: ### UX (v0.1.3+) - После создания: **QR** на share-link, иконки **скачать QR (PNG)** и **Web Share** (если есть `navigator.share`) под QR; отдельные кнопки Copy для ссылки / токена / пароля; пароль не советуется слать в той же переписке. -- Create: счётчик размера `≈ used / max`; **число открытий** 1–3 (потолок в админке); опционально **«доступно с»** (дата+время); человеческие TTL (в т.ч. «до вечера»); шаблоны текста; подписи элементов пакета. +- Create: счётчик размера `≈ used / max`; **число открытий** 1–3 (потолок в админке); опционально **«доступно с»** (дата + время); человеческие TTL (в т.ч. «до вечера»). - Unwrap: Enter в поле пароля; focus+select при ошибках пароля; zip all; trust-строка; экран **ещё недоступно** до `available_from`; при N>1 ciphertext остаётся до последнего открытия. - Картинки: lightbox. Тема: `prefers-color-scheme` при первом визите; haptic на Copy. PWA manifest + кнопка **На экран Домой** (Chrome install / iOS подсказка). - Страница [`/verify`](/verify) — как проверить ZK-модель. diff --git a/app/static/apple-touch-icon.png b/app/static/apple-touch-icon.png index b4a1fa6..4a030b0 100644 Binary files a/app/static/apple-touch-icon.png and b/app/static/apple-touch-icon.png differ diff --git a/app/static/css/app.css b/app/static/css/app.css index 2dfb554..bc6d418 100644 --- a/app/static/css/app.css +++ b/app/static/css/app.css @@ -178,7 +178,8 @@ html[data-theme="light"] body { 50% { filter: saturate(1.2); transform: scale(1.04); } } -.top-actions { display: flex; gap: 0.5rem; } +.top-actions { display: flex; gap: 0.5rem; overflow: visible; position: relative; z-index: 5; } +.topbar { overflow: visible; } .icon-btn { min-width: 42px; @@ -271,15 +272,22 @@ html[data-theme="dark"] .icon-btn:hover { overflow: visible; } #install-app .ui-tooltip { + top: calc(100% + 0.5rem); + bottom: auto; left: 50%; right: auto; - transform: translateX(-50%) translateY(4px); + transform: translateX(-50%) translateY(-4px); white-space: nowrap; } #install-app .ui-tooltip::after { + top: auto; + bottom: 100%; left: 50%; right: auto; transform: translateX(-50%); + border: 5px solid transparent; + border-bottom-color: var(--line); + border-top-color: transparent; } #install-app:hover .ui-tooltip, #install-app:focus-visible .ui-tooltip { @@ -287,6 +295,25 @@ html[data-theme="dark"] .icon-btn:hover { visibility: visible; transform: translateX(-50%); } +.datetime-split { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 0.45rem; +} +.datetime-split input { + min-width: 0; +} +.field-label { + display: block; + margin-bottom: 0.35rem; + font-weight: 600; + font-size: 0.88rem; +} +@media (max-width: 720px) { + .datetime-split { + grid-template-columns: 1fr; + } +} .composer, .success-panel, .result-panel { display: grid; @@ -834,51 +861,22 @@ body.busy-open { overflow: hidden; } } .file-list { list-style: none; padding: 0; margin: 0; display: grid; gap: 0.4rem; } -.file-list li, -.file-list-item { - display: grid; - grid-template-columns: minmax(0, 1fr) minmax(5.5rem, 9rem) auto; - gap: 0.45rem; +.file-list li { + display: flex; + justify-content: space-between; + gap: 0.75rem; align-items: center; padding: 0.55rem 0.75rem; border: 1px solid var(--line); border-radius: 10px; font-size: 0.9rem; } -.file-label-input { - width: 100%; - min-width: 0; - padding: 0.35rem 0.5rem; - font-size: 0.82rem; -} .file-list button { border: 0; background: transparent; color: var(--danger); cursor: pointer; } -.template-chips { - display: flex; - flex-wrap: wrap; - gap: 0.4rem; - margin: 0 0 0.35rem; -} -.template-chip { - border: 1px solid var(--line); - background: transparent; - color: var(--muted); - border-radius: 999px; - padding: 0.3rem 0.7rem; - font-size: 0.78rem; - font-weight: 600; - cursor: pointer; -} -.template-chip:hover, -.template-chip:focus-visible { - color: var(--accent-2); - border-color: rgba(75, 134, 240, 0.4); - outline: none; -} .success-meta-badges { display: flex; flex-wrap: wrap; @@ -1519,7 +1517,7 @@ html[data-theme="light"] .admin-nav { box-shadow: 0 8px 24px rgba(20, 40, 70, 0.08); } -@media (max-width: 860px) { +@media (max-width: 1015px) { .admin-top { display: grid; grid-template-columns: minmax(0, 1fr) auto; @@ -1617,7 +1615,7 @@ html[data-theme="light"] .admin-nav { } } -@media (min-width: 861px) { +@media (min-width: 1016px) { .admin-top > .brand { flex: 0 0 auto; } diff --git a/app/static/icon-192.png b/app/static/icon-192.png new file mode 100644 index 0000000..4dad87b Binary files /dev/null and b/app/static/icon-192.png differ diff --git a/app/static/icon-512.png b/app/static/icon-512.png new file mode 100644 index 0000000..12df585 Binary files /dev/null and b/app/static/icon-512.png differ diff --git a/app/static/js/admin-nav.js b/app/static/js/admin-nav.js index be39e29..1343562 100644 --- a/app/static/js/admin-nav.js +++ b/app/static/js/admin-nav.js @@ -4,7 +4,7 @@ const nav = document.getElementById("admin-nav"); if (!top || !toggle || !nav) return; - const mq = window.matchMedia("(max-width: 860px)"); + const mq = window.matchMedia("(max-width: 1015px)"); const syncLabel = () => { const open = top.classList.contains("is-nav-open"); diff --git a/app/static/js/create.js b/app/static/js/create.js index ff6d3b9..6cfa0de 100644 --- a/app/static/js/create.js +++ b/app/static/js/create.js @@ -1,14 +1,12 @@ (() => { const t = (k, vars) => window.WrappedI18n.t(k, vars); - /** @type {{ file: File, label: string }[]} */ + /** @type {File[]} */ const files = []; let settings = null; let captchaWidgetId = null; const els = { text: document.getElementById("payload-text"), - textLabel: document.getElementById("text-label"), - templateChips: document.getElementById("template-chips"), lang: document.getElementById("text-language"), langChip: document.getElementById("lang-chip"), langChipText: document.getElementById("lang-chip-text"), @@ -18,7 +16,8 @@ editor: document.getElementById("code-editor"), ttl: document.getElementById("ttl-seconds"), maxOpens: document.getElementById("max-opens"), - availableFrom: document.getElementById("available-from"), + availableFromDate: document.getElementById("available-from-date"), + availableFromTime: document.getElementById("available-from-time"), password: document.getElementById("password"), generatePassword: document.getElementById("generate-password"), dropzone: document.getElementById("dropzone"), @@ -114,7 +113,7 @@ function estimatePayloadBytes() { const textBytes = new TextEncoder().encode(els.text?.value || "").length; - const fileBytes = files.reduce((sum, entry) => sum + (Number(entry.file.size) || 0), 0); + const fileBytes = files.reduce((sum, f) => sum + (Number(f.size) || 0), 0); return textBytes + fileBytes; } @@ -142,21 +141,9 @@ function renderFiles() { els.fileList.innerHTML = ""; - files.forEach((entry, idx) => { - const f = entry.file; + files.forEach((f, idx) => { const li = document.createElement("li"); - li.className = "file-list-item"; - const meta = document.createElement("span"); - meta.innerHTML = `${escapeHtml(f.name)} (${escapeHtml(f.type || "file")} · ${window.WrappedUI.formatBytes(f.size)})`; - const labelInput = document.createElement("input"); - labelInput.type = "text"; - labelInput.className = "file-label-input"; - labelInput.maxLength = 120; - labelInput.placeholder = t("create.itemLabelPlaceholder"); - labelInput.value = entry.label || ""; - labelInput.addEventListener("input", () => { - entry.label = labelInput.value; - }); + li.innerHTML = `${escapeHtml(f.name)} (${escapeHtml(f.type || "file")} · ${window.WrappedUI.formatBytes(f.size)})`; const btn = document.createElement("button"); btn.type = "button"; btn.textContent = "×"; @@ -164,8 +151,6 @@ files.splice(idx, 1); renderFiles(); }); - li.appendChild(meta); - li.appendChild(labelInput); li.appendChild(btn); els.fileList.appendChild(li); }); @@ -186,7 +171,7 @@ showError(t("create.mimeDenied") + `: ${f.type || f.name}`); continue; } - files.push({ file: f, label: "" }); + files.push(f); } renderFiles(); } @@ -245,31 +230,11 @@ .join(""); } - function renderTemplates() { - if (!els.templateChips) return; - els.templateChips.innerHTML = ""; - const keys = [ - { id: "access", key: "create.tpl.access" }, - { id: "code", key: "create.tpl.code" }, - { id: "fileNote", key: "create.tpl.fileNote" }, - ]; - for (const item of keys) { - const btn = document.createElement("button"); - btn.type = "button"; - btn.className = "template-chip"; - btn.textContent = t(`create.tpl.${item.id}.name`); - btn.addEventListener("click", () => { - els.text.value = t(`create.tpl.${item.id}.body`); - refreshHighlight(); - refreshSizeMeter(); - }); - els.templateChips.appendChild(btn); - } - } - - function datetimeLocalToIso(value) { - if (!value) return null; - const d = new Date(value); + function availableFromToIso() { + const date = (els.availableFromDate?.value || "").trim(); + if (!date) return null; + const time = (els.availableFromTime?.value || "").trim() || "00:00"; + const d = new Date(`${date}T${time}`); if (Number.isNaN(d.getTime())) return null; return d.toISOString(); } @@ -351,7 +316,6 @@ settings = await resp.json(); fillTtl(); fillMaxOpens(); - renderTemplates(); loadCaptcha(); setLanguage("plaintext", { silent: true }); refreshHighlight(); @@ -525,18 +489,15 @@ const items = []; const contentTypes = []; if (text.trim()) { - const textItem = { + items.push({ type: "text", language: els.lang.value, content: text, - }; - const label = (els.textLabel?.value || "").trim(); - if (label) textItem.label = label.slice(0, 120); - items.push(textItem); + }); contentTypes.push("text/plain"); } - for (const entry of files) { - const item = await window.WrappedCrypto.fileToItem(entry.file, entry.label); + for (const f of files) { + const item = await window.WrappedCrypto.fileToItem(f); items.push(item); contentTypes.push(item.mime); } @@ -550,7 +511,7 @@ const password = els.password.value || ""; const maxOpens = Number(els.maxOpens?.value || 1); - const availableFromIso = datetimeLocalToIso(els.availableFrom?.value || ""); + const availableFromIso = availableFromToIso(); els.wrapBtn.disabled = true; window.WrappedUI.showBusy(t("create.working")); try { @@ -638,7 +599,6 @@ window.WrappedI18n.onChange(() => { fillTtl(); fillMaxOpens(); - renderTemplates(); refreshExpiresMeta(); refreshSuccessBadges(); refreshSizeMeter(); diff --git a/app/static/manifest.webmanifest b/app/static/manifest.webmanifest index 081ab98..4297c7b 100644 --- a/app/static/manifest.webmanifest +++ b/app/static/manifest.webmanifest @@ -8,10 +8,28 @@ "theme_color": "#0b1020", "icons": [ { - "src": "/static/favicon.svg", - "sizes": "any", - "type": "image/svg+xml", + "src": "/static/icon-192.png", + "sizes": "192x192", + "type": "image/png", "purpose": "any" + }, + { + "src": "/static/icon-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "any" + }, + { + "src": "/static/icon-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "/static/icon-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" } ] } diff --git a/app/templates/create.html b/app/templates/create.html index 5afa02e..9fa7480 100644 --- a/app/templates/create.html +++ b/app/templates/create.html @@ -14,11 +14,6 @@ -
-Empty = available immediately