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