# Files: opened with this app - JavaScript bridge API

> Collecting what another app handed over through "Open with", in chunks the page pulls.

- **Applies to:** Appwright
- **Source:** extracted from the app runtime and its per-method example files; this page is generated from them.
- **HTML:** https://freewebsitetoapk.com/docs/api/files-opened

### `AppMint.getOpenedFile`

```js
AppMint.getOpenedFile()
```

**Example**

Gives your page the file that was shared to the app, or opened with it ("Open with…"), however late your page starts.

**Returns:** `Promise<object | null>` - `null` when the app was opened normally. The object has `token, name, mimeType, size, kind, url, file, getFile()`, plus `text` (text files up to 32 MB) or `base64` (other data files up to 8 MB) when cheap. `kind` is `'image' | 'video' | 'audio' | 'document' | 'data'`. **Needs:** turn on **Receive Shares From Other Apps** when you build and tick the kinds of files your app accepts.

```js
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();
```

Or listen - the event (and the optional `window.onAppMintFileOpen` hook) fires once per new file, with the same object as `detail`:

```js
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}
});
```

In a React app, a small hook picks up the file however late the component mounts, and keeps following new ones:

```js
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;
}
```

**Notes:** Images, videos, audio and any file over 64 MB come only as `url` (`file` is `null`); call `f.getFile()` when you really need the bytes. Data files and documents also come as `f.file`. The file is kept for your page until you ask, so an SPA that mounts late still gets it; the latest one is returned each time. When the app is already open and another file arrives, the event fires again. Call `AppMint.releaseOpenedFile(f)` when done to free the cached copy.

### `AppMint.getOpenedFiles`

```js
AppMint.getOpenedFiles()
```

**Example**

Gives your page every file shared to, or opened with, the app during this session - useful when the user shares several photos at once.

**Returns:** `Promise<Array>` of the same objects `AppMint.getOpenedFile()` returns (`token, name, mimeType, size, kind, url, file, getFile()`, and `text` / `base64` when cheap). An empty array when nothing was received. **Needs:** turn on **Receive Shares From Other Apps** when you build and tick the kinds of files your app accepts.

```js
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();
```

New files that arrive while the page is open also fire `appmint:fileopen`, one event per file:

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

Uploading everything the user shared in one request, as real files:

```js
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 });
}
```

**Notes:** The list is a copy; it grows as more files arrive. Files you have released with `AppMint.releaseOpenedFile(f)` stay in the list, but their `url` no longer works.

### `AppMint.releaseOpenedFile`

```js
AppMint.releaseOpenedFile(file)
```

**Example**

Deletes the app's cached copy of a file that was shared to or opened with the app, once your page is done with it.

**Returns:** nothing. **Needs:** turn on **Receive Shares From Other Apps** when you build.

```js
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
}
```

A shared video plays from `f.url`, so release it only when playback is over:

```js
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 });
}
```

After importing a batch of shared photos, free every cached copy at once:

```js
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);
  }
}
```

**Notes:** Pass the object you got (it needs its `token`). A `File` you already pulled stays valid; the `url` stops working, and `getFile()` rejects unless it had already fetched the File. Do not release a video you are still playing from `f.url`. The app also clears these copies when its screen is closed, so releasing early is about disk space during a long session.

### `openedFileChunk`

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

Internal to AppMint.getOpenedFile() / getOpenedFiles() / releaseOpenedFile(). The page pulls, rather than being pushed at: a file opened on cold start is parked until it is asked for, so an SPA that mounts late still gets it. Each file's metadata carries `kind` (image | video | audio | document | data) and `url` - https://appassets.androidplatform.net/appmint-shared/<token>/<name>, served from the cache copy with byte ranges, so a shared video streams into <video src> and seeks. Data files and documents are also pulled in MAX_IO_CHUNK slices into a File; media and anything over 64 MB stay url-only (getFile() fetches on demand), so a 300 MB clip never sits in a JS array.

_Described by its group, Files shared to, or opened with, the app, rather than on its own._

**Example**

Internal plumbing behind `AppMint.getOpenedFile()`: returns one slice of a shared or opened file's bytes, which the helper joins into a `File`. Pages should use the public API - `f.file`, `f.getFile()` or `f.url`.

**Returns:** (sync) a base64 **string** of up to `len` bytes starting at `offset` (the offset is passed as a string, so files over 2 GB still work); `""` at the end of the file, for an unknown or released token, or on a read error. **Needs:** turn on **Receive Shares From Other Apps** when you build.

The public API (use this):

```js
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'); });
  });
}
```

What the helper does with it, shown only to explain the arguments:

```js
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);
  }
}
```

Reading a very large shared file without slices - stream `f.url` and show progress (here counting CSV rows):

```js
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);
}
```

**Notes:** `len` is clamped to 1 byte … 4 MB. For media files, streaming `f.url` is far cheaper than pulling slices.

### `openedFileCollected`

```js
window.WebToApk.openedFileCollected(token: String)
```

Internal to AppMint.getOpenedFile() / getOpenedFiles() / releaseOpenedFile(). The page pulls, rather than being pushed at: a file opened on cold start is parked until it is asked for, so an SPA that mounts late still gets it. Each file's metadata carries `kind` (image | video | audio | document | data) and `url` - https://appassets.androidplatform.net/appmint-shared/<token>/<name>, served from the cache copy with byte ranges, so a shared video streams into <video src> and seeks. Data files and documents are also pulled in MAX_IO_CHUNK slices into a File; media and anything over 64 MB stay url-only (getFile() fetches on demand), so a 300 MB clip never sits in a JS array.

_Described by its group, Files shared to, or opened with, the app, rather than on its own._

**Example**

Internal plumbing behind `AppMint.getOpenedFile()`: marks a shared or opened file as collected, so it is no longer offered as "new" but its cached copy is kept. Pages should use the public API; there is no reason to call this directly.

**Returns:** nothing. **Needs:** turn on **Receive Shares From Other Apps** when you build.

The public API (use this) - each file is collected once and handed to your page:

```js
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); });
```

A file collected at startup is both in the list and announced by the event, so key your own handling by `token` to process each file once:

```js
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); });
}
```

A file that could not be read is still collected; it is reported once through `appmint:fileopenerror` instead of `appmint:fileopen`:

```js
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);
});
```

**Notes:** Calling it yourself would hide a file from `AppMint.getOpenedFile()` and the `appmint:fileopen` event without giving it to anyone. To free the cached copy, use `AppMint.releaseOpenedFile(f)`.

### `openedFileMaxChunk`

```js
window.WebToApk.openedFileMaxChunk(): Int
```

Internal to AppMint.getOpenedFile() / getOpenedFiles() / releaseOpenedFile(). The page pulls, rather than being pushed at: a file opened on cold start is parked until it is asked for, so an SPA that mounts late still gets it. Each file's metadata carries `kind` (image | video | audio | document | data) and `url` - https://appassets.androidplatform.net/appmint-shared/<token>/<name>, served from the cache copy with byte ranges, so a shared video streams into <video src> and seeks. Data files and documents are also pulled in MAX_IO_CHUNK slices into a File; media and anything over 64 MB stay url-only (getFile() fetches on demand), so a 300 MB clip never sits in a JS array.

_Described by its group, Files shared to, or opened with, the app, rather than on its own._

**Example**

Internal plumbing behind `AppMint.getOpenedFile()`: the largest slice `openedFileChunk` returns in one call (4 MB). Pages should use the public API, which already reads in slices of this size.

**Returns:** (sync) a number of bytes (4194304). **Needs:** turn on **Receive Shares From Other Apps** when you build.

The public API (use this):

```js
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');
  });
}
```

The raw call, if you only want to know the number:

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

Estimating how many bridge calls the helper will make for each waiting file, when profiling a slow import:

```js
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)');
  });
}
```

**Notes:** Same cap as `getMaxIoChunkSize()` for the file-access bridge.

### `openedFileRelease`

```js
window.WebToApk.openedFileRelease(token: String)
```

Internal to AppMint.getOpenedFile() / getOpenedFiles() / releaseOpenedFile(). The page pulls, rather than being pushed at: a file opened on cold start is parked until it is asked for, so an SPA that mounts late still gets it. Each file's metadata carries `kind` (image | video | audio | document | data) and `url` - https://appassets.androidplatform.net/appmint-shared/<token>/<name>, served from the cache copy with byte ranges, so a shared video streams into <video src> and seeks. Data files and documents are also pulled in MAX_IO_CHUNK slices into a File; media and anything over 64 MB stay url-only (getFile() fetches on demand), so a 300 MB clip never sits in a JS array.

_Described by its group, Files shared to, or opened with, the app, rather than on its own._

**Example**

Internal plumbing behind `AppMint.releaseOpenedFile(f)`: deletes the cached copy of a shared or opened file, by token. Pages should use the public API.

**Returns:** nothing. **Needs:** turn on **Receive Shares From Other Apps** when you build.

The public API (use this):

```js
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)
}
```

The user dismisses a shared file without importing it - free the cached copy straight away:

```js
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();
  });
}
```

A file the app cannot use is released as soon as it is refused:

```js
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
});
```

**Notes:** After release, `f.url` returns an error and `openedFileChunk` returns `""` for that token. A `File` object you already have is not affected.

### `openedFilesPending`

```js
window.WebToApk.openedFilesPending(): String
```

A foreign page gets nothing: the files stay parked for the app's own page.

**Example**

Internal plumbing behind `AppMint.getOpenedFile()` / `AppMint.getOpenedFiles()`: lists the shared or opened files the page has not collected yet (metadata only, no bytes). Pages should use the public API.

**Returns:** (sync) a JSON **string**: an array of `{token, name, mimeType, size, isText, kind, url}`. `[]` when nothing is waiting. **Needs:** turn on **Receive Shares From Other Apps** when you build (without it the list is always empty and the `AppMint` helpers are not installed).

The public API (use this):

```js
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); });
```

Only for debugging - peek at what is waiting without collecting it:

```js
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; }));
}
```

Debugging a file that never reached the page - see what is still waiting and whether it will arrive as `f.file` or only as `f.url`:

```js
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)' : '');
  });
}
```

**Notes:** The helper collects these automatically after `load` and whenever a new file arrives, so a page rarely sees anything here. Once collected, a file leaves this list but stays readable until released.

