jsios/ui.js
2025-08-14 06:43:30 +00:00

307 lines
10 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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 `<!doctype html>
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>WebView UI</title>
<style>
body{margin:0;background:#0b0b0f;color:#eaecef;font-family:-apple-system,system-ui,Segoe UI,Roboto,Helvetica,Arial}
header{position:sticky;top:0;background:#10131a;padding:12px 16px;border-bottom:1px solid #1e2230}
h1{margin:0;font-size:16px}
main{padding:16px}
.card{background:#111827;border:1px solid #1f2937;border-radius:12px;padding:14px;margin-bottom:14px}
input,button{font:inherit}
input[type=text]{width:100%;padding:10px 12px;border:1px solid #374151;border-radius:10px;background:#0f1625;color:#eaecef;outline:none}
button{padding:10px 14px;border-radius:10px;border:1px solid #374151;background:#141c2e;color:#eaecef;cursor:pointer}
.row{display:flex;gap:10px;align-items:center}
.muted{color:#94a3b8;font-size:12px}
</style>
<header><h1>WebView UI (HTML + JS)</h1></header>
<main>
<div class="card">
<div class="row">
<button onclick="window.location.href='scriptable://action?type=inc'">+1</button>
<div>Счётчик: <b id="count">0</b></div>
</div>
<div class="muted">Клик по кнопке отправляет событие обратно в Scriptable</div>
</div>
<div class="card">
<div>Введи строку, затем «Сохранить»:</div>
<div class="row" style="margin-top:8px">
<input id="txt" type="text" placeholder="Любой текст">
<button onclick="save()">Сохранить</button>
</div>
<div class="muted">Строка уйдёт в Scriptable и скопируется в буфер</div>
</div>
<div class="card">
<div style="height:300px; overflow:auto; border:1px solid #273046; border-radius:8px; padding:10px">
<div class="muted">Скроллируемая область:</div>
<ul id="list"></ul>
</div>
</div>
</main>
<script>
function setCount(n){ document.getElementById('count').textContent = String(n); }
function save(){
const v = document.getElementById('txt').value || "";
window.location.href = "scriptable://action?type=save&value=" + encodeURIComponent(v);
}
// наполним список для демонстрации прокрутки
const ul = document.getElementById('list');
for (let i=1;i<=50;i++){ const li=document.createElement('li'); li.textContent="Строка "+i; ul.appendChild(li); }
</script>`;
}
}
/* 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;
}