JavaScript bridge API
Виджет главного экрана
Небольшая карточка Android на главном экране, которую ваша страница заполняет заголовком, значением и несколькими строками — AppMint.widget.
AppMint.widget помощник#
AppMint.widget.set(card) / update(partial) / clear() / placedCount() / requestPin()
Пример
Заполняет виджет главного экрана вашего приложения: нативную карточку с заголовком, одним крупным значением, подписью, до двенадцати строк (у каждой при желании свой цвет, маркер и маршрут по нажатию), вашими цветами и маршрутом, который открывается по нажатию. Когда пользователь увеличивает виджет, он показывает больше строк, а с textSize: 'auto' текст растёт, заполняя его, — пока каждая строка помещается, при этом длинная строка занимает две строки, а не обрезается. Виджет не может показывать HTML — вы отправляете ему текст.
Возвращает: каждый метод возвращает Promise. set(card) → {ok: true, placed} (placed = сколько копий на главном экране) или {ok: false, error}. clear() → undefined. placedCount() → число. requestPin() → true, если окно лаунчера «Добавить» открылось. Требуется: ничего — виджет есть в каждой сборке.
Задайте карточку — один раз при запуске и снова каждый раз, когда меняются показываемые в ней данные:
async function updateWidget(events) {
if (!(window.AppMint && AppMint.widget)) return; // in a browser
var r = await AppMint.widget.set({
title: 'Today',
value: events.length + ' events',
lines: events.slice(0, 6).map(function (ev) { return ev.time + ' ' + ev.name; }),
route: '/calendar', // a tap opens the app at this route
bg: '#0E7C86', // background
ink: '#FFFFFF', // title and lines
accent: '#FFE08A' // the big value
});
if (!r.ok) console.log('Widget not updated: ' + r.error);
}Повестка, заполняющая виджет — прозрачный фон, текст, растущий при изменении размера виджета, цветные строки с маркером, просроченная строка красным и нажатие на строку, открывающее этот день:
AppMint.widget.set({
title: 'Agenda',
value: 'Sun 18 Oct',
caption: '5 events, 1 overdue', // small text under the value
bg: 'rgba(20, 38, 74, 0.6)', // 60 % opaque — the wallpaper shows through
ink: '#FFFFFF', accent: '#FFE08A',
textSize: 'auto', // 'auto' | 'small' | 'normal' | 'large' | 'xl' | a number 0.6–3
align: 'start', // 'start' (left) | 'center' | 'end'
lines: [
{ text: '09:00 Stand-up', marker: '#29B6F6', route: '/#/day/2026-10-18' },
{ text: '10:30 Dentist, Dr Rao', marker: '#43A047', route: '/#/day/2026-10-18' },
{ text: 'Overdue: renew the car insurance', color: '#FF5252', marker: '#FF5252', bold: true, route: '/#/tasks/overdue' },
{ text: 'Tue 20: Team lunch', route: '/#/day/2026-10-20' },
'Wed 21: Gym' // a plain string is still a line
],
route: '/#/today' // a tap anywhere else on the card
});Строка — это текст или { text, color?, marker?, bold?, wrap?, route? }; marker ставит перед текстом цветной ●, wrap: true в одной строке (или на карточке) позволяет длинной строке занимать две строки при любом размере текста; при textSize: 'auto' строки уже так делают, а wrap: false удерживает строку в одном ряду. До шести строк показываются всегда; строки с седьмой по двенадцатую показываются, когда виджет достаточно высокий.
Более насыщенные виджеты — список, крупное число с индикатором выполнения, фоновая картинка и кнопки. Карточка рисуется так только при наличии одного из этих ключей; карточка без них выглядит как раньше:
AppMint.widget.set({
layout: 'list', // 'list' | 'progress' | 'card'
title: 'Today', subtitle: 'Tue 30 Sep',
items: [ // up to 5 rows; a row tap opens its own route
{ text: '09:00 Dentist', secondary: 'Dr Rao, 2nd floor', image: 'img/tooth.png', route: '/event/41' },
{ text: '13:30 Lunch with Sam', image: 'https://example.com/sam.jpg', route: '/event/42' }
],
buttons: [{ label: 'Add', route: '/new' }, { label: 'Week', route: '/week' }], // up to 2
bgImage: 'img/widget-bg.jpg', bg: '#101820', ink: '#FFFFFF', accent: '#FFB000'
});
AppMint.widget.set({
layout: 'progress', title: 'Steps', value: '8 214',
progress: { value: 8214, max: 10000, label: '82% of today's goal' }
});
AppMint.widget.update({ value: '8 950', progress: { value: 8950, max: 10000 } }); // change only these keysСобственный шрифт — положите файл шрифта в ваше приложение (.ttf или .otf, например из «Download family» Google Fonts) и укажите путь к нему, как для <img>; весь текст на карточке будет нарисован этим шрифтом:
AppMint.widget.set({
title: 'Score', value: '1 250',
font: 'fonts/RocknRollOne-Regular.ttf', // a file in your app, an https address, or a data: URL
lines: ['Level 7', { text: 'New record!', bold: true }]
}).then(function (r) { if (!r.ok) console.log(r.error); }); // e.g. "font file not found in the app: …"Имя шрифта, например 'RocknRoll One', работать не может: виджеты рисует главный экран, а у него есть только собственные шрифты телефона — set() отвечает {ok: false, error} с таким пояснением. Этим встроенным семействам файл не нужен: 'serif', 'monospace', 'sans-serif-condensed', 'sans-serif-light', 'sans-serif-medium', 'sans-serif-black', 'casual', 'cursive', 'sans-serif-smallcaps'. Файлы WOFF / WOFF2 отклоняются (Android не умеет их читать) — используйте .ttf. С файлом шрифта карточка рисуется как картинка размером с виджет и перерисовывается при изменении его размера; нажатия на строки, ряды и кнопки и программы чтения с экрана работают как на любой карточке.
Обновление при закрытом приложении — укажите адрес https, возвращающий тот же JSON; Android запрашивает его каждые refreshMinutes (30 или больше, по умолчанию 60), пока виджет находится на главном экране, даже после перезапуска:
AppMint.widget.set({
title: 'Orders', value: '…',
refreshUrl: 'https://api.example.com/widget.json?user=42',
refreshMinutes: 30
}).then(function (r) { console.log('refreshing every ' + r.refreshMinutes + ' min'); });
// widget.json answers e.g. {"title":"Orders","value":"12 new","layout":"list","items":[{"text":"#1042 paid"}]}Изображения: путь внутри вашего приложения (разрешается так же, как <img src> на странице), адрес https или URL data:image/…; они уменьшаются и хранятся на телефоне. В JSON обновления относительный путь означает файл приложения. refreshUrl: '' останавливает обновление; clear() тоже его останавливает. Момент каждого обновления определяет Android (он объединяет фоновую работу, чтобы экономить заряд).
Кнопка «Добавить на главный экран»:
async function addWidget() {
if (!(window.AppMint && AppMint.widget)) return;
if ((await AppMint.widget.placedCount()) > 0) { alert('The widget is already on your home screen'); return; }
var opened = await AppMint.widget.requestPin();
if (!opened) alert('Touch and hold your home screen, tap Widgets, and pick this app.');
}Очистите его (например, после выхода пользователя из аккаунта):
if (window.AppMint && AppMint.widget) AppMint.widget.clear(); // shows "Nothing to show yet"Нажатие на виджет открывает приложение со ссылкой запуска appmint-shortcut://<package><route> — читайте её через WebToApk.getLaunchUrl() при запуске и событие appmint:deep-link во время работы:
window.addEventListener('appmint:deep-link', function (e) {
var m = /^appmint-shortcut:\/\/[^\/?#]*([^#]*)/i.exec(e.detail.url);
if (m) goTo(m[1] || '/');
});Цвета: #RRGGBB, #RGB, rgb(20, 38, 74), rgba(20, 38, 74, 0.6) (также rgb(20 38 74 / 60%)), transparent и простые названия, например red. Восьмизначный hex задаётся в порядке Android, альфа-канал ПЕРВЫМ: #AARRGGBB. #9914264A — это 60 % непрозрачности; CSS-запись #14264A99 здесь читается как альфа 14 (8 %), почти невидимая карточка. Для прозрачных фонов rgba() избавляет от путаницы. Цвета линий (color, marker) принимают те же формы; неверный цвет линии отклоняется с {ok: false, error: 'lines[2].color is not a colour'}, а неверный цвет карточки заменяется цветом по умолчанию.
Примечания: Все поля карточки необязательны: пустой title показывает название приложения; пустые value, caption или строки скрываются; строки после двенадцатой отбрасываются. textSize и align применяются ко всему тексту на карточке; неизвестное значение отклоняется с {ok: false, error}. Карточка без новых ключей (caption, textSize, align, wrap, оформленные строки) выглядит как раньше. Виджет хранит последнюю карточку, даже после перезапуска, и меняется только тогда, когда ваша страница снова вызывает set(). requestPin() требует Android 8+ и лаунчер с поддержкой этой функции. Приложения, созданные с помощью ИИ, могут использовать widget из @/lib/appmintNative. Оформленные строки, caption, textSize, align, wrap и rgba() требуют приложения, собранного в AppMint 1.11.38 или новее — пересоберите приложение, прежде чем отправлять объекты строк, иначе старая сборка покажет их как сырой текст. font требует приложения, собранного в AppMint 1.11.41 или новее (старая сборка его игнорирует). Приложения в режиме сайта: нажатие открывает маршрут на вашем сайте — /#/today загружает https://<your site>/#/today — а getLaunchUrl() по-прежнему возвращает ссылку appmint-shortcut://.
widgetClear мост#
window.WebToApk.widgetClear()
Удаляет карточку; размещённые виджеты возвращаются в пустое состояние.
Пример
Внутренний механизм для AppMint.widget.clear() — удаляет карточку виджета; все размещённые виджеты возвращаются к состоянию «Пока нечего показывать».
Возвращает: ничего. Требуется: ничего.
Сначала публичный API:
async function onSignOut() {
if (window.AppMint && AppMint.widget) await AppMint.widget.clear();
}Прямой вызов:
if (window.WebToApk && typeof window.WebToApk.widgetClear === 'function') {
window.WebToApk.widgetClear();
}Свяжите виджет с переключателем «Показывать на главном экране» в настройках — выключение очищает его, включение заполняет снова:
document.getElementById('widget-toggle').onchange = async function (e) {
if (!(window.AppMint && AppMint.widget)) return;
if (e.target.checked) {
await AppMint.widget.set(buildWidgetCard()); // your own card builder
} else {
await AppMint.widget.clear();
}
};После удаления аккаунта очистите виджет и сообщите пользователю, если копия всё ещё на его главном экране:
function afterAccountDeleted() {
var W = window.WebToApk;
if (!W || typeof W.widgetClear !== 'function') return;
W.widgetClear();
if (W.widgetPlacedCount() > 0) {
showMessage('Your widget is now empty. You can remove it from your home screen.');
}
}Примечания: Виджет остаётся на главном экране; удаляется только его содержимое. Вызовите AppMint.widget.set(), чтобы заполнить его снова.
widgetPlacedCount мост#
window.WebToApk.widgetPlacedCount(): Int
Сколько копий виджета пользователь разместил на главном экране.
Пример
Внутренний механизм для AppMint.widget.placedCount() — сколько копий вашего виджета пользователь разместил на главном экране.
Возвращает: число, синхронно (0, если виджетов нет). Требуется: ничего.
Сначала публичный API — показывайте кнопку «Добавить виджет» только тогда, когда виджет ещё не размещён:
async function showAddWidgetButton() {
var btn = document.getElementById('add-widget');
if (!(window.AppMint && AppMint.widget)) { btn.hidden = true; return; }
btn.hidden = (await AppMint.widget.placedCount()) > 0;
}Прямой вызов:
if (window.WebToApk && typeof window.WebToApk.widgetPlacedCount === 'function') {
var n = window.WebToApk.widgetPlacedCount();
console.log('Widgets on the home screen: ' + n);
}Проверяйте заново каждый раз, когда пользователь возвращается из лаунчера, и поблагодарите его, как только виджет появится:
document.addEventListener('visibilitychange', async function () {
if (document.visibilityState !== 'visible') return;
if (!(window.AppMint && AppMint.widget)) return;
var n = await AppMint.widget.placedCount();
document.getElementById('widget-promo').hidden = n > 0;
if (n > 0) document.getElementById('widget-thanks').hidden = false;
});Покажите состояние виджета на странице настроек синхронным низкоуровневым вызовом:
var status = document.getElementById('widget-status');
if (window.WebToApk && typeof window.WebToApk.widgetPlacedCount === 'function') {
var n = window.WebToApk.widgetPlacedCount();
status.textContent = n === 0 ? 'Not on your home screen'
: n === 1 ? 'On your home screen' : 'On your home screen ' + n + ' times';
} else {
status.textContent = 'Available in the installed app';
}Примечания: Проверяйте ещё раз, когда приложение возвращается на передний план (visibilitychange), поскольку пользователь добавляет виджеты из лаунчера, вне вашего приложения.
widgetRequestPin мост#
window.WebToApk.widgetRequestPin(): Boolean
Открывает окно лаунчера «Добавить на главный экран» для виджета (Android 8+). Возвращает false там, где лаунчер этого не умеет.
Пример
Внутренний механизм для AppMint.widget.requestPin() — открывает окно лаунчера «Добавить на главный экран» для вашего виджета.
Возвращает: true, если окно открылось, false, если лаунчер этого не умеет (синхронно). Требуется: Android 8+ и лаунчер с поддержкой закрепления виджетов.
Сначала публичный API, по нажатию кнопки:
document.getElementById('add-widget').onclick = async function () {
if (!(window.AppMint && AppMint.widget)) return;
var opened = await AppMint.widget.requestPin();
if (!opened) {
alert('Touch and hold your home screen, tap Widgets, then pick this app.');
}
};Прямой вызов:
if (window.WebToApk && typeof window.WebToApk.widgetRequestPin === 'function') {
if (!window.WebToApk.widgetRequestPin()) showManualSteps();
}Сначала заполните карточку, затем откройте окно, чтобы виджет показывал настоящее содержимое в тот момент, когда появится:
document.getElementById('add-widget').onclick = async function () {
if (!(window.AppMint && AppMint.widget)) return;
await AppMint.widget.set({ title: 'Streak', value: '12 days', lines: ['Best 30'], route: '/streak' });
if (!(await AppMint.widget.requestPin())) {
document.getElementById('widget-help').hidden = false; // manual steps
}
};Узнайте, согласился ли пользователь: сравните число размещённых виджетов до открытия окна и после его возвращения:
document.getElementById('add-widget').onclick = function () {
var W = window.WebToApk;
if (!W || typeof W.widgetRequestPin !== 'function') return;
var before = W.widgetPlacedCount();
if (!W.widgetRequestPin()) { showManualSteps(); return; }
document.addEventListener('visibilitychange', function check() {
if (document.visibilityState !== 'visible') return;
document.removeEventListener('visibilitychange', check);
if (W.widgetPlacedCount() > before) showToast('Widget added');
});
};Примечания: true означает лишь, что окно открылось — пользователь всё ещё может его отменить. Позже используйте AppMint.widget.placedCount(), чтобы узнать, был ли виджет добавлен. Сначала заполните карточку через AppMint.widget.set(), чтобы новый виджет не был пустым.
widgetSet мост#
window.WebToApk.widgetSet(cardJson: String): String
Сохраняет карточку виджета [cardJson] `{title?, value?, lines?, route?, bg?, ink?, accent?}` и перерисовывает все размещённые виджеты. Возвращает `{ok, placed}`.
Пример
Внутренний механизм для AppMint.widget.set(card) — страницы используют этот помощник, чтобы заполнять виджет главного экрана.
Возвращает: JSON-строку {"ok":true,"placed":<number>} или {"ok":false,"error":"…"} (синхронно). Требуется: ничего — виджет есть в каждой сборке.
Сначала публичный API:
if (window.AppMint && AppMint.widget) {
AppMint.widget.set({ title: 'Steps', value: '8 214', lines: ['Goal 10 000'], route: '/' })
.then(function (r) { if (!r.ok) console.log(r.error); });
}Низкоуровневый вызов принимает карточку как JSON-строку и возвращает текст JSON:
if (window.WebToApk && typeof window.WebToApk.widgetSet === 'function') {
var card = { title: 'Steps', value: '8 214', lines: ['Goal 10 000'], route: '/',
bg: '#15161A', ink: '#FFFFFF', accent: '#22D3EE' };
var r = JSON.parse(window.WebToApk.widgetSet(JSON.stringify(card)));
console.log(r.ok ? 'on ' + r.placed + ' home screen(s)' : r.error);
}Держите виджет в соответствии со списком дел — число невыполненных как значение, первые шесть задач как строки, а нажатие открывает экран задач:
async function syncTaskWidget(tasks) {
if (!(window.AppMint && AppMint.widget)) return;
var open = tasks.filter(function (t) { return !t.done; });
var r = await AppMint.widget.set({
title: 'To do',
value: open.length ? open.length + ' left' : 'All done',
lines: open.slice(0, 6).map(function (t) { return '• ' + t.title; }),
route: '/tasks'
});
if (!r.ok) console.warn('Widget not updated: ' + r.error);
}Запишите последнее состояние, когда пользователь уходит из приложения; низкоуровневый вызов синхронный, поэтому он завершается до приостановки страницы:
document.addEventListener('visibilitychange', function () {
if (document.visibilityState !== 'hidden') return;
if (!window.WebToApk || typeof window.WebToApk.widgetSet !== 'function') return;
var s = getTodayStats(); // your app's own numbers
window.WebToApk.widgetSet(JSON.stringify({
title: 'Today', value: s.spent + ' spent',
lines: ['Budget ' + s.budget, 'Left ' + (s.budget - s.spent)], route: '/budget'
}));
});Примечания: Ключи карточки: title, value, caption, lines (до двенадцати; строка — это текст или {text, color, marker, bold, wrap, route}), route (если нет, добавляется /), bg, ink, accent, textSize, align, wrap, font (файл .ttf/.otf в вашем приложении или системное семейство, например serif), а также ключи расширенного макета — см. AppMint.widget. Цвета: #RRGGBB, #AARRGGBB (альфа первой), rgba(). Все размещённые виджеты перерисовываются сразу. placed равно 0, если пользователь ещё не добавил виджет — карточка всё равно сохраняется и появится, когда он это сделает.
widgetUpdate мост#
window.WebToApk.widgetUpdate(partialJson: String): String
Объединяет [partialJson] с сохранённой карточкой виджета (названные ключи заменяются, остальные остаются) и перерисовывает. Возвращает `{ok, placed}`.
Пример
Изменяет часть виджета главного экрана: переданные вами ключи заменяют сохранённые, всё остальное остаётся. Обычно страницы вызывают AppMint.widget.update(partial).
Возвращает: JSON-строку {"ok":true,"placed":<n>} или {"ok":false,"error":"…"}. Требуется: ничего — виджет есть в каждой сборке.
// Only the number and the progress bar change; title, rows and buttons stay as they were.
if (window.AppMint && AppMint.widget) {
AppMint.widget.update({ value: '7 420', progress: { value: 7420, max: 10000, label: '74% of goal' } });
}
// Raw form:
if (window.WebToApk && typeof window.WebToApk.widgetUpdate === 'function') {
var r = JSON.parse(window.WebToApk.widgetUpdate(JSON.stringify({ subtitle: 'Updated ' + new Date().toLocaleTimeString() })));
if (!r.ok) console.warn(r.error);
}Очистите карточку при выходе из аккаунта и сообщите об ошибке из update:
async function onSignOut() {
if (!(window.AppMint && AppMint.widget)) return;
await AppMint.widget.clear();
}
async function bumpCount(n) {
if (!(window.AppMint && AppMint.widget)) return;
const r = await AppMint.widget.update({ value: String(n) });
if (!r.ok) console.warn('Widget not updated: ' + r.error);
}Совместите это с проверкой размещения: обновляйте карточку только пока копия находится на главном экране.
async function refreshWidget(stats) {
if (!(window.AppMint && AppMint.widget)) return;
if (await AppMint.widget.placedCount() === 0) return;
AppMint.widget.update({ value: stats.total + ' today', subtitle: 'Updated ' + new Date().toLocaleTimeString() });
}Примечания: Если сохранённой карточки нет, update() ведёт себя как set(). Все ключи карточки см. в AppMint.widget.