Добавить ui.js
This commit is contained in:
parent
09997d3905
commit
1e0ac5af75
306
ui.js
Normal file
306
ui.js
Normal file
@ -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 `<!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;
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user