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

JavaScript bridge API

Файлы: открытые в этом приложении

Получение того, что другое приложение передало через «Открыть с помощью», порциями, которые страница забирает сама.

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

AppMint.getOpenedFile()

Пример

Передаёт вашей странице файл, которым поделились с приложением или который открыли в нём («Открыть с помощью…»), как бы поздно ни запустилась страница.

Возвращает: Promise<object | null> — null, когда приложение открыто обычным образом. У объекта есть token, name, mimeType, size, kind, url, file, getFile(), а также text (текстовые файлы до 32 МБ) или base64 (остальные файлы с данными до 8 МБ), когда это недорого. kind равен 'image' | 'video' | 'audio' | 'document' | 'data'. Требуется: при сборке включите Receive Shares From Other Apps и отметьте виды файлов, которые принимает ваше приложение.

async function handleStartFile() {
  if (!window.AppMint || typeof AppMint.getOpenedFile !== 'function') return;  // not in the app, or feature off
  const f = await AppMint.getOpenedFile();
  if (!f) return;                                  // opened normally
  console.log(f.name, f.mimeType, f.size, f.kind);
  if (f.kind === 'video') {
    document.querySelector('video').src = f.url;   // streams from disk, can seek
  } else if (f.kind === 'image') {
    document.querySelector('img').src = f.url;
  } else if (f.kind === 'document') {
    const file = await f.getFile();                // a real File
    await upload(file);
  } else {
    const text = typeof f.text === 'string' ? f.text : await f.file.text();
    importData(text);
  }
}
handleStartFile();

Или слушайте событие (и необязательный хук window.onAppMintFileOpen): оно срабатывает один раз на каждый новый файл, с тем же объектом, что и detail:

window.onAppMintFileOpen = function (f) { console.log('hook:', f.name); };

window.addEventListener('appmint:fileopen', function (e) {
  const f = e.detail;             // {token, name, mimeType, size, kind, url, file, getFile, text?, base64?}
  showFile(f);
});
window.addEventListener('appmint:fileopenerror', function (e) {
  alert('Could not read ' + e.detail.name + ': ' + e.detail.error);   // detail: {name, error}
});

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

import { useEffect, useState } from 'react';

export function useOpenedFile() {
  const [file, setFile] = useState(null);
  useEffect(function () {
    const A = window.AppMint;
    if (!A || typeof A.getOpenedFile !== 'function') return;
    let alive = true;
    A.getOpenedFile().then(function (f) { if (alive && f) setFile(f); });
    function onOpen(e) { setFile(e.detail); }
    window.addEventListener('appmint:fileopen', onOpen);
    return function () { alive = false; window.removeEventListener('appmint:fileopen', onOpen); };
  }, []);
  return file;
}

Примечания: Изображения, видео, аудио и любые файлы больше 64 МБ приходят только как url (file равен null); вызывайте f.getFile(), когда действительно нужны байты. Файлы с данными и документы приходят также как f.file. Файл хранится для вашей страницы, пока вы его не запросите, поэтому SPA, которое монтируется поздно, всё равно его получит; каждый раз возвращается самый последний. Если приложение уже открыто и приходит ещё один файл, событие срабатывает снова. Вызовите AppMint.releaseOpenedFile(f) по завершении, чтобы освободить кэшированную копию.

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

AppMint.getOpenedFiles()

Пример

Передаёт вашей странице все файлы, которыми поделились с приложением или которые открыли в нём за этот сеанс, — полезно, когда пользователь делится сразу несколькими фото.

Возвращает: Promise<Array> тех же объектов, которые возвращает AppMint.getOpenedFile() (token, name, mimeType, size, kind, url, file, getFile(), а также text / base64, когда это недорого). Пустой массив, если ничего не получено. Требуется: при сборке включите Receive Shares From Other Apps и отметьте виды файлов, которые принимает ваше приложение.

async function importShared() {
  if (!window.AppMint || typeof AppMint.getOpenedFiles !== 'function') return;
  const files = await AppMint.getOpenedFiles();
  const gallery = document.getElementById('gallery');
  files.forEach(function (f) {
    if (f.kind === 'image') {
      const img = document.createElement('img');
      img.src = f.url;
      img.alt = f.name;
      gallery.appendChild(img);
    } else {
      console.log('received', f.name, f.size + ' bytes');
    }
  });
}
importShared();

Новые файлы, которые приходят, пока страница открыта, тоже вызывают appmint:fileopen, по одному событию на файл:

window.addEventListener('appmint:fileopen', function (e) { addToGallery(e.detail); });

Загрузка всего, чем поделился пользователь, одним запросом, в виде настоящих файлов:

async function uploadShared() {
  if (!window.AppMint || typeof AppMint.getOpenedFiles !== 'function') return;
  const files = await AppMint.getOpenedFiles();
  if (!files.length) return;
  const form = new FormData();
  for (const f of files) {
    try {
      form.append('files', await f.getFile(), f.name);   // url-only media is fetched here
    } catch (e) {
      console.warn('skipped', f.name, e.message);        // released earlier
    }
  }
  await fetch('/api/upload', { method: 'POST', body: form });
}

Примечания: Список — это копия; он растёт по мере поступления новых файлов. Файлы, которые вы освободили через AppMint.releaseOpenedFile(f), остаются в списке, но их url больше не работает.

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

AppMint.releaseOpenedFile(file)

Пример

Удаляет кэшированную копию файла, которым поделились с приложением или который открыли в нём, когда ваша страница с ним закончила.

Возвращает: ничего. Требуется: при сборке включите Receive Shares From Other Apps.

async function importAndForget() {
  if (!window.AppMint || typeof AppMint.getOpenedFile !== 'function') return;
  const f = await AppMint.getOpenedFile();
  if (!f) return;
  const file = await f.getFile();        // take your own copy of the bytes first
  await saveToMyDatabase(file);
  AppMint.releaseOpenedFile(f);          // then free the cache on disk
}

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

async function playShared() {
  if (!window.AppMint || typeof AppMint.getOpenedFile !== 'function') return;
  const f = await AppMint.getOpenedFile();
  if (!f || f.kind !== 'video') return;
  const video = document.querySelector('video');
  video.src = f.url;
  video.play();
  video.addEventListener('ended', function () {
    video.removeAttribute('src');
    video.load();
    AppMint.releaseOpenedFile(f);        // the url stops working from here
  }, { once: true });
}

После импорта пачки фото, которыми поделились, освободите сразу все кэшированные копии:

async function importAllPhotos() {
  if (!window.AppMint || typeof AppMint.getOpenedFiles !== 'function') return;
  const files = await AppMint.getOpenedFiles();
  for (const f of files) {
    if (f.kind !== 'image') continue;
    const file = await f.getFile();      // fetched from f.url
    await savePhoto(file);
    AppMint.releaseOpenedFile(f);
  }
}

Примечания: Передайте объект, который вы получили (нужен его token). Уже полученный File остаётся действительным; url перестаёт работать, а getFile() отклоняется, если File ещё не был загружен. Не освобождайте видео, которое ещё воспроизводится из f.url. Приложение также очищает эти копии при закрытии экрана, поэтому раннее освобождение нужно ради места на диске во время долгого сеанса.

openedFileChunk мост#

window.WebToApk.openedFileChunk(token: String, offset: String, len: Int): String

Внутренний механизм для AppMint.getOpenedFile() / getOpenedFiles() / releaseOpenedFile(). Страница сама забирает файлы, а не получает их принудительно: файл, открытый при холодном запуске, ждёт, пока его не запросят, поэтому SPA, монтирующееся поздно, всё равно его получит. Метаданные каждого файла содержат `kind` (image | video | audio | document | data) и `url` — https://appassets.androidplatform.net/appmint-shared/<token>/<name>, отдаваемый из кэшированной копии с поддержкой диапазонов байт, поэтому видео передаётся в <video src> и перематывается. Файлы с данными и документы также забираются срезами MAX_IO_CHUNK в File; медиа и всё больше 64 МБ остаются только по url (getFile() загружает по требованию), поэтому клип в 300 МБ никогда не лежит в массиве JS.

Описано своей группой, Файлы, которыми поделились с приложением или которые открыли в нём, а не отдельно.

Пример

Внутренний механизм для AppMint.getOpenedFile(): возвращает один срез байт файла, которым поделились или который открыли, а помощник склеивает их в File. Страницам следует использовать публичный API — f.file, f.getFile() или f.url.

Возвращает: (синхронно) строку base64 длиной до len байт, начиная с offset (смещение передаётся строкой, поэтому файлы больше 2 ГБ тоже работают); "" в конце файла, для неизвестного или освобождённого токена или при ошибке чтения. Требуется: при сборке включите Receive Shares From Other Apps.

Публичный API (используйте его):

if (window.AppMint && typeof AppMint.getOpenedFile === 'function') {
  AppMint.getOpenedFile().then(function (f) {
    if (!f) return;
    return f.getFile().then(function (file) { console.log('got', file.size, 'bytes'); });
  });
}

Что помощник делает с ним — показано только чтобы пояснить аргументы:

if (window.WebToApk && typeof WebToApk.openedFileChunk === 'function') {
  const meta = JSON.parse(WebToApk.openedFilesPending() || '[]')[0];
  if (meta) {
    const b64 = WebToApk.openedFileChunk(meta.token, '0', WebToApk.openedFileMaxChunk());
    console.log('first slice is', atob(b64).length, 'bytes of', meta.size);
  }
}

Чтение очень большого файла без срезов — передавайте f.url потоком и показывайте прогресс (здесь подсчёт строк CSV):

async function countRows() {
  if (!window.AppMint || typeof AppMint.getOpenedFile !== 'function') return;
  const f = await AppMint.getOpenedFile();
  if (!f) return;
  const res = await fetch(f.url);                // served from disk, no base64 round trips
  if (!res.ok) throw new Error('shared file unavailable (' + res.status + ')');
  const reader = res.body.getReader();
  let seen = 0, rows = 0;
  for (;;) {
    const step = await reader.read();
    if (step.done) break;
    seen += step.value.length;
    for (let i = 0; i < step.value.length; i++) if (step.value[i] === 10) rows++;
    showProgress(seen / f.size);
  }
  console.log(rows + ' rows in ' + f.name);
}

Примечания: len ограничивается диапазоном от 1 байта до 4 МБ. Для медиафайлов передача f.url потоком обходится куда дешевле, чем получение срезов.

openedFileCollected мост#

window.WebToApk.openedFileCollected(token: String)

Внутренний механизм для AppMint.getOpenedFile() / getOpenedFiles() / releaseOpenedFile(). Страница сама забирает файлы, а не получает их принудительно: файл, открытый при холодном запуске, ждёт, пока его не запросят, поэтому SPA, монтирующееся поздно, всё равно его получит. Метаданные каждого файла содержат `kind` (image | video | audio | document | data) и `url` — https://appassets.androidplatform.net/appmint-shared/<token>/<name>, отдаваемый из кэшированной копии с поддержкой диапазонов байт, поэтому видео передаётся в <video src> и перематывается. Файлы с данными и документы также забираются срезами MAX_IO_CHUNK в File; медиа и всё больше 64 МБ остаются только по url (getFile() загружает по требованию), поэтому клип в 300 МБ никогда не лежит в массиве JS.

Описано своей группой, Файлы, которыми поделились с приложением или которые открыли в нём, а не отдельно.

Пример

Внутренний механизм для AppMint.getOpenedFile(): помечает файл, которым поделились или который открыли, как собранный, чтобы он больше не предлагался как «новый», но его кэшированная копия сохранялась. Страницам следует использовать публичный API; вызывать это напрямую незачем.

Возвращает: ничего. Требуется: при сборке включите Receive Shares From Other Apps.

Публичный API (используйте его) — каждый файл собирается один раз и передаётся вашей странице:

if (window.AppMint && typeof AppMint.getOpenedFiles === 'function') {
  AppMint.getOpenedFiles().then(function (files) {
    files.forEach(function (f) { console.log(f.name, f.kind); });
  });
}
window.addEventListener('appmint:fileopen', function (e) { console.log('new file', e.detail.name); });

Файл, собранный при запуске, есть и в списке, и в событии, поэтому ведите свою обработку по token, чтобы обработать каждый файл один раз:

const handled = new Set();
function handleOnce(f) {
  if (handled.has(f.token)) return;
  handled.add(f.token);
  addToInbox(f);
}
window.addEventListener('appmint:fileopen', function (e) { handleOnce(e.detail); });
if (window.AppMint && typeof AppMint.getOpenedFiles === 'function') {
  AppMint.getOpenedFiles().then(function (files) { files.forEach(handleOnce); });
}

Файл, который не удалось прочитать, всё равно собирается; о нём сообщается один раз через appmint:fileopenerror вместо appmint:fileopen:

window.addEventListener('appmint:fileopenerror', function (e) {
  const d = e.detail;             // {name, error}
  showBanner('Could not open "' + d.name + '". Try sharing it again.');
  console.warn('opened file failed:', d.error);
});

Примечания: Если вызвать это самостоятельно, файл исчезнет из AppMint.getOpenedFile() и события appmint:fileopen, но не достанется никому. Чтобы освободить кэшированную копию, используйте AppMint.releaseOpenedFile(f).

openedFileMaxChunk мост#

window.WebToApk.openedFileMaxChunk(): Int

Внутренний механизм для AppMint.getOpenedFile() / getOpenedFiles() / releaseOpenedFile(). Страница сама забирает файлы, а не получает их принудительно: файл, открытый при холодном запуске, ждёт, пока его не запросят, поэтому SPA, монтирующееся поздно, всё равно его получит. Метаданные каждого файла содержат `kind` (image | video | audio | document | data) и `url` — https://appassets.androidplatform.net/appmint-shared/<token>/<name>, отдаваемый из кэшированной копии с поддержкой диапазонов байт, поэтому видео передаётся в <video src> и перематывается. Файлы с данными и документы также забираются срезами MAX_IO_CHUNK в File; медиа и всё больше 64 МБ остаются только по url (getFile() загружает по требованию), поэтому клип в 300 МБ никогда не лежит в массиве JS.

Описано своей группой, Файлы, которыми поделились с приложением или которые открыли в нём, а не отдельно.

Пример

Внутренний механизм для AppMint.getOpenedFile(): наибольший срез, который openedFileChunk возвращает за один вызов (4 МБ). Страницам следует использовать публичный API, который уже читает срезами такого размера.

Возвращает: (синхронно) число байт (4194304). Требуется: при сборке включите Receive Shares From Other Apps.

Публичный API (используйте его):

if (window.AppMint && typeof AppMint.getOpenedFile === 'function') {
  AppMint.getOpenedFile().then(function (f) {
    if (f && f.file) console.log('pulled in 4 MB slices:', f.file.size, 'bytes');
  });
}

Прямой вызов, если нужно узнать только число:

if (window.WebToApk && typeof WebToApk.openedFileMaxChunk === 'function') {
  console.log('slice size', WebToApk.openedFileMaxChunk());
}

Оценка того, сколько вызовов bridge сделает помощник для каждого ожидающего файла, при профилировании медленного импорта:

if (window.WebToApk && typeof WebToApk.openedFileMaxChunk === 'function') {
  const slice = WebToApk.openedFileMaxChunk();
  JSON.parse(WebToApk.openedFilesPending() || '[]').forEach(function (m) {
    console.log(m.name, (m.size / 1048576).toFixed(1) + ' MB =', Math.ceil(m.size / slice), 'slice(s)');
  });
}

Примечания: Тот же предел, что у getMaxIoChunkSize() в bridge для доступа к файлам.

openedFileRelease мост#

window.WebToApk.openedFileRelease(token: String)

Внутренний механизм для AppMint.getOpenedFile() / getOpenedFiles() / releaseOpenedFile(). Страница сама забирает файлы, а не получает их принудительно: файл, открытый при холодном запуске, ждёт, пока его не запросят, поэтому SPA, монтирующееся поздно, всё равно его получит. Метаданные каждого файла содержат `kind` (image | video | audio | document | data) и `url` — https://appassets.androidplatform.net/appmint-shared/<token>/<name>, отдаваемый из кэшированной копии с поддержкой диапазонов байт, поэтому видео передаётся в <video src> и перематывается. Файлы с данными и документы также забираются срезами MAX_IO_CHUNK в File; медиа и всё больше 64 МБ остаются только по url (getFile() загружает по требованию), поэтому клип в 300 МБ никогда не лежит в массиве JS.

Описано своей группой, Файлы, которыми поделились с приложением или которые открыли в нём, а не отдельно.

Пример

Внутренний механизм для AppMint.releaseOpenedFile(f): удаляет кэшированную копию файла, которым поделились или который открыли, по токену. Страницам следует использовать публичный API.

Возвращает: ничего. Требуется: при сборке включите Receive Shares From Other Apps.

Публичный API (используйте его):

async function useAndRelease() {
  if (!window.AppMint || typeof AppMint.getOpenedFile !== 'function') return;
  const f = await AppMint.getOpenedFile();
  if (!f) return;
  const file = await f.getFile();       // keep your own copy first
  await store(file);
  AppMint.releaseOpenedFile(f);         // calls WebToApk.openedFileRelease(f.token)
}

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

async function offerImport() {
  if (!window.AppMint || typeof AppMint.getOpenedFile !== 'function') return;
  const f = await AppMint.getOpenedFile();
  if (!f) return;
  const box = showImportPrompt(f.name, f.size);   // your own "Import / Discard" UI
  box.querySelector('.discard').addEventListener('click', function () {
    AppMint.releaseOpenedFile(f);
    box.remove();
  });
  box.querySelector('.import').addEventListener('click', async function () {
    await store(await f.getFile());
    AppMint.releaseOpenedFile(f);
    box.remove();
  });
}

Файл, который приложение не может использовать, освобождается сразу после отклонения:

window.addEventListener('appmint:fileopen', function (e) {
  const f = e.detail;
  if (!/\.(gpx|tcx)$/i.test(f.name)) {
    alert(f.name + ' is not a GPX or TCX activity file.');
    AppMint.releaseOpenedFile(f);
    return;
  }
  importActivity(f.text);                // gpx/tcx arrive as text
});

Примечания: После освобождения f.url возвращает ошибку, а openedFileChunk возвращает "" для этого токена. Уже имеющийся у вас объект File не затрагивается.

openedFilesPending мост#

window.WebToApk.openedFilesPending(): String

Чужая страница ничего не получает: файлы остаются ждать собственную страницу приложения.

Пример

Внутренний механизм для AppMint.getOpenedFile() / AppMint.getOpenedFiles(): перечисляет файлы, которыми поделились или которые открыли и которые страница ещё не собрала (только метаданные, без байтов). Страницам следует использовать публичный API.

Возвращает: (синхронно) строку JSON: массив {token, name, mimeType, size, isText, kind, url}. [], если ничего не ждёт. Требуется: при сборке включите Receive Shares From Other Apps (без этого список всегда пуст, а помощники AppMint не устанавливаются).

Публичный API (используйте его):

if (window.AppMint && typeof AppMint.getOpenedFile === 'function') {
  AppMint.getOpenedFile().then(function (f) {
    if (f) console.log('opened with the app:', f.name, f.kind);
  });
}
window.addEventListener('appmint:fileopen', function (e) { console.log('arrived:', e.detail.name); });

Только для отладки — посмотрите, что ждёт, не собирая это:

if (window.WebToApk && typeof WebToApk.openedFilesPending === 'function') {
  const waiting = JSON.parse(WebToApk.openedFilesPending() || '[]');
  console.log(waiting.length + ' file(s) waiting', waiting.map(function (m) { return m.name; }));
}

Отладка файла, который так и не дошёл до страницы: посмотрите, что ещё ждёт и придёт ли он как f.file или только как f.url:

if (window.WebToApk && typeof WebToApk.openedFilesPending === 'function') {
  const STREAM_LIMIT = 64 * 1024 * 1024;   // the helper's url-only threshold
  JSON.parse(WebToApk.openedFilesPending() || '[]').forEach(function (m) {
    const urlOnly = m.kind === 'image' || m.kind === 'video' || m.kind === 'audio' || m.size > STREAM_LIMIT;
    console.log(m.name, m.mimeType, m.size + ' bytes',
      urlOnly ? 'will come as url only' : 'will be pulled into f.file',
      m.isText ? '(text)' : '');
  });
}

Примечания: Помощник собирает их автоматически после load и при поступлении каждого нового файла, поэтому страница редко видит здесь что-либо. После сбора файл уходит из этого списка, но остаётся доступным для чтения до освобождения.