From 1e0ac5af756aa09043a0f83b816fe6484e5895f4 Mon Sep 17 00:00:00 2001 From: z244300 Date: Thu, 14 Aug 2025 06:43:30 +0000 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D1=82?= =?UTF-8?q?=D1=8C=20ui.js?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ui.js | 306 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 306 insertions(+) create mode 100644 ui.js diff --git a/ui.js b/ui.js new file mode 100644 index 0000000..7c365a2 --- /dev/null +++ b/ui.js @@ -0,0 +1,306 @@ +// ui_showcase.js — демонстрация UI Scriptable +// Запускается через твой лоадер (__main(ctx)) + +async function __main(ctx) { + while (true) { + const a = new Alert(); + a.title = "UI Showcase"; + a.message = "Выбери модуль для демонстрации"; + a.addAction("1) Alert (кнопки/форма)"); + a.addAction("2) UITable (список + скролл)"); + a.addAction("3) WebView (HTML-UI, кнопки, инпуты)"); + a.addAction("4) QuickLook + DrawContext"); + a.addAction("5) Виджет (ListWidget, превью)"); + a.addAction("6) Safari (встроенный)"); + a.addAction("7) Локальное уведомление"); + a.addCancelAction("Выход"); + const i = await a.presentSheet(); + if (i === -1) return; + + try { + if (i === 0) await demoAlertForm(); + else if (i === 1) await demoUITable(); + else if (i === 2) await demoWebView(); + else if (i === 3) await demoQuickLook(); + else if (i === 4) { const end = await demoWidget(); if (end) return; } + else if (i === 5) await demoSafari(); + else if (i === 6) await demoNotification(ctx); + } catch (e) { + await toast("Ошибка", String(e && e.message ? e.message : e)); + } + } +} + +/* 1) ALERT: кнопки + текстовые поля */ +async function demoAlertForm() { + const a = new Alert(); + a.title = "Форма"; + a.message = "Alert с полями ввода и кнопками"; + a.addTextField("Имя", ""); + a.addTextField("Email", ""); + a.addAction("OK"); + a.addDestructiveAction("Сбросить"); + a.addCancelAction("Отмена"); + const idx = await a.presentAlert(); + + const name = a.textFieldValue(0) || ""; + const email = a.textFieldValue(1) || ""; + + if (idx === 0) { + const b = new Alert(); + b.title = "Принято"; + b.message = `Имя: ${name}\nEmail: ${email}`; + b.addAction("Скопировать"); + b.addCancelAction("Закрыть"); + if (await b.presentAlert() === 0) Pasteboard.copy(`${name} <${email}>`); + } else if (idx === 1) { + await toast("Сброшено", "Поля очищены"); + } +} + +/* 2) UITABLE: список, скролл, иконки, onSelect */ +async function demoUITable() { + const table = new UITable(); + table.showSeparators = true; + + // Заголовок + { + const r = new UITableRow(); + const c = r.addText("История событий", new Date().toLocaleString()); + c.titleFont = Font.boldSystemFont(16); + c.subtitleFont = Font.systemFont(12); + table.addRow(r); + } + + // Генерируем 20 элементов + for (let i = 1; i <= 20; i++) { + const r = new UITableRow(); + r.cellSpacing = 8; + + // Иконка SF Symbol + try { + const sf = SFSymbol.named(i % 2 ? "barcode" : "qrcode"); + sf.applyFont(Font.systemFont(18)); + r.addImage(sf.image); + } catch { /* не критично */ } + + const title = `Элемент #${i}`; + const subtitle = i % 2 ? "CODE_128 · 1234567890" : "QR_CODE · https://example.com"; + r.addText(title, subtitle); + + r.onSelect = async () => { + Pasteboard.copy(title + " — " + subtitle); + await toast("Скопировано", title); + }; + table.addRow(r); + } + + await table.present(); +} + +/* 3) WEBVIEW: кастомный HTML-экран + двусторонний обмен */ +async function demoWebView() { + const wv = new WebView(); + let clicks = 0; + + wv.shouldAllowRequest = req => { + const u = req.url || ""; + if (u.startsWith("scriptable://action")) { + const q = qs(u); + const type = q.type || ""; + if (type === "inc") { clicks++; wv.evaluateJavaScript(`setCount(${clicks})`, false); } + if (type === "save") { + const val = q.value || ""; + Pasteboard.copy(val); + toast("Сохранено", "Строка скопирована в буфер"); + } + return false; // не уходим со страницы + } + return true; + }; + + await wv.loadHTML(webviewHTML()); + await wv.present(true); + + // Пример вызова JS из Scriptable -> страница + await wv.evaluateJavaScript(`setCount(${clicks})`, false); + + function webviewHTML() { + return ` + +WebView UI + +

WebView UI (HTML + JS)

+
+
+
+ +
Счётчик: 0
+
+
Клик по кнопке отправляет событие обратно в Scriptable
+
+ +
+
Введи строку, затем «Сохранить»:
+
+ + +
+
Строка уйдёт в Scriptable и скопируется в буфер
+
+ +
+
+
Скроллируемая область:
+
    +
    +
    +
    + +`; + } +} + +/* 4) QUICKLOOK + рисование через DrawContext */ +async function demoQuickLook() { + const img = renderCardImage({ + title: "Scriptable UI", + subtitle: "QuickLook + DrawContext", + badge: new Date().toLocaleTimeString() + }); + await QuickLook.present(img); + + const a = new Alert(); + a.title = "Сохранить картинку?"; + a.addAction("Сохранить в Фото"); + a.addCancelAction("Нет"); + if (await a.presentAlert() === 0) Photos.save(img); +} + +function renderCardImage({ title, subtitle, badge }) { + const W = 900, H = 500; + const ctx = new DrawContext(); + ctx.size = new Size(W, H); + ctx.opaque = true; + + // фон + const bg = new LinearGradient(); + bg.colors = [new Color("#0b1220"), new Color("#111827")]; + bg.locations = [0, 1]; + ctx.setGradient(bg); + ctx.fillRect(new Rect(0, 0, W, H)); + + // карточка + const cardRect = new Rect(40, 40, W-80, H-80); + ctx.setFillColor(new Color("#0f1b2e")); + ctx.fillRoundedRect(cardRect, 26); + + // заголовок + ctx.setTextColor(Color.white()); + ctx.setFont(Font.boldSystemFont(48)); + ctx.drawTextInRect(title, new Rect(70, 90, W-140, 60)); + + // подзаголовок + ctx.setTextColor(new Color("#c7d2fe")); + ctx.setFont(Font.systemFont(26)); + ctx.drawTextInRect(subtitle, new Rect(70, 160, W-140, 40)); + + // бейдж + const badgeRect = new Rect(W-70-200, H-70-50, 200, 50); + ctx.setFillColor(new Color("#1d4ed8")); + ctx.fillRoundedRect(badgeRect, 12); + ctx.setTextColor(Color.white()); + ctx.setFont(Font.boldSystemFont(22)); + ctx.drawTextInRect(badge, new Rect(badgeRect.x, badgeRect.y+12, badgeRect.width, 30)); + + return ctx.getImage(); +} + +/* 5) ВИДЖЕТ: создаём ListWidget и показываем превью (завершает скрипт) */ +async function demoWidget() { + const warn = new Alert(); + warn.title = "Показать превью виджета?"; + warn.message = "Это завершит выполнение скрипта.\nПерезапусти лоадер, чтобы вернуться в меню."; + warn.addAction("Показать"); + warn.addCancelAction("Отмена"); + if (await warn.presentAlert() !== 0) return false; + + const w = new ListWidget(); + const grad = new LinearGradient(); + grad.colors = [new Color("#0b1220"), new Color("#1f2937")]; + grad.locations = [0, 1]; + w.backgroundGradient = grad; + + const t1 = w.addText("UI Showcase"); + t1.font = Font.boldSystemFont(16); + t1.textColor = Color.white(); + + w.addSpacer(6); + const t2 = w.addText("Последний элемент: #7"); + t2.textColor = new Color("#93c5fd"); + t2.font = Font.systemFont(12); + + w.addSpacer(12); + const t3 = w.addText(new Date().toLocaleString()); + t3.textColor = Color.gray(); + t3.font = Font.systemFont(10); + + Script.setWidget(w); + Script.complete(); + return true; // сказали выходим +} + +/* 6) SAFARI: открываем встроенный браузер */ +async function demoSafari() { + await Safari.openInApp("https://example.com"); +} + +/* 7) NOTIFICATION: локальное уведомление */ +async function demoNotification(ctx) { + const n = new Notification(); + n.title = "Scriptable"; + n.body = "Это локальное уведомление"; + await n.schedule(); + await toast("Отправлено", "Проверь уведомления iOS"); +} + +/* Вспомогательные */ +async function toast(title, body) { + try { + const n = new Notification(); + n.title = title; n.body = body; + // уведомление без звука как «тост» + n.sound = "none"; + await n.schedule(); + } catch {} +} + +function qs(url) { + const out = {}; const i = url.indexOf("?"); + if (i < 0) return out; + for (const p of url.slice(i+1).split("&")) { + const [k, v] = p.split("="); + out[decodeURIComponent(k||"")] = decodeURIComponent(v||""); + } + return out; +}