// 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
Клик по кнопке отправляет событие обратно в 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;
}