Создать APK бесплатно

JavaScript bridge API

Поделиться, буфер обмена и печать

Системное окно «Поделиться» Android, системный буфер обмена, текст, которым другое приложение поделилось с вашим, и диалог печати.

AppMint.pdf помощник#

AppMint.pdf.fromPage(options) / fromHtml(html, options)

Пример

Создаёт настоящий PDF (выделяемый текст, векторная графика) без окна печати: fromPage сохраняет страницу такой, как она на экране, с версткой по вашему CSS @media print; fromHtml отображает переданный вами HTML на невидимой странице по собственному адресу приложения — поэтому относительные изображения, таблицы стилей и шрифты загружаются так же, как на вашей странице, — дожидается шрифтов и всех изображений, затем сохраняет. window.print() по-прежнему открывает обычное окно печати.

Возвращает: Promise с {ok: true, fileName, pages, bytes, saved, uri?, base64?} или {ok: false, code, message} — code это busy (одновременно только один PDF), bad-options, failed, too-large (base64 больше 25 МБ) или permission-denied (хранилище отказало на Android 7–9). Требуется: ничего — есть в каждой сборке. Сохранение в «Загрузки» не требует разрешения на Android 10+.

Параметры: fileName (".pdf" добавляется; по умолчанию — заголовок страницы), pageSize 'A4' (по умолчанию) | 'Letter' | 'Legal' | 'A3' | 'A5', landscape (true/false), margins в миллиметрах (одно число или {top, right, bottom, left}; если не указано — ваше собственное правило @page), save 'downloads' (по умолчанию) | 'share' | 'base64'.

Сохранить текущую страницу в «Загрузки»:

async function saveInvoice() {
  if (!(window.AppMint && AppMint.pdf)) { window.print(); return; }   // in a browser
  var r = await AppMint.pdf.fromPage({ fileName: 'invoice-1042', pageSize: 'A4', margins: 12 });
  if (r.ok) console.log('Saved ' + r.fileName + ' (' + r.pages + ' pages)');
  else alert('Could not make the PDF: ' + r.message);
}

Чек, собранный из HTML, сразу в окно «Поделиться» (изображения и шрифты загружаются до создания PDF):

async function shareReceipt(order) {
  var html = '<style>body{font-family:sans-serif} td{padding:4px 8px}</style>' +
    '<img src="img/logo.png" width="120"><h1>Receipt #' + order.id + '</h1><table>' +
    order.items.map(function (i) { return '<tr><td>' + i.name + '</td><td>' + i.price + '</td></tr>'; }).join('') +
    '</table><p><b>Total ' + order.total + '</b></p>';
  var r = await AppMint.pdf.fromHtml(html, { fileName: 'receipt-' + order.id, pageSize: 'Letter', save: 'share' });
  if (!r.ok) console.warn(r.code, r.message);
}

Получите байты обратно, чтобы загрузить их самостоятельно:

async function uploadReport() {
  var r = await AppMint.pdf.fromPage({ landscape: true, margins: { top: 10, right: 8, bottom: 10, left: 8 }, save: 'base64' });
  if (!r.ok) return;
  var bin = atob(r.base64), buf = new Uint8Array(bin.length);
  for (var i = 0; i < bin.length; i++) buf[i] = bin.charCodeAt(i);
  await fetch('/api/reports', { method: 'POST', headers: { 'Content-Type': 'application/pdf' }, body: buf });
}

Прямые вызовы принимают параметры как JSON-текст и идентификатор обратного вызова; вместо этого используйте помощник выше:

if (window.WebToApk && typeof window.WebToApk.pdfFromPage === 'function') {
  // AppMint.pdf.fromPage wraps: WebToApk.pdfFromPage(JSON.stringify(options), callbackId)
  // and AppMint.pdf.fromHtml wraps: WebToApk.pdfFromHtml(html, JSON.stringify(options), callbackId)
  AppMint.pdf.fromPage({ save: 'downloads' }).then(function (r) { console.log(r); });
}

Примечания: Скрывайте кнопки и меню в PDF с помощью @media print { .no-print { display: none } }. Скачанный PDF показывает «Загружено <имя> · Открыть», как любая загрузка. fromHtml выполняет скрипты HTML (диаграммы, рисуемые скриптом, появляются), но не имеет доступа к родным функциям приложения. Страницы длиннее одного листа разбиваются так же, как это делает Chrome; используйте break-before: page, чтобы управлять местом разрыва.

navigator.clipboard веб-стандарт#

navigator.clipboard.writeText / readText (and write / read)

Пример

Стандартный Clipboard API, обслуживаемый настоящим буфером обмена телефона внутри приложения.

Возвращает: writeText(text) → Promise, который выполняется после копирования и иначе отклоняется с NotAllowedError. readText() → Promise с текстом из буфера обмена ("", если он пуст). write([item]) / read() → Promise, как в стандарте. Требуется: ничего. Устанавливается при запуске документа, поэтому проверка, выполненная во время разбора вашей страницы, уже его видит.

Копирование:

async function copyLink(link) {
  if (!(navigator.clipboard && navigator.clipboard.writeText)) { alert('Copy is not available'); return; }
  try {
    await navigator.clipboard.writeText(link);
    showMessage('Link copied');
  } catch (e) {
    showMessage('Could not copy');
  }
}

Вставка — по нажатию:

document.getElementById('paste').onclick = async function () {
  try {
    var text = await navigator.clipboard.readText();
    document.getElementById('input').value = text;
  } catch (e) {
    showMessage('Could not read the clipboard');
  }
};

Скопируйте изображение (например, QR-код, нарисованный на canvas) с помощью ClipboardItem:

async function copyCanvas(canvas) {
  if (!(navigator.clipboard && navigator.clipboard.write && window.ClipboardItem)) {
    alert('Copying images is not available here');
    return;
  }
  var blob = await new Promise(function (r) { canvas.toBlob(r, 'image/png'); });
  try {
    await navigator.clipboard.write([new ClipboardItem({ 'image/png': blob })]);
    showMessage('Image copied');
  } catch (e) {
    showMessage('Could not copy the image');
  }
}

Примечания: В приложении writeText/readText вызывают WebToApk.copyToClipboard и WebToApk.readClipboard. write() берёт только первый ClipboardItem и поддерживает text/plain, text/html и одно изображение (до 16 МБ; изображение имеет приоритет над текстом в том же элементе). read() возвращает [item] с текстом, HTML или изображением из буфера обмена ([], если он пуст). Читать его могут только собственные страницы приложения: чужой сайт, открытый внутри приложения, получает отказ (NotAllowedError). Читайте по нажатию пользователя — Android 12+ показывает уведомление «вставлено из буфера обмена». В браузере тот же код работает по собственным правилам разрешений браузера.

navigator.share веб-стандарт#

navigator.share(data) / navigator.canShare(data)

Пример

Стандартный Web Share API, настроенный так, чтобы работать внутри приложения: текст и ссылки открывают системное окно «Поделиться» Android, а files передаются как настоящие файлы (WhatsApp, Gmail, Drive…).

Возвращает: navigator.share → Promise, который выполняется при открытии окна (он не сообщает, какое приложение выбрано). navigator.canShare → true/false. Требуется: ничего. Устанавливается при запуске документа, поэтому проверка, выполненная во время разбора вашей страницы, уже его видит.

Текст и ссылка:

async function shareItem(item) {
  if (!navigator.share) { alert('Sharing is not available here'); return; }
  try {
    await navigator.share({ title: item.name, text: 'Look at this', url: item.link });
  } catch (e) { /* in a browser: AbortError when the user closes the sheet */ }
}

Файлы — сначала проверьте через canShare:

async function sharePdf(blob) {
  var file = new File([blob], 'invoice.pdf', { type: 'application/pdf' });
  var data = { files: [file], title: 'Invoice', text: 'Invoice for March' };
  if (!(navigator.canShare && navigator.canShare(data))) {
    alert('File sharing is not available here');
    return;
  }
  try {
    await navigator.share(data);
  } catch (e) { console.log('Share failed', e); }
}

Несколько файлов, выбранных пользователем: фото одного типа передаются с этим типом:

document.getElementById('photos').onchange = async function (e) {
  var files = Array.from(e.target.files);
  if (!files.length) return;
  var data = { files: files, title: 'Trip photos' };
  if (!(navigator.canShare && navigator.canShare(data))) {
    alert('File sharing is not available here');
    return;
  }
  try {
    await navigator.share(data);
  } catch (err) { console.log('Share failed', err); }
};

Примечания: В приложении title становится темой, а text + url отправляются одним текстом. С files каждый файл читается на странице и отправляется как base64 (WebToApk.shareFiles), поэтому держите файлы в пределах нескольких МБ. Если окно «Поделиться» открыть нельзя, файлы сохраняются как загрузки, а promise всё равно выполняется. canShare возвращает true для любой передачи текста. Приложения, созданные ИИ, могут использовать share из @/lib/appmintNative.

window.print веб-стандарт#

window.print()

Пример

Стандартный вызов печати, настроенный так, чтобы работать внутри приложения: он открывает экран печати Android, где пользователь печатает на бумаге или сохраняет PDF.

Возвращает: ничего. Требуется: ничего.

document.getElementById('print-receipt').onclick = function () {
  window.print();
};

Скройте элементы управления приложения на бумаге с помощью таблицы стилей для печати:

<style>
  @media print {
    nav, .toolbar, button { display: none !important; }
    body { background: #fff; color: #000; }
  }
</style>

Напечатайте транспортную этикетку без окружающей страницы — iframe srcdoc того же источника печатает только собственный документ:

function esc(s) {
  return String(s).replace(/[&<>"]/g, function (c) {
    return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c];
  });
}
function printLabel(name, address) {
  var frame = document.createElement('iframe');
  frame.style.cssText = 'position:fixed;width:0;height:0;border:0';
  frame.srcdoc = '<title>Label</title><style>body{font:18px sans-serif;margin:24px}</style>' +
                 '<b>' + esc(name) + '</b><br>' + esc(address).replace(/\n/g, '<br>');
  frame.onload = function () {
    frame.contentWindow.print();
    setTimeout(function () { frame.remove(); }, 1000);
  };
  document.body.appendChild(frame);
}

Примечания: window.print() печатает главную страницу в том виде, в каком она отображается сейчас. <iframe> того же источника — скрытый iframe, который библиотека печати заполняет чеком и вызывает на нём iframe.contentWindow.print(), или страница в iframe, вызывающая собственный window.print(), — печатает документ ИМЕННО этого iframe (см. __printHtml). Страницу в iframe другого источника напечатать с самой страницы нельзя. Замена существует до первого скрипта страницы. beforeprint отменить нельзя (ни в одном браузере).

__clipboardRead мост#

window.WebToApk.__clipboardRead(): String

navigator.clipboard.read(): `{parts:[{type, text|base64}]}`. Тот же охват, что у readClipboard() в этой оболочке (у которой нет проверки источника).

Пример

Внутренний транспорт для navigator.clipboard.read(). Страницы используют стандартный API; никогда не вызывайте это напрямую — методы __ могут измениться без предупреждения.

Возвращает: {parts:[{type, text|base64}]} — тот же охват, что у readClipboard() в этом приложении. Требуется: ничего включать не нужно.

Используйте публичный API:

canvas.toBlob(async (png) => {
  await navigator.clipboard.write([new ClipboardItem({ 'image/png': png })]);
});
const [item] = await navigator.clipboard.read();
if (item && item.types.includes('image/png')) preview(await item.getType('image/png'));

Вставьте форматированный текст в редактор, сохраняя форматирование, если в буфере обмена есть HTML:

async function pasteRich(editor) {
  try {
    var items = await navigator.clipboard.read();
    if (!items.length) { showMessage('The clipboard is empty'); return; }
    var item = items[0];
    if (item.types.includes('text/html')) {
      editor.insertHtml(sanitize(await (await item.getType('text/html')).text()));
    } else if (item.types.includes('text/plain')) {
      editor.insertText(await (await item.getType('text/plain')).text());
    }
  } catch (e) {
    showMessage('Could not read the clipboard');
  }
}

Вставьте любую скопированную картинку (PNG, JPEG, WebP…) как фото профиля и отличите отказ в чтении от других случаев:

document.getElementById('paste-photo').onclick = async function () {
  try {
    var items = await navigator.clipboard.read();
    var item = items[0];
    var type = item && item.types.find(function (t) { return t.indexOf('image/') === 0; });
    if (!type) { showMessage('Copy a picture first'); return; }
    uploadAvatar(await item.getType(type));   // a Blob of that type
  } catch (e) {
    if (e.name === 'NotAllowedError') showMessage('This page cannot read the clipboard');
    else showMessage('Could not paste the picture');
  }
};

Примечания: Android 12+ показывает собственное сообщение «вставлено из буфера обмена», когда приложение читает буфер обмена.

__clipboardWrite мост#

window.WebToApk.__clipboardWrite(partsJson: String): Boolean

navigator.clipboard.write(): [partsJson] — это `[{type, text}|{type, base64}]` для одного ClipboardItem — text/plain, text/html или изображение, копируемое как content URI.

Пример

Внутренний транспорт для navigator.clipboard.write([ClipboardItem]). Страницы используют стандартный API; никогда не вызывайте это напрямую — методы __ могут измениться без предупреждения.

Возвращает: true, когда фрагмент помещён в буфер. Требуется: ничего включать не нужно.

Используйте публичный API:

canvas.toBlob(async (png) => {
  await navigator.clipboard.write([new ClipboardItem({ 'image/png': png })]);
});
const [item] = await navigator.clipboard.read();
if (item && item.types.includes('image/png')) preview(await item.getType('image/png'));

Скопируйте форматированный текст и его простую версию в одном элементе, чтобы богатые редакторы получили таблицу, а простые поля — текст:

async function copyTable(table) {
  if (!(navigator.clipboard && navigator.clipboard.write && window.ClipboardItem)) return;
  try {
    await navigator.clipboard.write([new ClipboardItem({
      'text/html': new Blob([table.outerHTML], { type: 'text/html' }),
      'text/plain': new Blob([table.innerText], { type: 'text/plain' })
    })]);
    showMessage('Table copied');
  } catch (e) {
    showMessage('Could not copy (' + e.name + ')');   // NotAllowedError when the write fails
  }
}

Скопируйте изображение, которое показывает страница, например QR-код, чтобы его можно было вставить в чат:

async function copyImage(img) {
  try {
    var blob = await (await fetch(img.src)).blob();   // an image/* type, up to 16 MB
    await navigator.clipboard.write([new ClipboardItem({ [blob.type]: blob })]);
    showMessage('Image copied');
  } catch (e) {
    showMessage('Could not copy the image');
  }
}

Примечания: Текст, HTML и одно изображение на элемент. Изображение копируется как content URI через FileProvider приложения, поэтому его можно вставить в WhatsApp, Gmail или Keep.

__printHtml мост#

window.WebToApk.__printHtml(html: String, baseUrl: String, title: String)

Печатает [html] — сериализованный документ iframe того же источника, отправленный прокси `window.print()` ([PrintShim]), — через невидимый WebView по адресу [baseUrl], потому что адаптер печати основного WebView умеет печатать только страницу целиком. [title] задаёт имя задания печати («<название приложения> Document», если пусто).

Пример

Внутренний транспорт печати iframe: iframe.contentWindow.print() (и собственный window.print() страницы в iframe) отправляет сюда документ этого iframe, а приложение печатает его через экран печати Android. Страницы используют стандартные вызовы; никогда не вызывайте это напрямую.

Возвращает: ничего. Требуется: ничего.

Шаблон чека, который теперь работает, — скрытый iframe, заполненный только чеком:

function printReceipt(order) {
  var frame = document.createElement('iframe');
  frame.style.display = 'none';
  document.body.appendChild(frame);
  var doc = frame.contentWindow.document;
  doc.open();
  doc.write('<html><head><title>Receipt ' + order.id + '</title>' +
            '<style>body{font:14px sans-serif} td{padding:2px 8px}</style></head><body>' +
            '<h2>Receipt ' + order.id + '</h2><table>' +
            order.lines.map(function (l) { return '<tr><td>' + l.name + '</td><td>' + l.price + '</td></tr>'; }).join('') +
            '</table></body></html>');
  doc.close();
  frame.contentWindow.print();   // prints the RECEIPT, not the page behind it
  setTimeout(function () { frame.remove(); }, 1000);
}

Ваша собственная страница билета, загруженная в скрытый iframe того же источника: её CSS и изображения загружаются, а её <title> задаёт имя задания печати:

function printTicket(id) {
  var frame = document.createElement('iframe');
  frame.style.cssText = 'position:fixed;width:0;height:0;border:0';
  frame.src = 'ticket.html?id=' + encodeURIComponent(id);
  frame.onload = function () {
    frame.contentWindow.print();   // prints ticket.html's DOM as it stands now
    setTimeout(function () { frame.remove(); }, 1000);
  };
  document.body.appendChild(frame);
}

Измените документ iframe непосредственно перед снимком с помощью beforeprint в окне iframe:

function printWithTimestamp(frame) {
  var w = frame.contentWindow;
  w.addEventListener('beforeprint', function () {
    var p = w.document.createElement('p');
    p.textContent = 'Printed ' + new Date().toLocaleString();
    w.document.body.appendChild(p);
  }, { once: true });
  w.print();   // the printed copy includes the line added above
}

Примечания: Аргументы: (html, baseUrl, title) — сериализованный документ iframe, адрес, относительно которого разрешаются его ссылки (собственный адрес страницы для iframe about:blank), и имя задания печати (<title> iframe, иначе страницы). Снимок делается при вызове print() — скрипты внутри него повторно не выполняются — а события beforeprint / afterprint срабатывают в окне iframe вокруг него. Iframe другого источника напечатать с самой страницы нельзя.

clearSharedText мост#

window.WebToApk.clearSharedText()

Очищает сохранённое после того, как страница его использовала.

Пример

Забывает текст или ссылку, которыми поделились с приложением, после того как ваша страница их использовала.

Возвращает: ничего. Требуется: ничего (для самой передачи нужна опция Приём данных из других приложений с отмеченным пунктом Текст и ссылки).

Прочитайте сохранённое, используйте его, затем очистите, чтобы оно не обрабатывалось повторно после перезагрузки:

if (window.WebToApk && typeof window.WebToApk.getSharedText === 'function') {
  var raw = window.WebToApk.getSharedText();
  if (raw) {
    var shared = JSON.parse(raw);          // { text, subject, url, receivedAt }
    saveNote(shared.subject, shared.text);
    window.WebToApk.clearSharedText();     // getSharedText() now returns ""
  }
}

window.addEventListener('appmint:shared', function (e) {
  saveNote(e.detail.subject, e.detail.text);
  if (window.WebToApk && window.WebToApk.clearSharedText) window.WebToApk.clearSharedText();
});

Сначала спросите и очистите переданное, какую бы кнопку ни нажал пользователь:

function askAboutShare(s) {
  var dialog = document.getElementById('share-dialog');
  document.getElementById('share-preview').textContent = s.text;
  dialog.hidden = false;
  function done() {
    dialog.hidden = true;
    if (window.WebToApk && typeof window.WebToApk.clearSharedText === 'function') {
      window.WebToApk.clearSharedText();
    }
  }
  document.getElementById('share-save').onclick = function () { saveNote(s.subject, s.text); done(); };
  document.getElementById('share-discard').onclick = done;
}

Очищайте его только после надёжного сохранения, чтобы неудавшуюся загрузку можно было повторить:

async function importShare() {
  if (!(window.WebToApk && typeof window.WebToApk.getSharedText === 'function')) return;
  var raw = window.WebToApk.getSharedText();
  if (!raw) return;
  var s = JSON.parse(raw);
  try {
    var res = await fetch('/api/bookmarks', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ url: s.url || s.text, title: s.subject })
    });
    if (!res.ok) throw new Error('HTTP ' + res.status);
    window.WebToApk.clearSharedText();
  } catch (e) {
    showMessage('Could not save the link — tap Retry');   // Retry calls importShare() again
  }
}

Примечания: Очистка влияет только на getSharedText(). Новый обмен заменяет сохранённый и снова вызывает appmint:shared. Файлы, которыми поделились с приложением, освобождаются отдельно с помощью AppMint.releaseOpenedFile().

copyToClipboard мост#

window.WebToApk.copyToClipboard(text: String): Boolean

Используйте обёртку на промисах: const r = await AppMint.requestReview(); // {result, openedStore} const r = await AppMint.requestReview({ openStore: true }); // страница в магазине, если окна нет r.result: 'shown' | 'not_from_play' | 'quota' | 'error'. См. RatePrompt.

Описано в своей группе, Окно оценки Google Play внутри приложения, а не отдельно.

Пример

Копирует простой текст в буфер обмена телефона. Обычно страницы используют стандартный navigator.clipboard.writeText(text), который оболочка направляет сюда.

Возвращает: true при успехе, false при неудаче (синхронно). Требуется: ничего.

Сначала стандартный API:

document.getElementById('copy-btn').onclick = async function () {
  try {
    await navigator.clipboard.writeText('PROMO-2026');
    showMessage('Copied');
  } catch (e) {
    showMessage('Could not copy');
  }
};

Прямой вызов:

if (window.WebToApk && typeof window.WebToApk.copyToClipboard === 'function') {
  var ok = window.WebToApk.copyToClipboard('PROMO-2026');
  showMessage(ok ? 'Copied' : 'Could not copy');
}

Кнопка «Копировать» у каждого блока кода, которая использует синхронный ответ для обновления подписи:

document.querySelectorAll('pre').forEach(function (pre) {
  var btn = document.createElement('button');
  btn.textContent = 'Copy';
  btn.onclick = function () {
    var ok = !!(window.WebToApk && typeof window.WebToApk.copyToClipboard === 'function' &&
                window.WebToApk.copyToClipboard(pre.innerText));
    btn.textContent = ok ? 'Copied' : 'Copy failed';
    setTimeout(function () { btn.textContent = 'Copy'; }, 1500);
  };
  pre.parentNode.insertBefore(btn, pre);
});

Примечания: Только простой текст (без изображений и HTML). Android 13+ показывает собственную небольшую подсказку «Скопировано», поэтому там своё сообщение может не понадобиться.

getSharedText мост#

window.WebToApk.getSharedText(): String

Текст или ссылка, которыми другое приложение поделилось С этим приложением («Поделиться в <приложение>» — видео YouTube, которым поделились из приложения YouTube, приходит в виде ссылки), в виде JSON `{text, subject, url, receivedAt}` или "", если ничем не делились. Хранится до прочтения, поэтому приложение, запущенное после передачи, всё равно её увидит; передача, пришедшая во время работы приложения, также вызывает `appmint:shared`. Фото или видео, которым поделились с подписью, доставляют файл через AppMint.getOpenedFile(), а подпись — сюда. Требуется опция сборки «Приём данных из других приложений» с отмеченным пунктом «Текст и ссылки», которая регистрирует пункт в окне «Поделиться».

Пример

Текст или ссылка, которыми другое приложение поделилось С вашим приложением («Поделиться → ваше приложение»), в виде JSON-строки или "", если ничем не делились.

Возвращает: JSON-строку {text, subject, url, receivedAt} или "" (синхронно). url — первая ссылка http(s):// в text (или ""); receivedAt — время в миллисекундах. Требуется: включённая при сборке опция Приём данных из других приложений с отмеченным пунктом Текст и ссылки — она добавляет ваше приложение в меню «Поделиться» других приложений.

Переданное хранится, пока вы его не очистите, поэтому прочитайте его при запуске и слушайте то, что приходит, пока приложение работает:

function handleShare(s) {
  addBookmark(s.url || s.text, s.subject);
  if (window.WebToApk && window.WebToApk.clearSharedText) window.WebToApk.clearSharedText();
}

// 1. On start (the app may have been opened by the share).
if (window.WebToApk && typeof window.WebToApk.getSharedText === 'function') {
  var raw = window.WebToApk.getSharedText();
  if (raw) handleShare(JSON.parse(raw));
}

// 2. While running: detail has the same keys { text, subject, url, receivedAt }.
window.addEventListener('appmint:shared', function (e) {
  handleShare(e.detail);
});

Глобальный хук получает тот же объект, если вам так удобнее:

window.onAppMintShared = function (s) { handleShare(s); };

Откройте видео YouTube, которым поделились с приложением, используя поле url:

function openSharedVideo() {
  if (!(window.WebToApk && typeof window.WebToApk.getSharedText === 'function')) return;
  var raw = window.WebToApk.getSharedText();
  if (!raw) return;
  var s = JSON.parse(raw);
  var m = /(?:youtu\.be\/|[?&]v=)([\w-]{11})/.exec(s.url || s.text);
  if (m) {
    showVideo(m[1]);
    window.WebToApk.clearSharedText();
  } else {
    showMessage('That share is not a video link');
  }
}

Примечания: Видео YouTube, которым поделились из приложения YouTube, приходит в виде ссылки. Фото или видео, которым поделились С подписью, доставляют файл через AppMint.getOpenedFile(), а подпись — сюда. Очищайте переданное после использования, иначе вы увидите его снова при следующем запуске. Приложения, созданные ИИ, могут использовать sharedText() / onShared() из @/lib/appmintNative.

pdfFromHtml мост#

window.WebToApk.pdfFromHtml(html: String, optionsJson: String, callbackId: String)

Отображает [html] на невидимой странице по адресу приложения и сохраняет как PDF — используйте `AppMint.pdf.fromHtml(html, options)`.

Пример

Описано вместе с AppMint.pdf — пример там показывает и этот.

Создаёт настоящий PDF (выделяемый текст, векторная графика) без окна печати: fromPage сохраняет страницу такой, как она на экране, с версткой по вашему CSS @media print; fromHtml отображает переданный вами HTML на невидимой странице по собственному адресу приложения — поэтому относительные изображения, таблицы стилей и шрифты загружаются так же, как на вашей странице, — дожидается шрифтов и всех изображений, затем сохраняет. window.print() по-прежнему открывает обычное окно печати.

Возвращает: Promise с {ok: true, fileName, pages, bytes, saved, uri?, base64?} или {ok: false, code, message} — code это busy (одновременно только один PDF), bad-options, failed, too-large (base64 больше 25 МБ) или permission-denied (хранилище отказало на Android 7–9). Требуется: ничего — есть в каждой сборке. Сохранение в «Загрузки» не требует разрешения на Android 10+.

Параметры: fileName (".pdf" добавляется; по умолчанию — заголовок страницы), pageSize 'A4' (по умолчанию) | 'Letter' | 'Legal' | 'A3' | 'A5', landscape (true/false), margins в миллиметрах (одно число или {top, right, bottom, left}; если не указано — ваше собственное правило @page), save 'downloads' (по умолчанию) | 'share' | 'base64'.

Сохранить текущую страницу в «Загрузки»:

async function saveInvoice() {
  if (!(window.AppMint && AppMint.pdf)) { window.print(); return; }   // in a browser
  var r = await AppMint.pdf.fromPage({ fileName: 'invoice-1042', pageSize: 'A4', margins: 12 });
  if (r.ok) console.log('Saved ' + r.fileName + ' (' + r.pages + ' pages)');
  else alert('Could not make the PDF: ' + r.message);
}

Чек, собранный из HTML, сразу в окно «Поделиться» (изображения и шрифты загружаются до создания PDF):

async function shareReceipt(order) {
  var html = '<style>body{font-family:sans-serif} td{padding:4px 8px}</style>' +
    '<img src="img/logo.png" width="120"><h1>Receipt #' + order.id + '</h1><table>' +
    order.items.map(function (i) { return '<tr><td>' + i.name + '</td><td>' + i.price + '</td></tr>'; }).join('') +
    '</table><p><b>Total ' + order.total + '</b></p>';
  var r = await AppMint.pdf.fromHtml(html, { fileName: 'receipt-' + order.id, pageSize: 'Letter', save: 'share' });
  if (!r.ok) console.warn(r.code, r.message);
}

Получите байты обратно, чтобы загрузить их самостоятельно:

async function uploadReport() {
  var r = await AppMint.pdf.fromPage({ landscape: true, margins: { top: 10, right: 8, bottom: 10, left: 8 }, save: 'base64' });
  if (!r.ok) return;
  var bin = atob(r.base64), buf = new Uint8Array(bin.length);
  for (var i = 0; i < bin.length; i++) buf[i] = bin.charCodeAt(i);
  await fetch('/api/reports', { method: 'POST', headers: { 'Content-Type': 'application/pdf' }, body: buf });
}

Прямые вызовы принимают параметры как JSON-текст и идентификатор обратного вызова; вместо этого используйте помощник выше:

if (window.WebToApk && typeof window.WebToApk.pdfFromPage === 'function') {
  // AppMint.pdf.fromPage wraps: WebToApk.pdfFromPage(JSON.stringify(options), callbackId)
  // and AppMint.pdf.fromHtml wraps: WebToApk.pdfFromHtml(html, JSON.stringify(options), callbackId)
  AppMint.pdf.fromPage({ save: 'downloads' }).then(function (r) { console.log(r); });
}

Примечания: Скрывайте кнопки и меню в PDF с помощью @media print { .no-print { display: none } }. Скачанный PDF показывает «Загружено <имя> · Открыть», как любая загрузка. fromHtml выполняет скрипты HTML (диаграммы, рисуемые скриптом, появляются), но не имеет доступа к родным функциям приложения. Страницы длиннее одного листа разбиваются так же, как это делает Chrome; используйте break-before: page, чтобы управлять местом разрыва.

pdfFromPage мост#

window.WebToApk.pdfFromPage(optionsJson: String, callbackId: String)

Сохраняет страницу как PDF без окна печати — используйте `AppMint.pdf.fromPage(options)`. [optionsJson] `{fileName, pageSize, landscape, margins, save}`; завершается с [callbackId].

Пример

Описано вместе с AppMint.pdf — пример там показывает и этот.

Создаёт настоящий PDF (выделяемый текст, векторная графика) без окна печати: fromPage сохраняет страницу такой, как она на экране, с версткой по вашему CSS @media print; fromHtml отображает переданный вами HTML на невидимой странице по собственному адресу приложения — поэтому относительные изображения, таблицы стилей и шрифты загружаются так же, как на вашей странице, — дожидается шрифтов и всех изображений, затем сохраняет. window.print() по-прежнему открывает обычное окно печати.

Возвращает: Promise с {ok: true, fileName, pages, bytes, saved, uri?, base64?} или {ok: false, code, message} — code это busy (одновременно только один PDF), bad-options, failed, too-large (base64 больше 25 МБ) или permission-denied (хранилище отказало на Android 7–9). Требуется: ничего — есть в каждой сборке. Сохранение в «Загрузки» не требует разрешения на Android 10+.

Параметры: fileName (".pdf" добавляется; по умолчанию — заголовок страницы), pageSize 'A4' (по умолчанию) | 'Letter' | 'Legal' | 'A3' | 'A5', landscape (true/false), margins в миллиметрах (одно число или {top, right, bottom, left}; если не указано — ваше собственное правило @page), save 'downloads' (по умолчанию) | 'share' | 'base64'.

Сохранить текущую страницу в «Загрузки»:

async function saveInvoice() {
  if (!(window.AppMint && AppMint.pdf)) { window.print(); return; }   // in a browser
  var r = await AppMint.pdf.fromPage({ fileName: 'invoice-1042', pageSize: 'A4', margins: 12 });
  if (r.ok) console.log('Saved ' + r.fileName + ' (' + r.pages + ' pages)');
  else alert('Could not make the PDF: ' + r.message);
}

Чек, собранный из HTML, сразу в окно «Поделиться» (изображения и шрифты загружаются до создания PDF):

async function shareReceipt(order) {
  var html = '<style>body{font-family:sans-serif} td{padding:4px 8px}</style>' +
    '<img src="img/logo.png" width="120"><h1>Receipt #' + order.id + '</h1><table>' +
    order.items.map(function (i) { return '<tr><td>' + i.name + '</td><td>' + i.price + '</td></tr>'; }).join('') +
    '</table><p><b>Total ' + order.total + '</b></p>';
  var r = await AppMint.pdf.fromHtml(html, { fileName: 'receipt-' + order.id, pageSize: 'Letter', save: 'share' });
  if (!r.ok) console.warn(r.code, r.message);
}

Получите байты обратно, чтобы загрузить их самостоятельно:

async function uploadReport() {
  var r = await AppMint.pdf.fromPage({ landscape: true, margins: { top: 10, right: 8, bottom: 10, left: 8 }, save: 'base64' });
  if (!r.ok) return;
  var bin = atob(r.base64), buf = new Uint8Array(bin.length);
  for (var i = 0; i < bin.length; i++) buf[i] = bin.charCodeAt(i);
  await fetch('/api/reports', { method: 'POST', headers: { 'Content-Type': 'application/pdf' }, body: buf });
}

Прямые вызовы принимают параметры как JSON-текст и идентификатор обратного вызова; вместо этого используйте помощник выше:

if (window.WebToApk && typeof window.WebToApk.pdfFromPage === 'function') {
  // AppMint.pdf.fromPage wraps: WebToApk.pdfFromPage(JSON.stringify(options), callbackId)
  // and AppMint.pdf.fromHtml wraps: WebToApk.pdfFromHtml(html, JSON.stringify(options), callbackId)
  AppMint.pdf.fromPage({ save: 'downloads' }).then(function (r) { console.log(r); });
}

Примечания: Скрывайте кнопки и меню в PDF с помощью @media print { .no-print { display: none } }. Скачанный PDF показывает «Загружено <имя> · Открыть», как любая загрузка. fromHtml выполняет скрипты HTML (диаграммы, рисуемые скриптом, появляются), но не имеет доступа к родным функциям приложения. Страницы длиннее одного листа разбиваются так же, как это делает Chrome; используйте break-before: page, чтобы управлять местом разрыва.

print мост#

window.WebToApk.print()

Передаёт страницу в её нынешнем виде в диалог печати Android, который может напечатать её на бумаге или сохранить как PDF. Настраивать ничего не нужно — документ отображает WebView.

Описано в своей группе, Печать, а не отдельно.

Пример

Открывает экран печати Android для страницы в её нынешнем виде — печать на бумаге или «Сохранить как PDF». Обычно страницы используют стандартный window.print(), который оболочка направляет сюда.

Возвращает: ничего. Требуется: ничего.

Сначала стандартный API:

document.getElementById('print-btn').onclick = function () {
  window.print();   // in the app: the Android print screen
};

Прямой вызов делает то же самое:

if (window.WebToApk && typeof window.WebToApk.print === 'function') {
  window.WebToApk.print();
}

Напечатайте один счёт из списка, пометив его для таблицы стилей печати (оставьте метку — экран печати размечает страницу после возврата из print()):

// CSS: @media print { .invoice:not(.print-me), nav, button { display: none !important; } }
function printInvoice(id) {
  document.querySelectorAll('.invoice.print-me').forEach(function (el) {
    el.classList.remove('print-me');
  });
  document.getElementById('invoice-' + id).classList.add('print-me');
  if (window.WebToApk && typeof window.WebToApk.print === 'function') {
    window.WebToApk.print();
  } else {
    window.print();   // a normal browser
  }
}

Примечания: Печатается весь WebView — главная страница. Собственный print() iframe того же источника печатает только этот iframe (см. window.print). Используйте таблицу стилей @media print, чтобы скрыть кнопки и меню. Задание называется «<название приложения> Document». Ошибки только записываются в журнал.

readClipboard мост#

window.WebToApk.readClipboard(): String

Используйте обёртку на промисах: const r = await AppMint.requestReview(); // {result, openedStore} const r = await AppMint.requestReview({ openStore: true }); // страница в магазине, если окна нет r.result: 'shown' | 'not_from_play' | 'quota' | 'error'. См. RatePrompt.

Описано в своей группе, Окно оценки Google Play внутри приложения, а не отдельно.

Пример

Читает текст, который сейчас находится в буфере обмена телефона. Обычно страницы используют стандартный navigator.clipboard.readText(), который оболочка направляет сюда.

Возвращает: текст или "", если буфер обмена пуст или его нельзя прочитать (синхронно). Требуется: ничего.

Сначала стандартный API — например, кнопка «Вставить код»:

document.getElementById('paste-btn').onclick = async function () {
  try {
    var text = await navigator.clipboard.readText();
    if (text) document.getElementById('code').value = text.trim();
    else showMessage('The clipboard is empty');
  } catch (e) {
    showMessage('Could not read the clipboard');
  }
};

Прямой вызов:

if (window.WebToApk && typeof window.WebToApk.readClipboard === 'function') {
  var text = window.WebToApk.readClipboard();
  if (text) document.getElementById('code').value = text;
}

Кнопка «Вставить ссылку», которая берёт первый веб-адрес из скопированного:

document.getElementById('paste-link').onclick = function () {
  if (!(window.WebToApk && typeof window.WebToApk.readClipboard === 'function')) return;
  var text = window.WebToApk.readClipboard();
  var m = /https?:\/\/\S+/.exec(text);
  if (m) document.getElementById('link').value = m[0];
  else showMessage('There is no link on the clipboard');
};

Примечания: Читать его могут только страницы вашего собственного приложения: чужой сайт, открытый внутри приложения, получает "". Android позволяет приложению читать буфер обмена только пока оно на экране, а Android 12+ показывает уведомление «вставлено из буфера обмена» — читайте по нажатию пользователя, а не при загрузке страницы.

shareFiles мост#

window.WebToApk.shareFiles(filesJson: String, title: String, text: String): Boolean

НАСТОЯЩАЯ нативная передача файлов для navigator.share({files:[...]}). В Android WebView нет Web Share API, а text/plain ACTION_SEND не может передать файл — поэтому раньше изображение лишь СОХРАНЯЛОСЬ, а не передавалось. filesJson — это [{name, mimeType, base64}, ...]; мы записываем каждый файл в кэш, открываем его через FileProvider приложения и вызываем ACTION_SEND / ACTION_SEND_MULTIPLE, чтобы открылось настоящее окно «Поделиться» Android (WhatsApp, Gmail, …). Возвращает true, если окно открылось.

Пример

Делится одним или несколькими настоящими файлами (изображением, PDF…) через системное окно «Поделиться» Android. Обычно страницы используют стандартный navigator.share({ files: [...] }), который оболочка направляет сюда.

Возвращает: true, если окно «Поделиться» открылось, иначе false (синхронно). Требуется: ничего.

Сначала стандартный API:

async function shareCanvas(canvas) {
  var blob = await new Promise(function (r) { canvas.toBlob(r, 'image/png'); });
  var file = new File([blob], 'drawing.png', { type: 'image/png' });
  if (!(navigator.canShare && navigator.canShare({ files: [file] }))) {
    alert('File sharing is not available here');
    return;
  }
  try {
    await navigator.share({ files: [file], title: 'My drawing', text: 'Made in my app' });
  } catch (e) { /* in a browser: the user closed the sheet */ }
}

Прямой вызов принимает JSON-строку с [{name, mimeType, base64}] (base64 без префикса data:), затем title и text:

function shareBase64Png(base64) {
  if (!(window.WebToApk && typeof window.WebToApk.shareFiles === 'function')) return false;
  var files = [{ name: 'receipt.png', mimeType: 'image/png', base64: base64 }];
  return window.WebToApk.shareFiles(JSON.stringify(files), 'Receipt', 'Your receipt');
}

Экспортируйте таблицу в файл CSV (текст перед base64 кодируется в UTF-8):

function shareCsv(rows) {
  if (!(window.WebToApk && typeof window.WebToApk.shareFiles === 'function')) return;
  var csv = rows.map(function (r) {
    return r.map(function (c) { return '"' + String(c).replace(/"/g, '""') + '"'; }).join(',');
  }).join('\n');
  var base64 = btoa(unescape(encodeURIComponent(csv)));
  var files = [{ name: 'expenses.csv', mimeType: 'text/csv', base64: base64 }];
  if (!window.WebToApk.shareFiles(JSON.stringify(files), 'Expenses', '')) {
    showMessage('Could not open the share sheet');
  }
}

Примечания: Файлы записываются в кэш приложения и передаются через его FileProvider. Небезопасные символы в name становятся _. Несколько файлов одного типа передаются с этим типом; смешанные типы — как */*. Файл целиком передаётся по мосту как base64, поэтому держите его в пределах нескольких МБ. Если navigator.share({files}) не может открыть окно, оболочка сохраняет файлы как загрузки, а promise всё равно выполняется.

shareNative мост#

window.WebToApk.shareNative(title: String, text: String, url: String)

Системное окно «Поделиться» Android, системный буфер обмена и закрытие окна, открытого страницей. Это обычные веб-аналоги (navigator.share, navigator.clipboard, window.close), реализованные нативно, чтобы внутри приложения они вели себя так же.

Описано в своей группе, Поделиться, буфер обмена и окно, а не отдельно.

Пример

Открывает системное окно «Поделиться» Android с заголовком, текстом и ссылкой. Обычно страницы используют стандартный navigator.share({title, text, url}), который оболочка направляет сюда.

Возвращает: ничего (не сообщает, какое приложение выбрал пользователь). Требуется: ничего.

Сначала стандартный API — он работает и в мобильном браузере:

document.getElementById('share-btn').onclick = async function () {
  if (!navigator.share) { alert('Sharing is not available here'); return; }
  try {
    await navigator.share({ title: 'My app', text: 'Look at this', url: 'https://mysite.com/item/42' });
  } catch (e) {
    // in a browser: AbortError when the user closes the sheet
  }
};

Прямой вызов, все три аргумента — строки (используйте '', чтобы пропустить один):

if (window.WebToApk && typeof window.WebToApk.shareNative === 'function') {
  window.WebToApk.shareNative('My app', 'Look at this', 'https://mysite.com/item/42');
}

Только текст, без ссылки — например, счёт в игре:

function shareScore(score) {
  if (!(window.WebToApk && typeof window.WebToApk.shareNative === 'function')) return;
  window.WebToApk.shareNative('My score', 'I scored ' + score + ' points. Can you beat it?', '');
}

Примечания: Передаваемый текст — это text + новая строка + url (или только одно из них). title становится темой письма и заголовком окна «Поделиться». Только текст и ссылки — чтобы поделиться файлом, используйте navigator.share({files}) (см. shareFiles).

Ссылки кнопок share: в сообщениях внутри приложения. Кнопка со ссылкой share:<text> открывает это же окно «Поделиться» с этим текстом, без кода на странице. Шаблон Пригласить друзей использует share:Try {app}: {store}; приложение подставляет собственное название и адрес в Play. Ваша страница может узнать, что это произошло:

window.addEventListener('appmint:inapp', function (e) {
  var d = e.detail;
  if (d.type === 'clicked' && d.link && d.link.indexOf('share:') === 0) analytics('invite_shared', d.name);
});

Если вы сами рисуете сообщения внутри приложения (setInAppPresentation('none')), обрабатывайте кнопку share: таким же образом:

function runInAppButton(link) {
  if (link.indexOf('share:') === 0 && window.WebToApk && WebToApk.shareNative) {
    WebToApk.shareNative('', link.slice(6), '');   // text only
  }
}

Ссылка copy:<text> копирует, а не передаёт, например код купона из рекламного сообщения:

window.addEventListener('appmint:inapp', function (e) {
  var d = e.detail;
  if (d.type === 'clicked' && d.link === 'copy:SAVE20') showToast('Code SAVE20 copied');
});