मुफ़्त APK बनाएँ

JavaScript ब्रिज API

फ़ाइलें: एक बार में एक फ़ाइल

सिस्टम पिकर और खंडों में बाइनरी पढ़ना/लिखना, उन फ़ाइलों के लिए जो स्ट्रिंग में रखने के लिए बहुत बड़ी हैं।

AppMint.downloadFile हेल्पर#

AppMint.downloadFile(data, fileName, mimeType?)

उदाहरण

आपके पेज की बनाई फ़ाइल सेव करता है - base64 स्ट्रिंग, data: URL, blob: URL या Blob से - आपके चुने फ़ाइल नाम के साथ। Android की "Save as…" शीट उस नाम के साथ भरी हुई खुलती है।

लौटाता है: (सिंक) सेव Android को सौंपे जाने पर true (false सिर्फ़ ऐसे data: URL के लिए जिसे वह पार्स न कर सका)। अगर data Blob या स्ट्रिंग नहीं है तो TypeError फेंकता है। फ़ोल्डर फिर भी यूज़र चुनता है, और वह रद्द कर सकता है। ज़रूरी: कुछ नहीं - यह ऐप में हमेशा मौजूद रहता है।

Base64 स्ट्रिंग और MIME प्रकार:

function exportCsv(rows) {
  const csv = rows.map(function (r) { return r.join(','); }).join('\n');
  const b64 = btoa(unescape(encodeURIComponent(csv)));          // UTF-8 safe
  if (window.AppMint && typeof AppMint.downloadFile === 'function') {
    AppMint.downloadFile(b64, 'Ledger_Q3.csv', 'text/csv');
  } else {
    const a = document.createElement('a');                       // a normal browser
    a.href = 'data:text/csv;base64,' + b64;
    a.download = 'Ledger_Q3.csv';
    a.click();
  }
}

data: URL (MIME टाइप URL से आता है) या Blob:

const png = document.querySelector('canvas').toDataURL('image/png');
AppMint.downloadFile(png, 'Chart.png');

const blob = new Blob([JSON.stringify(state)], { type: 'application/json' });
AppMint.downloadFile(blob, 'Backup.json');                        // same as AppMint.saveBlob

आपका सर्वर JSON में जो base64 PDF लौटाता है; बिना एक्सटेंशन वाले नाम को MIME प्रकार से .pdf मिल जाता है:

async function downloadInvoice(id) {
  const res = await fetch('/api/invoices/' + id + '?format=base64');
  const body = await res.json();                                  // { base64: 'JVBERi0x…' }
  if (!window.AppMint || typeof AppMint.downloadFile !== 'function') {
    location.href = 'data:application/pdf;base64,' + body.base64;
    return;
  }
  AppMint.downloadFile(body.base64, 'Invoice_' + id, 'application/pdf');   // saved as Invoice_<id>.pdf
}

स्क्रीन पर प्रीव्यू के लिए आपका पहले से बनाया blob: URL:

// earlier: preview.src = URL.createObjectURL(editedImageBlob);
document.getElementById('save-photo').addEventListener('click', function () {
  const preview = document.getElementById('preview');
  if (window.AppMint && typeof AppMint.downloadFile === 'function') {
    AppMint.downloadFile(preview.src, 'Edited_Photo.jpg');
  }
});

ध्यान दें: fileName का सिर्फ़ आख़िरी हिस्सा इस्तेमाल होता है (फ़ोल्डर नहीं)। बिना एक्सटेंशन वाले नाम को MIME प्रकार से एक्सटेंशन मिल जाता है। तीसरा आर्गुमेंट सिर्फ़ सादी base64 स्ट्रिंग के लिए इस्तेमाल होता है। अगर फ़ोन में "Save as…" स्क्रीन बिल्कुल नहीं है, तो फ़ाइल सीधे Downloads में लिखी जाती है। आपका सामान्य <a download> कोड भी चलता है - देखें downloads.

AppMint.saveBlob हेल्पर#

AppMint.saveBlob(blob, fileName)

उदाहरण

आपके चुने नाम से Blob (या File) सेव करता है। Android की "Save as…" शीट उस नाम के साथ भरी हुई खुलती है।

लौटाता है: (सिंक) सेव Android को सौंपे जाने पर true। पहला आर्गुमेंट Blob न होने पर TypeError फेंकता है। ज़रूरी: कुछ नहीं - यह ऐप में हमेशा मौजूद रहता है।

async function saveReport() {
  const blob = await makePdfBlob();                    // e.g. jsPDF: doc.output('blob')
  if (window.AppMint && typeof AppMint.saveBlob === 'function') {
    AppMint.saveBlob(blob, 'Site_Report_2026-08.pdf');
    return;
  }
  const url = URL.createObjectURL(blob);               // a normal browser
  const a = document.createElement('a');
  a.href = url;
  a.download = 'Site_Report_2026-08.pdf';
  a.click();
  setTimeout(function () { URL.revokeObjectURL(url); }, 10000);
}

File अपना नाम खुद साथ लाता है, इसलिए नाम छोड़ा जा सकता है:

const file = new File(['name,score\n'], 'Scores.csv', { type: 'text/csv' });
AppMint.saveBlob(file);            // saved as Scores.csv

आपके सर्वर की फ़ाइल जिसे साइन-इन हेडर चाहिए, आपके चुने नाम से सेव:

async function saveStatement(month, token) {
  const res = await fetch('/api/statements/' + month, {
    headers: { Authorization: 'Bearer ' + token }
  });
  if (!res.ok) { alert('Could not fetch the statement (' + res.status + ')'); return; }
  const blob = await res.blob();
  if (window.AppMint && typeof AppMint.saveBlob === 'function') {
    AppMint.saveBlob(blob, 'Statement_' + month + '.pdf');
  }
}

कई फ़ाइलें एक ZIP में बंडल करके (यहाँ JSZip से) एक ही बार में सेव करना:

async function exportAll(notes) {
  const zip = new JSZip();
  notes.forEach(function (n) { zip.file(n.title + '.txt', n.body); });
  zip.file('index.json', JSON.stringify(notes.map(function (n) { return n.title; })));
  const blob = await zip.generateAsync({ type: 'blob' });
  if (window.AppMint && typeof AppMint.saveBlob === 'function') {
    AppMint.saveBlob(blob, 'Notes_' + new Date().toISOString().slice(0, 10) + '.zip');
  }
}

ध्यान दें: Blob को मेमोरी में पढ़ा जाता है और एक ही टुकड़े में Android को भेजा जाता है, इसलिए बहुत बड़े Blob (सैकड़ों MB) में मेमोरी कम पड़ सकती है। फ़ोल्डर उपयोगकर्ता चुनता है और रद्द भी कर सकता है; पेज को यह नहीं बताया जाता कि क्या हुआ।

AppMint.setDownloadName हेल्पर#

AppMint.setDownloadName(url, fileName)

उदाहरण

किसी blob: या data: URL को वह फ़ाइल नाम देता है जिससे वह सेव होनी चाहिए, इससे पहले कि आपका कोड (या कोई लाइब्रेरी) डाउनलोड शुरू करे।

लौटाता है: कुछ नहीं। ज़रूरी: कुछ नहीं - यह ऐप में हमेशा मौजूद रहता है।

तब काम आता है जब कोई लाइब्रेरी आपके पहले बनाए URL को डाउनलोड करती है और आप उसे नाम नहीं दे सकते:

const blob = new Blob([csvText], { type: 'text/csv' });
const url = URL.createObjectURL(blob);
if (window.AppMint && typeof AppMint.setDownloadName === 'function') {
  AppMint.setDownloadName(url, 'Invoice_0042.csv');
}
location.href = url;           // saved as Invoice_0042.csv, not "download.csv"

window.open से खोला गया आपका PDF फ़ोन के PDF व्यूअर में जाता है; पहले उसे नाम दें ताकि वह "download.pdf" न रहे:

function previewPdf(pdfBlob, invoiceNo) {
  const url = URL.createObjectURL(pdfBlob);
  if (window.AppMint && typeof AppMint.setDownloadName === 'function') {
    AppMint.setDownloadName(url, 'Invoice_' + invoiceNo + '.pdf');
  }
  window.open(url, '_blank');
}

data: URL भी इसी तरह काम करता है, जैसे कोई चार्ट इमेज जिसे लाइब्रेरी बिना download एट्रिब्यूट के लिंक करती है:

const dataUrl = chart.toBase64Image();         // e.g. Chart.js: 'data:image/png;base64,…'
if (window.AppMint && typeof AppMint.setDownloadName === 'function') {
  AppMint.setDownloadName(dataUrl, 'Sales_by_month.png');
}
const link = document.getElementById('chart-link');
link.href = dataUrl;                           // a tap on this link saves Sales_by_month.png
link.textContent = 'Save chart';

ध्यान दें: URL.revokeObjectURL(url) के बाद भी नाम याद रखा जाता है, क्योंकि कुछ लाइब्रेरी सेव खत्म होने से पहले ही revoke कर देती हैं। आपको इसकी कम ही ज़रूरत पड़ेगी: <a download="name">, URL.createObjectURL(file) (File का एक नाम होता है) और FileSaver का saveAs(blob, name) सब अपने आप पकड़ लिए जाते हैं। नाम 'download' को "कोई नाम नहीं" मानकर अनदेखा किया जाता है।

WebToApkFS.maxChunkSize हेल्पर#

WebToApkFS.maxChunkSize()

उदाहरण

बताता है कि एक readBytes या writeBytes कॉल ज़्यादा से ज़्यादा कितने बाइट (4 MB) ले जा सकती है, ताकि आप अपने चंक लूप का आकार तय कर सकें।

लौटाता है: (तुरंत) बाइट की संख्या, या 0 जब ब्रिज वाला तरीका मौजूद न हो। ज़रूरत: बिल्ड करते समय चरण 4 (Access) में Native Folder Access (SAF) टिक करें (यही इसे जोड़ता है WebToApkFS).

function copyFile(fromUri, toPath) {
  if (!window.WebToApkFS) return { ok: false, error: 'not in the app' };
  const step = WebToApkFS.maxChunkSize() || 1048576;   // fall back to 1 MB pieces
  let offset = 0;
  for (;;) {
    const r = WebToApkFS.readBytes(fromUri, offset, step);
    if (!r.ok) return r;
    if (r.bytesRead > 0) {
      const w = WebToApkFS.writeBytes(toPath, r.base64, offset, offset === 0 ? 'truncate' : 'patch');
      if (!w.ok) return w;
    }
    offset += r.bytesRead;
    if (r.eof || r.bytesRead <= 0) return { ok: true, size: offset };
  }
}

पढ़ने की प्रगति दिखाएँ: चंक का आकार बताता है कि एक फ़ाइल में कितने चरण लगेंगे:

function readWithProgress(uri, onProgress) {
  const info = WebToApkFS.stat(uri);
  if (!info.ok) return info;
  const step = WebToApkFS.maxChunkSize() || 1048576;
  const steps = Math.max(1, Math.ceil(info.size / step));
  for (let i = 0; i < steps; i++) {
    const r = WebToApkFS.readBytes(uri, i * step, step);
    if (!r.ok) return r;
    onProgress(Math.round(((i + 1) / steps) * 100), r.base64);
  }
  return { ok: true, size: info.size };
}

बड़ी बाइट ऐरे को दिए गए फ़ोल्डर की फ़ाइल में जोड़ें, एक बार में एक अनुमत टुकड़ा:

function appendBytes(path, bytes) {                    // bytes: a Uint8Array
  const step = WebToApkFS.maxChunkSize();
  if (!step) return { ok: false, error: 'not in the app' };
  for (let at = 0; at < bytes.length; at += step) {
    const piece = bytes.subarray(at, at + step);
    let bin = '';
    for (let i = 0; i < piece.length; i += 0x8000) bin += String.fromCharCode.apply(null, piece.subarray(i, i + 0x8000));
    const w = WebToApkFS.writeBytes(path, btoa(bin), 0, 'append');   // 'append' ignores the offset
    if (!w.ok) return w;
  }
  return { ok: true };
}

ध्यान दें: छोटा टुकड़ा पेज को ज़्यादा रेस्पॉन्सिव रखता है (हर कॉल सिंक्रोनस है); सीमा सिर्फ़ यह बताती है कि एक कॉल कितना बड़ा हो सकता है।

WebToApkFS.pickFile हेल्पर#

WebToApkFS.pickFile(mimeFilter)

उदाहरण

Android का फ़ाइल पिकर खोलता है ताकि उपयोगकर्ता कोई एक फ़ाइल चुने; इसके बाद आपका पेज उस फ़ाइल के असली बाइट पढ़ और लिख सकता है, ऐप दोबारा शुरू होने के बाद भी।

लौटाता है: Promise<{ok, uri, name, size, mime, error}>। सफल होने पर ok का मान true होता है और uri एक content:// पता होता है, जिसे आप stat, readBytes, writeBytes को देते हैं। विफल होने पर ok का मान false होता है और error में 'cancelled', 'disabled', 'no-picker', 'busy' (पिछली कॉल के लिए पिकर पहले से खुला है, जो अपना जवाब खुद रखता है), 'foreign_origin' या 'unsupported' (कोई ऐप नहीं) में से एक आता है। ज़रूरत: बिल्ड करते समय चरण 4 (Access) में Native Folder Access (SAF) पर टिक करें।

async function openOneFile() {
  if (!window.WebToApkFS) { alert('Open this page in the app to pick a file.'); return; }
  const f = await WebToApkFS.pickFile('application/pdf');   // '' = any file, 'image/*' = pictures
  if (!f.ok) {
    if (f.error !== 'cancelled') alert('Cannot open a file: ' + f.error);
    return;
  }
  console.log(f.name, f.size + ' bytes', f.mime);
  localStorage.setItem('lastFile', f.uri);   // the grant survives a restart
}

सेव किए गए uri को बाद में दोबारा इस्तेमाल करें, यूज़र से दूसरी बार पूछे बिना:

const uri = localStorage.getItem('lastFile');
if (uri && window.WebToApkFS) {
  const info = WebToApkFS.stat(uri);
  if (info.ok) console.log('still here:', info.name);
  else console.log('file is gone or access was removed:', info.error);
}

फ़िल्टर सिर्फ़ ONE पैटर्न लेता है, इसलिए मोटे तौर पर चुनें और नाम खुद जाँचें:

async function importTable() {
  if (!window.WebToApkFS) return;
  const f = await WebToApkFS.pickFile('text/*');        // CSV and TSV files are both text/*
  if (!f.ok) {
    if (f.error === 'busy') return;                     // a picker is already open
    if (f.error !== 'cancelled') alert('Cannot open a file: ' + f.error);
    return;
  }
  if (!/\.(csv|tsv)$/i.test(f.name)) {
    WebToApkFS.releaseFile(f.uri);
    alert(f.name + ' is not a CSV or TSV file.');
    return;
  }
  const r = WebToApkFS.readAll(f.uri);
  if (r.ok) showTable(atob(r.base64), f.name.toLowerCase().endsWith('.tsv') ? '\t' : ',');
  WebToApkFS.releaseFile(f.uri);
}

ध्यान दें: फ़िल्टर एक MIME टाइप पैटर्न ('image/*', 'text/csv') है, सूची नहीं। पिकर पढ़ने और लिखने दोनों की पहुँच माँगता है और उसे बनाए रखता है। Android एक ऐप को इस तरह कितनी फ़ाइलें रखने देता है इसकी सीमा है, इसलिए किसी फ़ाइल का काम खत्म होने पर WebToApkFS.releaseFile(uri) कॉल करें। पूरे फ़ोल्डर के लिए इसके बजाय WebToApkFS.requestAccess() इस्तेमाल करें। WebToApkFS तब इंस्टॉल होता है जब Native Folder Access या Secure Keys टिक हो; इसके कॉल सिर्फ़ Native Folder Access के साथ काम करते हैं (WebToApkFS.isAvailable()).

WebToApkFS.readAll हेल्पर#

WebToApkFS.readAll(pathOrUri)

उदाहरण

पूरी फ़ाइल को एक base64 स्ट्रिंग में पढ़ता है। यह आपके लिए readBytes पर लूप चलाता है, एक बार में 4 MB।

लौटाता है: (sync) {ok:true, base64, size}, या पहले नाकाम चंक का {ok:false, error}। ज़रूरत: बिल्ड करते समय चरण 4 (Access) में Native Folder Access (SAF) पर टिक करें।

चुनी हुई इमेज को इसमें लोड करें <img>:

async function showPicture() {
  if (!window.WebToApkFS) return;
  const f = await WebToApkFS.pickFile('image/*');
  if (!f.ok) return;
  const r = WebToApkFS.readAll(f.uri);
  if (!r.ok) { alert('Read failed: ' + r.error); return; }
  document.getElementById('photo').src = 'data:' + f.mime + ';base64,' + r.base64;
}

UTF-8 टेक्स्ट फ़ाइल पढ़ें (किसी भी भाषा के लिए चलती है, सिर्फ़ अंग्रेज़ी के लिए नहीं):

function readUtf8(pathOrUri) {
  const r = WebToApkFS.readAll(pathOrUri);
  if (!r.ok) throw new Error(r.error);
  const bytes = Uint8Array.from(atob(r.base64), function (c) { return c.charCodeAt(0); });
  return new TextDecoder('utf-8').decode(bytes);
}

फ़ाइल का SHA-256 चेकसम दिखाएँ, जैसे डाउनलोड को प्रकाशित वाले से मिलाने के लिए:

async function checksum() {
  if (!window.WebToApkFS) return;
  const f = await WebToApkFS.pickFile('');
  if (!f.ok) return;
  const r = WebToApkFS.readAll(f.uri);
  if (!r.ok) { alert('Read failed: ' + r.error); return; }
  const bytes = Uint8Array.from(atob(r.base64), function (c) { return c.charCodeAt(0); });
  const hash = new Uint8Array(await crypto.subtle.digest('SHA-256', bytes));
  const hex = Array.from(hash, function (b) { return b.toString(16).padStart(2, '0'); }).join('');
  console.log(f.name, r.size + ' bytes', hex);
}

ध्यान दें: पूरी फ़ाइल मेमोरी में आ जाती है (पहले बाइट्स के रूप में, फिर base64 के रूप में), और कॉल चलने तक पेज रुका रहता है। इसे उन फ़ाइलों के लिए इस्तेमाल करें जो आसानी से मेमोरी में आ जाती हैं; बड़ी फ़ाइलों के लिए WebToApkFS.readBytes को लूप में चलाएँ और हर हिस्से को संभालें। दी गई फ़ोल्डर के अंदर सादे टेक्स्ट के लिए WebToApkFS.readText(path) ज़्यादा आसान है।

WebToApkFS.readBytes हेल्पर#

WebToApkFS.readBytes(pathOrUri, offset, length)

उदाहरण

किसी फ़ाइल के असली बाइट, एक तय बाइट पोज़िशन से शुरू करके, base64 में पढ़ता है। इमेज, PDF और ZIP के लिए सुरक्षित है - टेक्स्ट डिकोडिंग से कुछ नहीं बदलता।

लौटाता है: (सिंक) {ok:true, base64, bytesRead, offset, size, eof} या {ok:false, error} ('not-found', 'disabled', 'cannot-open', 'offset past end of file (N bytes)', 'foreign_origin', 'unsupported')। ज़रूरी: बिल्ड करते समय चरण 4 (Access) में Native Folder Access (SAF) टिक करें।

पहले 64 KB पढ़ें, जैसे फ़ाइल का हेडर जाँचने के लिए:

async function checkPng() {
  if (!window.WebToApkFS) return;
  const f = await WebToApkFS.pickFile('image/*');
  if (!f.ok) return;
  const r = WebToApkFS.readBytes(f.uri, 0, 65536);
  if (!r.ok) { alert('Read failed: ' + r.error); return; }
  const head = atob(r.base64).slice(1, 4);
  console.log(head === 'PNG' ? 'a PNG' : 'not a PNG', '- file is', r.size, 'bytes');
}

बड़ी फ़ाइल टुकड़ों में पढ़ें, ताकि उसे एक बार में पूरा मेमोरी में न रखना पड़े:

function eachChunk(uri, onChunk) {
  const step = WebToApkFS.maxChunkSize() || 1048576;
  let offset = 0;
  for (;;) {
    const r = WebToApkFS.readBytes(uri, offset, step);
    if (!r.ok) return r;                 // stop on the first error
    onChunk(r.base64, offset);
    offset += r.bytesRead;
    if (r.eof || r.bytesRead <= 0) return { ok: true, size: offset };
  }
}

फ़ाइल का सिर्फ़ अंत पढ़ें, जैसे MP3 के अंत का 128-बाइट टैग:

function readMp3Title(uri) {
  const info = WebToApkFS.stat(uri);
  if (!info.ok || info.size < 128) return null;
  const r = WebToApkFS.readBytes(uri, info.size - 128, 128);
  if (!r.ok) return null;
  const tail = atob(r.base64);
  if (tail.slice(0, 3) !== 'TAG') return null;          // no old-style tag in this file
  return tail.slice(3, 33).replace(/\0+$/, '').trim();  // bytes 3-32 hold the title
}

नोट: एक कॉल में ज़्यादा से ज़्यादा WebToApkFS.maxChunkSize() बाइट (4 MB) पढ़े जाते हैं; length 0 का मतलब है "जितना एक कॉल में हो सके"। bytesRead पर लूप चलाएँ और eof पर रुक जाएँ। कॉल सिंक्रोनस है, इसलिए UI थ्रेड पर चंक मध्यम आकार के रखें। छोटी फ़ाइलों के लिए WebToApkFS.readAll(uri) आपके लिए लूप चला देता है।

WebToApkFS.readSealed हेल्पर#

WebToApkFS.readSealed(pathOrUri, alias)

उदाहरण

WebToApkFS.writeSealed से लिखी गई फ़ाइल को पढ़ता है और उसी कीस्टोर कुंजी से डिक्रिप्ट करता है।

लौटाता है: (तुरंत) {ok:true, base64} (मूल बाइट), या {ok:false, error} - 'keystore-disabled', 'corrupt-sealed-file', कीस्टोर कोड (कुंजी हटने के बाद 'no-such-key', 'auth-required', 'key-invalidated') या फ़ाइल त्रुटि ('not-found' …)। ज़रूरत: बिल्ड करते समय चरण 4 (Access) में Native Folder Access (SAF) और Secure Keys (encryption) टिक करें।

function loadSecret() {
  if (!window.WebToApkFS || !window.AppMintKeystore || !AppMintKeystore.isAvailable()) return null;
  const out = WebToApkFS.readSealed('secret.bin', 'vault');
  if (out.ok) {
    const bytes = Uint8Array.from(atob(out.base64), function (c) { return c.charCodeAt(0); });
    return new TextDecoder().decode(bytes);
  }
  if (out.error === 'auth-required') {
    alert('Unlock your phone, then try again.');
  } else if (out.error === 'no-such-key') {
    alert('This note was deleted.');
  } else {
    alert('Could not open the note: ' + out.error);
  }
  return null;
}

ऐप सेटिंग्स को सील की हुई JSON फ़ाइल में रखें, पहली बार सेव से पहले डिफ़ॉल्ट के साथ:

function loadSettings() {
  const defaults = { theme: 'light', pinHint: '' };
  const out = WebToApkFS.readSealed('settings.sealed', 'settings');
  if (!out.ok) {
    if (out.error === 'not-found') return defaults;      // nothing saved yet
    throw new Error('Settings could not be opened: ' + out.error);
  }
  const bytes = Uint8Array.from(atob(out.base64), function (c) { return c.charCodeAt(0); });
  return Object.assign(defaults, JSON.parse(new TextDecoder().decode(bytes)));
}

सील की हुई फ़ाइल को नई कुंजी पर ले जाएँ, फिर पुरानी कुंजी नष्ट करें:

function rekey(path, oldAlias, newAlias) {
  const plain = WebToApkFS.readSealed(path, oldAlias);
  if (!plain.ok) return plain;
  if (!AppMintKeystore.hasKey(newAlias)) {
    const k = AppMintKeystore.generateKey(newAlias, {});
    if (!k.ok) return k;
  }
  const w = WebToApkFS.writeSealed(path, plain.base64, newAlias);
  if (!w.ok) return w;
  AppMintKeystore.deleteKey(oldAlias);                  // only after the new copy is written
  return { ok: true };
}

ध्यान दें: AppMintKeystore.deleteKey(alias) चलने के बाद यह हमेशा के लिए फ़ेल हो जाता है - यही इसका मकसद है। पूरी फ़ाइल मेमोरी में पढ़ी और डिक्रिप्ट की जाती है, तुरंत; सील की गई फ़ाइलें छोटी रखें (नोट, कुंजियाँ, टोकन)।

WebToApkFS.releaseFile हेल्पर#

WebToApkFS.releaseFile(uri)

उदाहरण

वह लंबे समय की पहुँच वापस कर देता है जो आपके ऐप को तब मिली थी जब यूज़र ने इससे फ़ाइल चुनी थी WebToApkFS.pickFile.

लौटाता है: (सिंक) पहुँच छोड़ दी गई तो true, वरना false (कोई पहुँच नहीं, गलत URI, ऐप में नहीं)। ज़रूरी: बिल्ड करते समय चरण 4 (Access) में Native Folder Access (SAF) टिक करें।

async function importOnce() {
  if (!window.WebToApkFS) return;
  const f = await WebToApkFS.pickFile('text/csv');
  if (!f.ok) return;
  const r = WebToApkFS.readAll(f.uri);
  if (r.ok) importCsv(atob(r.base64));
  WebToApkFS.releaseFile(f.uri);       // done with it: do not keep the grant
}

जब यूज़र नई डिफ़ॉल्ट फ़ाइल चुने, तो पुरानी फ़ाइल की पहुँच वापस कर दें:

async function chooseDefaultFile() {
  if (!window.WebToApkFS) return;
  const f = await WebToApkFS.pickFile('');
  if (!f.ok) return;
  const old = localStorage.getItem('defaultFile');
  if (old && old !== f.uri) WebToApkFS.releaseFile(old);
  localStorage.setItem('defaultFile', f.uri);
}

"Forget recent files" बटन जो ऐप की याद रखी हर फ़ाइल को छोड़ देता है:

function forgetRecentFiles() {
  const recent = JSON.parse(localStorage.getItem('recentFiles') || '[]');
  let released = 0;
  for (const uri of recent) {
    if (WebToApkFS.releaseFile(uri)) released++;       // false: already gone or never granted
  }
  localStorage.removeItem('recentFiles');
  console.log('released', released, 'of', recent.length);
}

ध्यान दें: Android सीमित करता है कि एक ऐप कितनी चुनी हुई फ़ाइलों की पहुँच रख सकता है। जो ऐप कई फ़ाइलें चुनता है (इम्पोर्टर, व्यूअर) उसे हर फ़ाइल का काम खत्म होने पर उसे छोड़ देना चाहिए। छोड़ने के बाद उस URI पर stat / readBytes / writeBytes का जवाब not-found होता है। इससे फ़ाइल मिटती नहीं।

WebToApkFS.stat हेल्पर#

WebToApkFS.stat(pathOrUri)

उदाहरण

फ़ाइल की सामग्री पढ़े बिना उसका नाम, साइज़, टाइप और तारीख बताता है।

लौटाता है: (सिंक) {ok:true, name, size, mime, lastModified, uri, isDirectory, canWrite}, या {ok:false, error} जिसमें error = 'not-found', 'disabled', 'foreign_origin' या 'unsupported'। ज़रूरी: बिल्ड करते समय चरण 4 (Access) में Native Folder Access (SAF) टिक करें।

async function showInfo() {
  if (!window.WebToApkFS) return;
  const f = await WebToApkFS.pickFile('');
  if (!f.ok) return;
  const info = WebToApkFS.stat(f.uri);
  if (!info.ok) { alert('Cannot read file info: ' + info.error); return; }
  console.log(info.name, info.size + ' bytes', info.mime,
              new Date(info.lastModified).toLocaleString(),
              info.canWrite ? 'writable' : 'read only');
}

WebToApkFS.requestAccess() से उपयोगकर्ता के दिए फ़ोल्डर के अंदर का पाथ भी काम करता है:

const info = WebToApkFS.stat('photos/cover.jpg');
if (info.ok && !info.isDirectory) console.log('cover is', info.size, 'bytes');

याद रखी हुई फ़ाइल को दोबारा इम्पोर्ट तभी करें जब पिछली बार इम्पोर्ट करने के बाद वह बदली हो:

function importIfChanged(uri) {
  const info = WebToApkFS.stat(uri);
  if (!info.ok) return 'gone: ' + info.error;
  const seen = JSON.parse(localStorage.getItem('lastImport') || '{}');
  if (seen.size === info.size && seen.lastModified === info.lastModified) return 'unchanged';
  const r = WebToApkFS.readAll(uri);
  if (!r.ok) return 'read failed: ' + r.error;
  importData(atob(r.base64));
  localStorage.setItem('lastImport', JSON.stringify({ size: info.size, lastModified: info.lastModified }));
  return 'imported';
}

ध्यान दें: pathOrUri pickFile() से मिला content:// URI है या दिए गए फ़ोल्डर का सापेक्ष पाथ। जो URI आपके ऐप को कभी दिया ही नहीं गया वह not-found लौटाता है। lastModified 1970 से मिलीसेकंड में है (0 जब प्रोवाइडर न बताए)।

WebToApkFS.writeAll हेल्पर#

WebToApkFS.writeAll(pathOrUri, base64)

उदाहरण

फ़ाइल की पूरी सामग्री आपके बाइट से बदल देता है। यह डेटा को आपके लिए 4 MB के टुकड़ों में बाँटता है: पहला टुकड़ा फ़ाइल को truncate करता है, बाकी उसके बाद पैच होते हैं।

लौटाता है: (तुरंत) {ok:true, bytesWritten}, या पहले फ़ेल हुए टुकड़े का {ok:false, error}। ज़रूरत: बिल्ड करते समय चरण 4 (Access) में Native Folder Access (SAF) टिक करें।

UTF-8 टेक्स्ट (किसी भी भाषा का) उस फ़ाइल में वापस सेव करें जो उपयोगकर्ता ने चुनी थी:

function bytesToBase64(bytes) {
  let s = '';
  for (let i = 0; i < bytes.length; i += 0x8000) {
    s += String.fromCharCode.apply(null, bytes.subarray(i, i + 0x8000));
  }
  return btoa(s);
}

async function saveNote(text) {
  if (!window.WebToApkFS) return;
  const f = await WebToApkFS.pickFile('text/plain');
  if (!f.ok) return;
  const r = WebToApkFS.writeAll(f.uri, bytesToBase64(new TextEncoder().encode(text)));
  if (r.ok) console.log('saved', r.bytesWritten, 'bytes');
  else alert('Could not save: ' + r.error);
}

दिए गए फ़ोल्डर में कैनवस इमेज लिखें (फ़ाइल न हो तो बन जाती है):

const b64 = document.querySelector('canvas').toDataURL('image/png').split(',')[1];
const r = WebToApkFS.writeAll('drawings/sketch.png', b64);
if (!r.ok) alert('Could not save: ' + r.error);

दिए गए फ़ोल्डर में JSON बैकअप सेव करें, फ़ोल्डर न हो तो पहले माँगें, फिर डिस्क पर उसका आकार जाँचें:

async function backup(data) {
  if (!window.WebToApkFS || !WebToApkFS.isAvailable()) return;
  if (!WebToApkFS.hasAccess()) {
    const a = await WebToApkFS.requestAccess();
    if (!a.success) return;
  }
  const bytes = new TextEncoder().encode(JSON.stringify(data));
  let bin = '';
  for (let i = 0; i < bytes.length; i += 0x8000) bin += String.fromCharCode.apply(null, bytes.subarray(i, i + 0x8000));
  const r = WebToApkFS.writeAll('backup.json', btoa(bin));
  if (!r.ok) { alert('Backup failed: ' + r.error); return; }
  const info = WebToApkFS.stat('backup.json');
  console.log('backup is', info.ok ? info.size : '?', 'bytes');
}

ध्यान दें: लिखते समय पूरा इनपुट मेमोरी में रहता है, और कॉल पेज को रोक देती है। अगर बाद का कोई टुकड़ा फ़ेल हो, तो फ़ाइल में पहले के टुकड़े पहले से होते हैं - ok जाँचें और उपयोगकर्ता को बताएँ। फ़ाइल को गुप्त रखने के लिए इसके बजाय WebToApkFS.writeSealed इस्तेमाल करें।

WebToApkFS.writeBytes हेल्पर#

WebToApkFS.writeBytes(pathOrUri, base64, offset, mode)

उदाहरण

फ़ाइल में असली बाइट लिखता है और बताता है कि कितने लिखे गए। यह बीच के बाइट पैच कर सकता है, पूरी फ़ाइल बदल सकता है, या अंत में जोड़ सकता है।

लौटाता है: (sync) {ok, bytesWritten, offset, size} - डेटा डिस्क पर फ़्लश होने के बाद size फ़ाइल से वापस पढ़ा जाता है - या {ok:false, error}। ज़रूरत: बिल्ड करते समय चरण 4 (Access) में Native Folder Access (SAF) पर टिक करें।

पैच - पोज़िशन 100 पर 4 बाइट बदलें, फ़ाइल का बाकी हिस्सा वैसा ही रखें:

const r = WebToApkFS.writeBytes(fileUri, btoa('ABCD'), 100, 'patch');
if (r.ok) console.log('file is now', r.size, 'bytes');
else alert('Write failed: ' + r.error);

Truncate - पूरी फ़ाइल बदल दें (offset अनदेखा किया जाता है):

const csv = 'name,score\nAsha,12\n';
const r = WebToApkFS.writeBytes('exports/scores.csv', btoa(csv), 0, 'truncate');
if (!r.ok) alert('Could not save: ' + r.error);

Append - अंत में जोड़ें (offset अनदेखा किया जाता है):

const line = new Date().toISOString() + ' opened\n';
WebToApkFS.writeBytes('log.txt', btoa(line), 0, 'append');

नोट: mode या तो 'patch' (डिफ़ॉल्ट), 'truncate' या 'append' होता है। एक कॉल में ज़्यादा से ज़्यादा 4 MB (WebToApkFS.maxChunkSize()); बड़े डेटा के लिए WebToApkFS.writeAll इस्तेमाल करें। दिए गए फ़ोल्डर के अंदर का पाथ, अगर मौजूद न हो, तो बना दिया जाता है; चुना गया content:// URI वही होना चाहिए जो उपयोगकर्ता ने चुना हो। btoa सिर्फ़ Latin-1 टेक्स्ट लेता है - दूसरे टेक्स्ट को पहले बाइट में एनकोड करें (देखें WebToApkFS.writeAll)। त्रुटियाँ: disabled, foreign_origin (स्क्रीन पर मौजूद पेज ऐप का अपना नहीं है), bad mode: x, bad base64, chunk too large (max N bytes), not-found, cannot-open, short write.

WebToApkFS.writeSealed हेल्पर#

WebToApkFS.writeSealed(pathOrUri, base64, alias)

उदाहरण

AppMintKeystore की हार्डवेयर-समर्थित कुंजी से एन्क्रिप्ट की गई फ़ाइल लिखता है। बाद में वह कुंजी हटाने पर फ़ाइल सबके लिए पढ़ने लायक नहीं रहती - फ़ोन पर असली "secure delete" यही है।

लौटाता है: (सिंक) {ok:true, bytesWritten} (डिस्क पर एन्क्रिप्टेड साइज़), या {ok:false, error} - 'keystore-disabled', 'no-such-key', 'auth-required', 'key-invalidated' जैसा कोई keystore कोड, या फ़ाइल त्रुटि। ज़रूरी: बिल्ड करते समय चरण 4 (Access) में Native Folder Access (SAF) और Secure Keys (encryption) टिक करें।

function saveSecret(text) {
  if (!window.WebToApkFS || !window.AppMintKeystore || !AppMintKeystore.isAvailable()) {
    alert('Secure storage is not available in this build.');
    return false;
  }
  if (!AppMintKeystore.hasKey('vault')) {
    const k = AppMintKeystore.generateKey('vault', {});
    if (!k.ok) { alert('Could not create key: ' + k.error); return false; }
  }
  const bytes = new TextEncoder().encode(text);
  let bin = '';
  for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);
  const r = WebToApkFS.writeSealed('secret.bin', btoa(bin), 'vault');
  if (!r.ok) alert('Could not save: ' + r.error);
  return r.ok;
}

इसे सच में मिटाएँ - कुंजी नष्ट करें, फिर फ़ाइल हटाएँ:

AppMintKeystore.deleteKey('vault');   // secret.bin can never be decrypted again
WebToApkFS.delete('secret.bin');

उपयोगकर्ता की चुनी फ़ोटो को कच्चे बाइट के रूप में, उसके दिए फ़ोल्डर में सील करें:

async function sealPhoto(file) {                  // a File from <input type="file" accept="image/*">
  if (!window.WebToApkFS || !window.AppMintKeystore || !AppMintKeystore.isAvailable()) return false;
  if (!WebToApkFS.hasAccess()) {
    const g = await WebToApkFS.requestAccess();
    if (!g.success) return false;
  }
  if (!AppMintKeystore.hasKey('vault')) AppMintKeystore.generateKey('vault', {});
  const dataUrl = await new Promise(function (resolve, reject) {
    const fr = new FileReader();
    fr.onload = function () { resolve(fr.result); };
    fr.onerror = reject;
    fr.readAsDataURL(file);
  });
  const b64 = dataUrl.slice(dataUrl.indexOf(',') + 1);
  const r = WebToApkFS.writeSealed('photo-' + Date.now() + '.sealed', b64, 'vault');
  if (!r.ok) alert('Not saved: ' + r.error);
  return r.ok;
}

डायरी की कुंजी जो फ़ोन अनलॉक होने के सिर्फ़ 5 मिनट बाद तक काम करती है, हर त्रुटि समझाई हुई:

if (!AppMintKeystore.hasKey('diary')) {
  AppMintKeystore.generateKey('diary', { requireAuth: true, authValiditySeconds: 300 });
}

function saveDiary(entry) {
  const b64 = btoa(unescape(encodeURIComponent(JSON.stringify(entry))));   // UTF-8 safe
  const r = WebToApkFS.writeSealed('diary.bin', b64, 'diary');
  if (r.ok) return true;
  if (r.error === 'auth-required') showMessage('Unlock your phone again, then tap Save.');
  else if (r.error === 'key-invalidated') showMessage('Your screen lock changed, so this key no longer works.');
  else showMessage('Could not save: ' + r.error);
  return false;
}

ध्यान दें: मिटाने के लिए "शून्यों से ओवरराइट" न करें: फ़ोन का फ़्लैश स्टोरेज पुराने ब्लॉक रखता है। फ़ाइल फ़ॉर्मेट लंबाई-उपसर्ग वाले AES-GCM टुकड़ों की सूची है; इसे सिर्फ़ WebToApkFS.readSealed और उसी alias से वापस पढ़ें। {requireAuth:true} से बनी कुंजी तब तक auth-required देती है जब तक यूज़र ने हाल में अनलॉक न किया हो।

downloads वेब स्टैंडर्ड#

<a download> and Blob downloads

उदाहरण

आपका सामान्य वेब डाउनलोड कोड ऐप में वैसे ही काम करता है जैसे Chrome में। WebView blob: और data: डाउनलोड को अपने आप अनदेखा करता है; ऐप उन्हें पकड़ता है और फ़ाइल को फ़ोन के Downloads फ़ोल्डर में आपके फ़ाइल नाम से सेव करता है, फिर "Downloaded <name> · Open" और "Download complete" नोटिफ़िकेशन दिखाता है।

लौटाता है: पेज को कुछ नहीं। ज़रूरत: चालू करने के लिए कुछ नहीं। वेब-पते वाले डाउनलोड (https://…) भी Downloads फ़ोल्डर में जाते हैं (PDF या इमेज पर पहले "View or Download" पूछा जाता है), और ऐप के अंदर पैक की गई फ़ाइलें (<a href="manual.pdf" download>) भी; Android 9 और पुराने पर ऐप पहले स्टोरेज की अनुमति माँगता है। बड़ी फ़ाइलें ठीक हैं: फ़ाइल टुकड़ों में ऐप तक स्ट्रीम होती है।

<a download> Blob के साथ - सबसे आम तरीका:

<button id="save">Download report</button>
<script>
document.getElementById('save').addEventListener('click', function () {
  const blob = new Blob(['Name,Total\nAsha,120\n'], { type: 'text/csv' });
  const a = document.createElement('a');
  a.href = URL.createObjectURL(blob);
  a.download = 'Report_2026-08.csv';     // this is the name the user sees
  document.body.appendChild(a);
  a.click();                             // caught by the app, saved to Downloads
  a.remove();
});
</script>

data: URL, FileSaver.js और navigator.msSaveBlob भी काम करते हैं:

const a = document.createElement('a');
a.href = canvas.toDataURL('image/png');
a.download = 'Drawing.png';
a.click();

if (typeof saveAs === 'function') saveAs(blob, 'Backup.json');   // FileSaver.js
if (navigator.msSaveBlob) navigator.msSaveBlob(blob, 'Backup.json');

window.open किसी blob: या data: URL का मतलब "दिखाओ" है, "सेव करो" नहीं: PDF ऐप के PDF व्यूअर में खुलता है, बाकी प्रकार फ़ोन के उस प्रकार वाले ऐप में:

const pdfUrl = URL.createObjectURL(pdfBlob);
const w = window.open(pdfUrl);    // never null in the app, so no "popup blocked" message

ध्यान दें: नाम के बिना फ़ाइल download.<ext> के रूप में सेव होती है; उसी नाम की दूसरी फ़ाइल name (1).ext बन जाती है। File ऑब्जेक्ट अपना नाम साथ लाता है (URL.createObjectURL(file))। आपकी अपनी साइट पर <a href="/files/x" download="Name.pdf"> Name.pdf रखता है (Chrome दूसरी साइटों के लिंक पर यह एट्रिब्यूट अनदेखा करता है, और ऐप भी)। अगर फ़ोन पर कोई ऐप window.open फ़ाइल नहीं दिखा सकता, तो वह Downloads में सेव हो जाती है। यूज़र को फ़ोल्डर चुनने देने के लिए showSaveFilePicker() इस्तेमाल करें। <a> एलिमेंट के बिना सेव करने के लिए AppMint.saveBlob(blob, name) या इस्तेमाल करें AppMint.downloadFile(base64, name, mime).

input-file वेब स्टैंडर्ड#

<input type="file"> — camera, photos and files

उदाहरण

सामान्य फ़ाइल इनपुट ऐप में Chrome की तरह काम करता है: वह फ़ोटो पिकर, कैमरा या फ़ाइल पिकर खोलता है, और आपके पेज को सामान्य File ऑब्जेक्ट मिलते हैं।

लौटाता है: इनपुट का change इवेंट, input.files के साथ (यूज़र के रद्द करने पर खाली)। ज़रूरी: फ़ोटो और फ़ाइलों के लिए कुछ नहीं। capture (कैमरा) के लिए बिल्ड करते समय चरण 4 (Access) में Camera टिक करें; ऐप पहली बार कैमरा अनुमति माँगता है।

फ़ोटो - Android फ़ोटो पिकर खोलता है (कोई अनुमति प्रॉम्प्ट नहीं; पेज को सिर्फ़ वही मिलता है जो यूज़र ने चुना):

<input type="file" id="pics" accept="image/*" multiple>
<div id="preview"></div>
<script>
document.getElementById('pics').addEventListener('change', function (e) {
  const box = document.getElementById('preview');
  for (let i = 0; i < e.target.files.length; i++) {
    const img = document.createElement('img');
    img.src = URL.createObjectURL(e.target.files[i]);
    img.style.width = '100px';
    box.appendChild(img);
  }
});
</script>

कैमरा - capture पिकर को छोड़कर सीधे कैमरा खोल देता है:

<input type="file" id="snap" accept="image/*" capture="environment">
<input type="file" id="clip" accept="video/*" capture="environment">
<script>
document.getElementById('snap').addEventListener('change', function (e) {
  const photo = e.target.files[0];
  if (!photo) return;                        // user closed the camera
  const form = new FormData();
  form.append('photo', photo);
  fetch('/api/upload', { method: 'POST', body: form });
});
</script>

कोई भी फ़ाइल, या कई प्रकार (एक्सटेंशन भी चलते हैं):

<input type="file" accept=".pdf,.csv,application/json" multiple>
आप लिखेंक्या खुलता है
accept="image/*" / "video/*" / "image/*,video/*"फ़ोटो पिकर (फ़ोटो, वीडियो, या दोनों)
जोड़ें multipleयूज़र कई चुन सकता है
capture जोड़ें (Camera टिक होने पर)सीधे कैमरा - image/* के लिए फ़ोटो, वीडियो के लिए video/*
accept="image/*,.pdf" या नहीं acceptसिस्टम फ़ाइल पिकर

नोट: Camera पर टिक न हो तो capture को अनदेखा किया जाता है और पिकर खुल जाता है। जब Native Folder Access (SAF) पर टिक हो, तो ऐप इसके बजाय अपना "Select Upload Source" मेनू दिखाता है: Take Photo / Record Video (अगर Camera पर टिक है), Choose Files, और Upload Folder (फ़ोल्डर की हर फ़ाइल भेजता है)। Android 12 और उससे पुराने वर्ज़न पर Storage पर टिक होने से, फ़ोटो के अलावा कोई फ़ाइल चुनने पर पहले स्टोरेज की अनुमति माँगी जा सकती है। जिन फ़ोन में फ़ोटो पिकर नहीं है, उन्हें फ़ाइल पिकर मिलता है, जो वही फ़ोटो दिखाता है।

showOpenFilePicker वेब स्टैंडर्ड#

showOpenFilePicker / showSaveFilePicker / showDirectoryPicker — File System Access

उदाहरण

स्टैंडर्ड File System Access API ऐप के अंदर Android के अपने दस्तावेज़ पिकर के ऊपर काम करता है: फ़ाइल खोलें और उसी फ़ाइल में वापस सेव करें, Save-as शीट से फ़ाइल बनाएँ, या पूरा फ़ोल्डर पाएँ जिसमें फ़ाइलें पढ़ें, बनाएँ और मिटाएँ।

लौटाता है: showOpenFilePicker(options) → FileSystemFileHandle की ऐरे का Promise; showSaveFilePicker(options) → एक FileSystemFileHandle; showDirectoryPicker(options) → एक FileSystemDirectoryHandle। रद्द किया गया पिकर AbortError से रिजेक्ट होता है; उपयोगकर्ता का जेस्चर न हो तो SecurityError से; एक पिकर खुला होते दूसरा खोलने पर NotAllowedError से। ज़रूरत: चालू करने के लिए कुछ नहीं और कोई अनुमति नहीं - उपयोगकर्ता खुद चुनता है।

let handle = null;

document.getElementById('open').addEventListener('click', async () => {
  try {
    [handle] = await showOpenFilePicker({
      types: [{ description: 'Text', accept: { 'text/plain': ['.txt', '.md'] } }]
    });
    const file = await handle.getFile();
    editor.value = await file.text();
    title.textContent = file.name;
  } catch (e) {
    if (e.name !== 'AbortError') alert('Could not open: ' + e.message);
  }
});

document.getElementById('save').addEventListener('click', async () => {
  if (!handle) handle = await showSaveFilePicker({ suggestedName: 'note.txt' });
  const w = await handle.createWritable();   // writes go to a copy…
  await w.write(editor.value);
  await w.close();                           // …that replaces the file only here
});

document.getElementById('export').addEventListener('click', async () => {
  const dir = await showDirectoryPicker({ mode: 'readwrite' });
  const out = await dir.getFileHandle('export.csv', { create: true });
  const w = await out.createWritable();
  await w.write(csvText);
  await w.close();
  for await (const [name, entry] of dir.entries()) console.log(entry.kind, name);
});

एक साथ कई फ़ोटो चुनें, Pictures फ़ोल्डर से शुरू करके:

document.getElementById('add-photos').addEventListener('click', async () => {
  try {
    const handles = await showOpenFilePicker({
      multiple: true,
      startIn: 'pictures',
      types: [{ description: 'Images', accept: { 'image/*': ['.jpg', '.jpeg', '.png', '.webp'] } }]
    });
    for (const h of handles) {
      const img = document.createElement('img');
      img.src = URL.createObjectURL(await h.getFile());
      gallery.appendChild(img);
    }
  } catch (e) {
    if (e.name !== 'AbortError') alert('Could not open: ' + e.message);
  }
});

लॉग फ़ाइल के अंत में एक लाइन जोड़ें, जो पहले से लिखा है उसे दोबारा लिखे बिना:

async function appendLine(handle, line) {      // handle from showOpenFilePicker / showSaveFilePicker
  const size = (await handle.getFile()).size;
  const w = await handle.createWritable({ keepExistingData: true });
  await w.seek(size);
  await w.write(line + '\n');
  await w.close();
}

बैकअप फ़ोल्डर सिर्फ़ एक बार चुनें और हर अगले लॉन्च पर उसमें अपने आप सेव करें - हैंडल को IndexedDB में रखें, ठीक Chrome की तरह। ऐप फ़ोल्डर की अनुमति रीस्टार्ट के बाद भी रखता है, इसलिए queryPermission() का जवाब 'granted' होता है और बैकअप के लिए न टैप चाहिए न पिकर:

// Tiny IndexedDB helpers (idb-keyval's get/set work the same way).
function db() {
  return new Promise((ok, fail) => {
    const r = indexedDB.open('backup-settings', 1);
    r.onupgradeneeded = () => r.result.createObjectStore('kv');
    r.onsuccess = () => ok(r.result);
    r.onerror = () => fail(r.error);
  });
}
async function kvGet(key) {
  const d = await db();
  return new Promise((ok, fail) => {
    const r = d.transaction('kv').objectStore('kv').get(key);
    r.onsuccess = () => ok(r.result);
    r.onerror = () => fail(r.error);
  });
}
async function kvSet(key, value) {
  const d = await db();
  return new Promise((ok, fail) => {
    const t = d.transaction('kv', 'readwrite');
    t.objectStore('kv').put(value, key);
    t.oncomplete = () => ok();
    t.onerror = () => fail(t.error);
  });
}

// "Choose backup folder" button (the picker needs a tap).
document.getElementById('choose-folder').addEventListener('click', async () => {
  try {
    const dir = await showDirectoryPicker({ mode: 'readwrite', startIn: 'documents' });
    await kvSet('backupDir', dir);
    status.textContent = 'Backups go to ' + dir.name;
  } catch (e) {
    if (e.name !== 'AbortError') status.textContent = 'Could not choose: ' + e.message;
  }
});

// Called after every workout, and on a timer: no tap needed once the folder is chosen.
async function autoBackup(data) {
  const dir = await kvGet('backupDir');
  if (!dir) return 'no folder chosen yet';
  if ((await dir.queryPermission({ mode: 'readwrite' })) !== 'granted') {
    return 'folder access was removed: tap "Choose backup folder" again';
  }
  const file = await dir.getFileHandle('backup-' + new Date().toISOString().slice(0, 10) + '.json', { create: true });
  const w = await file.createWritable();
  await w.write(JSON.stringify(data));
  await w.close();
  return 'saved ' + file.name;
}

अगर पहुँच हटा दी गई (यूज़र ने ऐप की पहुँच साफ़ कर दी, या फ़ोल्डर मिटा दिया), तो queryPermission() का जवाब 'prompt' होता है। बटन टैप से, requestPermission() फ़ोल्डर पिकर को उसी फ़ोल्डर पर दोबारा खोलता है; वही फ़ोल्डर चुनने पर जवाब 'granted' होता है, कुछ और चुनने (या रद्द करने) पर 'denied':

document.getElementById('reconnect').addEventListener('click', async () => {
  const dir = await kvGet('backupDir');
  if (dir && (await dir.requestPermission({ mode: 'readwrite' })) === 'granted') {
    status.textContent = 'Backups go to ' + dir.name + ' again';
  } else {
    status.textContent = 'Choose a backup folder';
  }
});

यूज़र के चुने फ़ोल्डर को साफ़ करें: पुरानी .tmp फ़ाइलें और इस नाम के सब-फ़ोल्डर हटाएँ cache:

document.getElementById('clean').addEventListener('click', async () => {
  const dir = await showDirectoryPicker({ mode: 'readwrite' });
  let removed = 0;
  for await (const entry of dir.values()) {
    if (entry.kind === 'file' && entry.name.endsWith('.tmp')) {
      await dir.removeEntry(entry.name);
      removed++;
    } else if (entry.kind === 'directory' && entry.name === 'cache') {
      await dir.removeEntry(entry.name, { recursive: true });
      removed++;
    }
  }
  alert(removed + ' item(s) removed');
});

ध्यान दें: createWritable() स्पेक के स्वैप-फ़ाइल मॉडल पर चलता है - abort() (या पेज के चले जाने) से मूल फ़ाइल अछूती रहती है; { keepExistingData: true } मौजूदा सामग्री से शुरू होता है; write({type:'seek'|'truncate'|'write', …}), seek() और truncate() काम करते हैं। जिस फ़ाइल का ऐप उसे सिर्फ़-पढ़ने के लिए देता है, वह queryPermission({mode:'readwrite'}) का जवाब 'denied' से देती है और createWritable() NoModificationAllowedError से रिजेक्ट होता है। getFile() दस्तावेज़ को स्ट्रीम करता है, इसलिए बड़ी फ़ाइलें ठीक हैं। हैंडल IndexedDB में रखे जा सकते हैं (ऑब्जेक्ट और ऐरे के अंदर भी) और ऐप के रीस्टार्ट के बाद फिर इस्तेमाल हो सकते हैं: ऐप हर उस फ़ोल्डर और फ़ाइल के लिए Android की अनुमति रखता है जो किसी पिकर ने लौटाई (सबसे हाल की 64; सबसे पुरानी पहले छोड़ दी जाती है)। queryPermission() अनुमति रहने तक 'granted' और जाने के बाद 'prompt' देता है; तब requestPermission() को टैप चाहिए और वह पिकर को हैंडल के अपने फ़ोल्डर पर दोबारा खोलता है। 2026-10-03 से पहले बने ऐप रीस्टार्ट पर हैंडल खो देते थे - दोबारा बिल्ड करें, और उपयोगकर्ता फ़ोल्डर एक बार फिर चुनता है। types पिकर का फ़ाइल फ़िल्टर बन जाते हैं। पिकर सिर्फ़ टॉप-लेवल पेज के पास हैं। अंदरूनी ट्रांसपोर्ट: __fsPick (जवाब appmint:fs इवेंट से आता है), __fsFile, __fsList, __fsChild, __fsRemove, __fsPermission, __fsWritableOpen/Write/Truncate/Close/Abort। AI से बने ऐप जाँचते हैं can('fileHandles').

__dlAbort ब्रिज#

window.WebToApk.__dlAbort(token: String)

डाउनलोड [token] छोड़ देता है (पेज का पढ़ना फ़ेल हुआ या उसने हार मान ली)।

उदाहरण

WebToApk.__dlBegin के साथ दस्तावेज़ित - वहाँ का उदाहरण इसे भी दिखाता है।

<a download>, Blob डाउनलोड, window.open(blobUrl) और AppMint.saveBlob / AppMint.downloadFile के पीछे का अंदरूनी ट्रांसपोर्ट: फ़ाइल पेज से ऐप तक टुकड़ों में स्ट्रीम होती है, इसलिए बड़ा एक्सपोर्ट कभी मेमोरी में एक विशाल स्ट्रिंग नहीं बनता। पेज मानक डाउनलोड कोड इस्तेमाल करते हैं; इन्हें कभी सीधे कॉल न करें, क्योंकि __ वाले तरीके बिना सूचना के बदल सकते हैं।

लौटाता है: __dlBegin(name, type, size) एक टोकन लौटाता है (या फ़ोन में जगह न होने पर ""); अगर कोई स्लाइस लिखी न जा सके तो __dlChunk(token, base64) false लौटाता है; __dlEnd(token, action) ट्रांसफ़र पूरा करता है (save = डाउनलोड्स, open = खोलकर दिखाना, share, copy)। ज़रूरत: कुछ नहीं।

पब्लिक API इस्तेमाल करें:

const blob = new Blob([csvText], { type: 'text/csv' });
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = 'Report.csv';
a.click();   // lands in Downloads as "Report.csv"

उसी रास्ते से कैनवास की ड्रॉइंग एक्सपोर्ट करें:

canvas.toBlob(function (blob) {
  const a = document.createElement('a');
  a.href = URL.createObjectURL(blob);
  a.download = 'drawing.png';
  a.click();
  setTimeout(function () { URL.revokeObjectURL(a.href); }, 10000);
}, 'image/png');

JSON बैकअप सेव करें जिसे यूज़र रख सके, पूरा होने पर संदेश के साथ:

function exportBackup(data) {
  const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
  const a = document.createElement('a');
  a.href = URL.createObjectURL(blob);
  a.download = 'backup-' + new Date().toISOString().slice(0, 10) + '.json';
  document.body.appendChild(a);
  a.click();
  a.remove();
  showMessage('Backup saved to Downloads.');
}

नोट: यह ऐप शेल (ShellDownloads.kt) में लागू है।

__dlBegin ब्रिज#

window.WebToApk.__dlBegin(name: String, type: String, size: Double): String

[size] बाइट का, [name] नाम का ([type] = उसका MIME टाइप) डाउनलोड खोलता है जिसे पेज __dlChunk से स्ट्रीम करेगा। एक token लौटाता है, या जगह न होने पर ""।

उदाहरण

<a download>, Blob डाउनलोड, window.open(blobUrl) और AppMint.saveBlob / AppMint.downloadFile के पीछे का अंदरूनी ट्रांसपोर्ट: फ़ाइल पेज से ऐप तक टुकड़ों में स्ट्रीम होती है, इसलिए बड़ा एक्सपोर्ट कभी मेमोरी में एक विशाल स्ट्रिंग नहीं बनता। पेज मानक डाउनलोड कोड इस्तेमाल करते हैं; इन्हें कभी सीधे कॉल न करें, क्योंकि __ वाले तरीके बिना सूचना के बदल सकते हैं।

लौटाता है: __dlBegin(name, type, size) एक टोकन लौटाता है (या फ़ोन में जगह न होने पर ""); अगर कोई स्लाइस लिखी न जा सके तो __dlChunk(token, base64) false लौटाता है; __dlEnd(token, action) ट्रांसफ़र पूरा करता है (save = डाउनलोड्स, open = खोलकर दिखाना, share, copy)। ज़रूरत: कुछ नहीं।

पब्लिक API इस्तेमाल करें:

const blob = new Blob([csvText], { type: 'text/csv' });
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = 'Report.csv';
a.click();   // lands in Downloads as "Report.csv"

उसी रास्ते से कैनवास की ड्रॉइंग एक्सपोर्ट करें:

canvas.toBlob(function (blob) {
  const a = document.createElement('a');
  a.href = URL.createObjectURL(blob);
  a.download = 'drawing.png';
  a.click();
  setTimeout(function () { URL.revokeObjectURL(a.href); }, 10000);
}, 'image/png');

JSON बैकअप सेव करें जिसे यूज़र रख सके, पूरा होने पर संदेश के साथ:

function exportBackup(data) {
  const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
  const a = document.createElement('a');
  a.href = URL.createObjectURL(blob);
  a.download = 'backup-' + new Date().toISOString().slice(0, 10) + '.json';
  document.body.appendChild(a);
  a.click();
  a.remove();
  showMessage('Backup saved to Downloads.');
}

नोट: यह ऐप शेल (ShellDownloads.kt) में लागू है।

__dlChunk ब्रिज#

window.WebToApk.__dlChunk(token: String, base64: String): Boolean

डाउनलोड [token] का एक base64 स्लाइस; ट्रांसफ़र नाकाम होने पर false।

उदाहरण

WebToApk.__dlBegin के साथ दस्तावेज़ित - वहाँ का उदाहरण इसे भी दिखाता है।

<a download>, Blob डाउनलोड, window.open(blobUrl) और AppMint.saveBlob / AppMint.downloadFile के पीछे का अंदरूनी ट्रांसपोर्ट: फ़ाइल पेज से ऐप तक टुकड़ों में स्ट्रीम होती है, इसलिए बड़ा एक्सपोर्ट कभी मेमोरी में एक विशाल स्ट्रिंग नहीं बनता। पेज मानक डाउनलोड कोड इस्तेमाल करते हैं; इन्हें कभी सीधे कॉल न करें, क्योंकि __ वाले तरीके बिना सूचना के बदल सकते हैं।

लौटाता है: __dlBegin(name, type, size) एक टोकन लौटाता है (या फ़ोन में जगह न होने पर ""); अगर कोई स्लाइस लिखी न जा सके तो __dlChunk(token, base64) false लौटाता है; __dlEnd(token, action) ट्रांसफ़र पूरा करता है (save = डाउनलोड्स, open = खोलकर दिखाना, share, copy)। ज़रूरत: कुछ नहीं।

पब्लिक API इस्तेमाल करें:

const blob = new Blob([csvText], { type: 'text/csv' });
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = 'Report.csv';
a.click();   // lands in Downloads as "Report.csv"

उसी रास्ते से कैनवास की ड्रॉइंग एक्सपोर्ट करें:

canvas.toBlob(function (blob) {
  const a = document.createElement('a');
  a.href = URL.createObjectURL(blob);
  a.download = 'drawing.png';
  a.click();
  setTimeout(function () { URL.revokeObjectURL(a.href); }, 10000);
}, 'image/png');

JSON बैकअप सेव करें जिसे यूज़र रख सके, पूरा होने पर संदेश के साथ:

function exportBackup(data) {
  const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
  const a = document.createElement('a');
  a.href = URL.createObjectURL(blob);
  a.download = 'backup-' + new Date().toISOString().slice(0, 10) + '.json';
  document.body.appendChild(a);
  a.click();
  a.remove();
  showMessage('Backup saved to Downloads.');
}

नोट: यह ऐप शेल (ShellDownloads.kt) में लागू है।

__dlEnd ब्रिज#

window.WebToApk.__dlEnd(token: String, action: String): Boolean

डाउनलोड [token] पूरा करता है और उस पर कार्रवाई करता है: [action] "save" (Downloads में, "Downloaded · Open" बार और नोटिफ़िकेशन के साथ), "open" (PDF व्यूअर या उस टाइप के लिए फ़ोन का ऐप), "share" (शेयर शीट) या "copy" (इमेज क्लिपबोर्ड पर)।

उदाहरण

WebToApk.__dlBegin के साथ दस्तावेज़ित - वहाँ का उदाहरण इसे भी दिखाता है।

<a download>, Blob डाउनलोड, window.open(blobUrl) और AppMint.saveBlob / AppMint.downloadFile के पीछे का अंदरूनी ट्रांसपोर्ट: फ़ाइल पेज से ऐप तक टुकड़ों में स्ट्रीम होती है, इसलिए बड़ा एक्सपोर्ट कभी मेमोरी में एक विशाल स्ट्रिंग नहीं बनता। पेज मानक डाउनलोड कोड इस्तेमाल करते हैं; इन्हें कभी सीधे कॉल न करें, क्योंकि __ वाले तरीके बिना सूचना के बदल सकते हैं।

लौटाता है: __dlBegin(name, type, size) एक टोकन लौटाता है (या फ़ोन में जगह न होने पर ""); अगर कोई स्लाइस लिखी न जा सके तो __dlChunk(token, base64) false लौटाता है; __dlEnd(token, action) ट्रांसफ़र पूरा करता है (save = डाउनलोड्स, open = खोलकर दिखाना, share, copy)। ज़रूरत: कुछ नहीं।

पब्लिक API इस्तेमाल करें:

const blob = new Blob([csvText], { type: 'text/csv' });
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = 'Report.csv';
a.click();   // lands in Downloads as "Report.csv"

उसी रास्ते से कैनवास की ड्रॉइंग एक्सपोर्ट करें:

canvas.toBlob(function (blob) {
  const a = document.createElement('a');
  a.href = URL.createObjectURL(blob);
  a.download = 'drawing.png';
  a.click();
  setTimeout(function () { URL.revokeObjectURL(a.href); }, 10000);
}, 'image/png');

JSON बैकअप सेव करें जिसे यूज़र रख सके, पूरा होने पर संदेश के साथ:

function exportBackup(data) {
  const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
  const a = document.createElement('a');
  a.href = URL.createObjectURL(blob);
  a.download = 'backup-' + new Date().toISOString().slice(0, 10) + '.json';
  document.body.appendChild(a);
  a.click();
  a.remove();
  showMessage('Backup saved to Downloads.');
}

नोट: यह ऐप शेल (ShellDownloads.kt) में लागू है।

__fsChild ब्रिज#

window.WebToApk.__fsChild(uri: String, name: String, kind: String, create: Boolean): String

getFileHandle / getDirectoryHandle (`create` के साथ)।

उदाहरण

getFileHandle(name, {create}) और getDirectoryHandle(name, {create}) के पीछे का आंतरिक ट्रांसपोर्ट। पेज स्टैंडर्ड API इस्तेमाल करते हैं; इसे कभी सीधे कॉल न करें - __ वाले मेथड बिना सूचना बदल सकते हैं।

लौटाता है: JSON {ok, kind, name, uri, canWrite} या {ok:false, error} (NotFoundError, TypeMismatchError, NoModificationAllowedError)। ज़रूरी: इससे मिला फ़ोल्डर showDirectoryPicker().

पब्लिक API इस्तेमाल करें:

const dir = await showDirectoryPicker();
const photos = await dir.getDirectoryHandle('photos', { create: true });
const file = await photos.getFileHandle('list.json', { create: true });

फ़ोल्डर में फ़ाइल बनाने से पहले जाँचें कि वह पहले से मौजूद है या नहीं:

async function exists(dir, name) {
  try {
    await dir.getFileHandle(name);                     // no {create} = look up only
    return true;
  } catch (e) {
    if (e.name === 'NotFoundError') return false;
    if (e.name === 'TypeMismatchError') return true;   // a folder already has that name
    throw e;
  }
}

ऐप सेटिंग्स को "config" सब-फ़ोल्डर में JSON के रूप में सेव करें, ज़रूरत पड़ने पर दोनों बनाकर:

async function saveSettings(root, settings) {
  const config = await root.getDirectoryHandle('config', { create: true });
  const file = await config.getFileHandle('settings.json', { create: true });
  const w = await file.createWritable();
  await w.write(JSON.stringify(settings, null, 2));
  await w.close();
}
// root = await showDirectoryPicker()  (from a button tap)

ध्यान दें: ऐप शेल (FileSystemAccess.kt) में लागू।

__fsFile ब्रिज#

window.WebToApk.__fsFile(uri: String): String

getFile(): `{ok, name, type, size, lastModified, url}` (सामग्री url से स्ट्रीम होती है)।

उदाहरण

FileSystemFileHandle.getFile() के पीछे का आंतरिक ट्रांसपोर्ट। पेज स्टैंडर्ड API इस्तेमाल करते हैं; इसे कभी सीधे कॉल न करें - __ मेथड बिना सूचना के बदल सकते हैं।

लौटाता है: JSON {ok, name, type, size, lastModified, url} - बॉडी url से स्ट्रीम होती है (ऐप का अपना origin, ranges के साथ) - या {ok:false, error} (NotFoundError, TypeMismatchError, foreign_origin)। ज़रूरी: किसी एक पिकर से मिला हैंडल।

पब्लिक API इस्तेमाल करें:

const [handle] = await showOpenFilePicker();
const file = await handle.getFile();
console.log(file.name, file.size, await file.text());

चुनी हुई इमेज को स्ट्रिंग में पढ़े बिना पेज पर दिखाएँ:

document.querySelector('#choose').addEventListener('click', async function () {
  const [handle] = await showOpenFilePicker({ types: [{ accept: { 'image/*': [] } }] });
  const file = await handle.getFile();                    // a real File, streamed from the app
  const img = document.querySelector('#preview');
  if (img.src) URL.revokeObjectURL(img.src);
  img.src = URL.createObjectURL(file);
  img.alt = file.name + ' (' + file.type + ')';
});

पहले खोली गई फ़ाइल को दोबारा पढ़ें और पता लगाएँ कि वह बदली या हटाई गई है:

async function reloadIfChanged(handle, lastSeen) {
  try {
    const file = await handle.getFile();
    if (file.lastModified === lastSeen) return null;      // unchanged
    return { data: JSON.parse(await file.text()), lastModified: file.lastModified };
  } catch (e) {
    if (e.name === 'NotFoundError') alert(handle.name + ' was moved or deleted.');
    else throw e;
    return null;
  }
}

ध्यान दें: ऐप शेल (FileSystemAccess.kt) में लागू।

__fsList ब्रिज#

window.WebToApk.__fsList(uri: String): String

डायरेक्टरी हैंडल की एंट्री: `{ok, entries:[{kind, name, uri, canWrite}]}`।

उदाहरण

FileSystemDirectoryHandle (entries(), keys(), values(), for await) पर इटरेट करने के पीछे का आंतरिक ट्रांसपोर्ट। पेज स्टैंडर्ड API इस्तेमाल करते हैं; इसे कभी सीधे कॉल न करें - __ वाले मेथड बिना सूचना बदल सकते हैं।

लौटाता है: JSON {ok, entries:[{kind, name, uri, canWrite}]} या {ok:false, error}। ज़रूरी: इससे मिला फ़ोल्डर showDirectoryPicker().

पब्लिक API इस्तेमाल करें:

const dir = await showDirectoryPicker();
for await (const [name, entry] of dir.entries()) console.log(entry.kind, name);

किसी फ़ोल्डर और उसके सभी सब-फ़ोल्डर की फ़ाइलें गिनें:

async function countFiles(dir) {
  let n = 0;
  for await (const entry of dir.values()) {
    if (entry.kind === 'file') n++;
    else n += await countFiles(entry);                 // entry is a FileSystemDirectoryHandle
  }
  return n;
}
document.querySelector('#scan').addEventListener('click', async function () {
  const dir = await showDirectoryPicker();
  console.log(dir.name, 'holds', await countFiles(dir), 'files');
});

यूज़र के चुने फ़ोल्डर की फ़ोटो की क्रमबद्ध सूची बनाएँ:

document.querySelector('#open-album').addEventListener('click', async function () {
  const dir = await showDirectoryPicker({ startIn: 'pictures' });
  const names = [];
  for await (const name of dir.keys()) {
    if (/\.(jpe?g|png|webp)$/i.test(name)) names.push(name);
  }
  names.sort(function (a, b) { return a.localeCompare(b); });
  renderAlbum(dir, names);                             // dir.getFileHandle(name) opens each one
});

ध्यान दें: ऐप शेल (FileSystemAccess.kt) में लागू।

__fsPermission ब्रिज#

window.WebToApk.__fsPermission(rootUri: String, uri: String): String

queryPermission() / requestPermission(): ऐसे हैंडल के लिए `{ok, state:'granted'|'prompt'}` जिसका चुना हुआ रूट [rootUri] है - ऐप के पास उस चुनाव की पहुँच रहने तक 'granted', रीलॉन्च के बाद भी (पहुँच सेव रहती है; हैंडल IndexedDB से वापस आता है)।

उदाहरण

FileSystemHandle.queryPermission() और requestPermission() के पीछे का आंतरिक ट्रांसपोर्ट। पेज स्टैंडर्ड API इस्तेमाल करते हैं; इसे कभी सीधे कॉल न करें - __ वाले मेथड बिना सूचना बदल सकते हैं।

लौटाता है: JSON {ok:true, state:'granted'} जब तक ऐप के पास उस चुने हुए फ़ोल्डर या फ़ाइल की अनुमति है जिससे हैंडल आया है (रीस्टार्ट के बाद भी रहती है), अनुमति जाने पर {ok:true, state:'prompt'}, या {ok:false, error:'foreign_origin'}। सिर्फ़-पढ़ने वाली फ़ाइल पेज में {mode:'readwrite'} के लिए 'denied' देती है, बिना कॉल किए। ज़रूरत: किसी पिकर से मिला हैंडल, IndexedDB से वापस पढ़ा हुआ भी।

पब्लिक API इस्तेमाल करें:

const dir = await kvGet('backupDir');                 // a handle stored in IndexedDB earlier
if ((await dir.queryPermission({ mode: 'readwrite' })) === 'granted') {
  const f = await dir.getFileHandle('backup.json', { create: true });
  const w = await f.createWritable();
  await w.write(JSON.stringify(state));
  await w.close();
}

अनुमति जाने पर टैप से दोबारा माँगें - फ़ोल्डर पिकर उसी फ़ोल्डर पर खुलता है, और सिर्फ़ वही फ़ोल्डर अनुमति देता है:

document.getElementById('reconnect').addEventListener('click', async () => {
  const dir = await kvGet('backupDir');
  const state = await dir.requestPermission({ mode: 'readwrite' });   // 'granted' | 'denied'
  status.textContent = state === 'granted' ? 'Backups resumed' : 'Choose a backup folder';
});

नोट: यह ऐप शेल (FileSystemAccess.kt) में लागू है। पूरे File System Access API के लिए showOpenFilePicker देखें।

__fsPick ब्रिज#

window.WebToApk.__fsPick(requestId: String, kind: String, optionsJson: String): String

[kind] पिकर (open | save | directory) खोलता है; हैंडल इस रूप में आते हैं `appmint:fs`.

उदाहरण

showOpenFilePicker(), showSaveFilePicker() और showDirectoryPicker() के पीछे का आंतरिक ट्रांसपोर्ट। पेज स्टैंडर्ड API इस्तेमाल करते हैं; इसे सीधे कभी कॉल न करें - __ वाले मेथड बिना सूचना के बदल सकते हैं।

लौटाता है: पिकर स्क्रीन पर आते ही {ok:true}, या {ok:false, error} (पहले से एक खुला होने पर NotAllowedError, foreign_origin)। हैंडल appmint:fs इवेंट {requestId, handles:[{kind, name, uri, canWrite}]} या {requestId, error:'cancelled'} के रूप में आते हैं। ज़रूरी: चालू करने के लिए कुछ नहीं।

पब्लिक API इस्तेमाल करें:

const [handle] = await showOpenFilePicker({ types: [{ accept: { 'image/*': ['.png', '.jpg'] } }] });
const file = await handle.getFile();

बटन दबाने पर सुझाए गए नाम के साथ रिपोर्ट सेव करें (पिकर के लिए यूज़र का टैप ज़रूरी है):

document.querySelector('#export').addEventListener('click', async function () {
  try {
    const handle = await showSaveFilePicker({
      suggestedName: 'report.csv',
      types: [{ description: 'CSV', accept: { 'text/csv': ['.csv'] } }]
    });
    const w = await handle.createWritable();
    await w.write(buildCsv());
    await w.close();
  } catch (e) {
    if (e.name !== 'AbortError') alert('Export failed: ' + e.message);   // AbortError = cancelled
  }
});

यूज़र को एक साथ कई फ़ोटो चुनने दें, Pictures फ़ोल्डर से शुरू करके:

document.querySelector('#add-photos').addEventListener('click', async function () {
  let handles;
  try {
    handles = await showOpenFilePicker({
      multiple: true,
      startIn: 'pictures',
      types: [{ accept: { 'image/*': ['.jpg', '.jpeg', '.png', '.webp'] } }]
    });
  } catch (e) { return; }                                 // cancelled or already open
  for (const h of handles) addToGallery(await h.getFile());
});

ध्यान दें: ऐप शेल (FileSystemAccess.kt) में लागू। वही पेज कोड डेस्कटॉप Chrome में बिना बदलाव के काम करता है।

__fsRemove ब्रिज#

window.WebToApk.__fsRemove(uri: String, name: String, recursive: Boolean): String

removeEntry(name, {recursive})।

उदाहरण

FileSystemDirectoryHandle.removeEntry(name, {recursive}) के पीछे का आंतरिक ट्रांसपोर्ट। पेज स्टैंडर्ड API इस्तेमाल करते हैं; इसे कभी सीधे कॉल न करें - __ मेथड बिना सूचना के बदल सकते हैं।

लौटाता है: JSON {ok:true} या {ok:false, error} (NotFoundError, खाली न होने वाले फ़ोल्डर के लिए recursive के बिना InvalidModificationError)। ज़रूरी: इससे मिला फ़ोल्डर showDirectoryPicker().

पब्लिक API इस्तेमाल करें:

const dir = await showDirectoryPicker();
await dir.removeEntry('old-drafts', { recursive: true });

एक फ़ाइल हटाएँ और "पहले ही हट चुकी" को पूरा हुआ मानें:

async function deleteFile(dir, name) {
  try {
    await dir.removeEntry(name);
  } catch (e) {
    if (e.name !== 'NotFoundError') throw e;   // NotFoundError: nothing left to delete
  }
}

फ़ोल्डर हटाएँ, और सिर्फ़ तब पहले पूछें जब उसमें अब भी कुछ हो:

async function removeFolder(dir, name) {
  try {
    await dir.removeEntry(name);                          // succeeds for an empty folder
  } catch (e) {
    if (e.name !== 'InvalidModificationError') throw e;
    if (!confirm('"' + name + '" is not empty. Delete it and everything inside?')) return false;
    await dir.removeEntry(name, { recursive: true });
  }
  return true;
}

ध्यान दें: ऐप शेल (FileSystemAccess.kt) में लागू।

__fsWritableAbort ब्रिज#

window.WebToApk.__fsWritableAbort(token: String)

abort(): स्वैप छोड़ दिया जाता है; दस्तावेज़ को छुआ नहीं जाता।

उदाहरण

FileSystemWritableFileStream.abort() के पीछे का आंतरिक ट्रांसपोर्ट - स्वैप कॉपी छोड़ दी जाती है और फ़ाइल जैसी थी वैसी रहती है। पेज स्टैंडर्ड API इस्तेमाल करते हैं; इसे कभी सीधे कॉल न करें - __ वाले मेथड बिना सूचना बदल सकते हैं।

लौटाता है: कुछ नहीं। ज़रूरी: एक खुली लिखने योग्य स्ट्रीम।

पब्लिक API इस्तेमाल करें:

const w = await handle.createWritable();
try { await w.write(await buildExport()); await w.close(); }
catch (e) { await w.abort(); }

उपयोगकर्ता को लंबा एक्सपोर्ट रद्द करने दें; आधी लिखी कोई चीज़ फ़ाइल तक नहीं पहुँचती:

let cancelled = false;
document.getElementById('cancel').onclick = function () { cancelled = true; };

async function exportAll(handle, pages) {
  const w = await handle.createWritable();
  for (const page of pages) {
    if (cancelled) { await w.abort(); return 'cancelled'; }   // the file keeps its old content
    await w.write(await renderPage(page));
  }
  await w.close();
  return 'saved';
}

pipeTo से सीधे फ़ाइल में डाउनलोड करें; डाउनलोड रोकने पर लिखना रद्द हो जाता है:

const ctl = new AbortController();
document.getElementById('stop').onclick = function () { ctl.abort(); };

async function downloadInto(handle, url) {
  const res = await fetch(url);
  if (!res.ok) throw new Error('HTTP ' + res.status);
  const w = await handle.createWritable();
  try {
    await res.body.pipeTo(w, { signal: ctl.signal });     // closes the file when the body ends
    return 'saved';
  } catch (e) {
    return e.name === 'AbortError' ? 'stopped, file unchanged' : 'failed: ' + e.message;
  }
}

ध्यान दें: ऐप शेल (FileSystemAccess.kt) में लागू।

__fsWritableClose ब्रिज#

window.WebToApk.__fsWritableClose(token: String): String

close(): स्वैप होने पर दस्तावेज़ बदल जाता है।

उदाहरण

FileSystemWritableFileStream.close() के पीछे का आंतरिक ट्रांसपोर्ट - स्वैप कॉपी फ़ाइल की जगह ले लेती है। पेज स्टैंडर्ड API इस्तेमाल करते हैं; इसे कभी सीधे कॉल न करें - __ वाले मेथड बिना सूचना बदल सकते हैं।

लौटाता है: JSON {ok:true} या {ok:false, error} (NoModificationAllowedError, InvalidModificationError जब फ़ाइल का ऐप उसे छोटा न कर सके)। ज़रूरत: एक खुली लिखने योग्य स्ट्रीम।

पब्लिक API इस्तेमाल करें:

const w = await handle.createWritable();
await w.write(json);
await w.close();   // nothing changes on disk before this

उपयोगकर्ता को बताएँ कि सेव क्यों नहीं हो पाया (close() वह जगह है जहाँ यह नाकाम हो सकता है):

async function saveText(handle, text) {
  const w = await handle.createWritable();
  await w.write(text);
  try {
    await w.close();
    return true;
  } catch (e) {
    if (e.name === 'InvalidModificationError') alert('This file\'s app cannot make it shorter. Save a copy instead.');
    else if (e.name === 'NoModificationAllowedError') alert('This file cannot be changed.');
    else alert('Save failed: ' + e.message);
    return false;
  }
}

फ़ाइल कई टुकड़ों में लिखें; पढ़ने वाले तब तक पुरानी सामग्री ही देखते हैं जब तक वह एक close():

async function saveRows(handle, rows) {
  const w = await handle.createWritable();
  await w.write('id,name,total\n');
  for (const r of rows) await w.write(r.id + ',' + r.name + ',' + r.total + '\n');
  console.log('before close:', (await handle.getFile()).size, 'bytes (old file)');
  await w.close();
  console.log('after close:', (await handle.getFile()).size, 'bytes');
}

ध्यान दें: ऐप शेल (FileSystemAccess.kt) में लागू।

__fsWritableOpen ब्रिज#

window.WebToApk.__fsWritableOpen(uri: String, keepExisting: Boolean): String

createWritable(): एक स्वैप फ़ाइल, जो [keepExisting] होने पर फ़ाइल से कॉपी की जाती है।

उदाहरण

FileSystemFileHandle.createWritable({keepExistingData}) के पीछे का आंतरिक ट्रांसपोर्ट। पेज स्टैंडर्ड API इस्तेमाल करते हैं; इसे कभी सीधे कॉल न करें - __ मेथड बिना सूचना के बदल सकते हैं।

लौटाता है: निजी स्वैप कॉपी के लिए JSON {ok, token}, या {ok:false, error} (सिर्फ़-पढ़ने वाली फ़ाइल के लिए NoModificationAllowedError)। ज़रूरी: किसी एक पिकर से मिला हैंडल।

पब्लिक API इस्तेमाल करें:

const handle = await showSaveFilePicker({ suggestedName: 'report.csv' });
const w = await handle.createWritable({ keepExistingData: false });
await w.write('a,b\n1,2\n');
await w.close();

मौजूदा लॉग फ़ाइल के अंत में एक लाइन जोड़ें, जो उसमें पहले से है उसे रखते हुए:

async function appendLine(handle, line) {
  const size = (await handle.getFile()).size;
  const w = await handle.createWritable({ keepExistingData: true });
  await w.seek(size);                                     // write after the old content
  await w.write(new Date().toISOString() + ' ' + line + '\n');
  await w.close();                                        // only now does the file change
}

कैनवस को PNG के रूप में सेव करें, और कुछ गड़बड़ होने पर फ़ाइल को जैसी है वैसी छोड़ दें:

document.querySelector('#save-png').addEventListener('click', async function () {
  const handle = await showSaveFilePicker({
    suggestedName: 'drawing.png',
    types: [{ accept: { 'image/png': ['.png'] } }]
  });
  const blob = await new Promise(function (r) { document.querySelector('canvas').toBlob(r, 'image/png'); });
  const w = await handle.createWritable();
  try {
    await w.write(blob);
    await w.close();
  } catch (e) {
    try { await w.abort(); } catch (x) {}                 // nothing reaches the file before close()
    alert('Could not save: ' + e.message);
  }
});

ध्यान दें: ऐप शेल (FileSystemAccess.kt) में लागू।

__fsWritableTruncate ब्रिज#

window.WebToApk.__fsWritableTruncate(token: String, size: Double): String

लिखने योग्य स्ट्रीम पर truncate(size)।

उदाहरण

FileSystemWritableFileStream.truncate(size) के पीछे का आंतरिक ट्रांसपोर्ट। पेज स्टैंडर्ड API इस्तेमाल करते हैं; इसे कभी सीधे कॉल न करें - __ मेथड बिना सूचना के बदल सकते हैं।

लौटाता है: JSON {ok:true} या {ok:false, error}। ज़रूरत: एक खुली लिखने योग्य स्ट्रीम।

पब्लिक API इस्तेमाल करें:

const w = await handle.createWritable({ keepExistingData: true });
await w.truncate(0);
await w.write('fresh start');
await w.close();

फ़ाइल के सिर्फ़ पहले 1,000 बाइट रखें और उसके बाद का सब कुछ काट दें:

async function keepFirstKilobyte(handle) {
  const w = await handle.createWritable({ keepExistingData: true });
  await w.truncate(1000);   // a file shorter than this grows instead (see the next example)
  await w.close();
}

जगह रिज़र्व करें: बड़े आकार पर काटने से फ़ाइल बढ़ती है, और नए बाइट शून्य के रूप में पढ़े जाते हैं:

async function makeEmptySlotFile(handle, slots, slotSize) {
  const w = await handle.createWritable();
  await w.truncate(slots * slotSize);                  // e.g. 100 slots of 256 bytes, all zero
  await w.write({ type: 'write', position: 0, data: new Uint8Array([1, 0, 0, 0]) });
  await w.close();
  console.log('file is now', (await handle.getFile()).size, 'bytes');
}

ध्यान दें: ऐप शेल (FileSystemAccess.kt) में लागू।

__fsWritableWrite ब्रिज#

window.WebToApk.__fsWritableWrite(token: String, position: Double, base64: String): String

लिखने योग्य स्ट्रीम के write() का एक टुकड़ा, [position] पर।

उदाहरण

FileSystemWritableFileStream.write() के पीछे का आंतरिक ट्रांसपोर्ट - स्वैप कॉपी में किसी स्थान पर एक टुकड़ा। पेज स्टैंडर्ड API इस्तेमाल करते हैं; इसे कभी सीधे कॉल न करें - __ वाले मेथड बिना सूचना बदल सकते हैं।

लौटाता है: JSON {ok:true} या {ok:false, error}। ज़रूरत: एक खुली लिखने योग्य स्ट्रीम।

पब्लिक API इस्तेमाल करें:

const w = await handle.createWritable({ keepExistingData: true });
await w.write({ type: 'write', position: 0, data: new Blob(['HEADER\n']) });
await w.close();

यूज़र की चुनी लॉग फ़ाइल के अंत में एक लाइन जोड़ें (फ़ाइल पिकर को टैप चाहिए):

document.getElementById('addLine').addEventListener('click', async () => {
  const [handle] = await showOpenFilePicker({
    types: [{ description: 'Text', accept: { 'text/plain': ['.txt', '.log'] } }]
  });
  const size = (await handle.getFile()).size;
  const w = await handle.createWritable({ keepExistingData: true });
  await w.seek(size);                                   // start writing after the old content
  await w.write(new Date().toISOString() + ' checked in\n');
  await w.close();
});

"इस रूप में सेव करें" के साथ कैनवास की ड्रॉइंग को नई PNG के रूप में सेव करें (Blob अपने कच्चे बाइट के रूप में लिखा जाता है):

document.getElementById('saveDrawing').addEventListener('click', async () => {
  const handle = await showSaveFilePicker({
    suggestedName: 'drawing.png',
    types: [{ description: 'PNG image', accept: { 'image/png': ['.png'] } }]
  });
  const blob = await new Promise(function (ok) { document.querySelector('canvas').toBlob(ok, 'image/png'); });
  const w = await handle.createWritable();
  await w.write(blob);
  await w.close();
});

ध्यान दें: ऐप शेल (FileSystemAccess.kt) में लागू किया गया है। बड़ा डेटा चंक में भेजा जाता है।

getMaxIoChunkSize ब्रिज#

window.WebToApk.getMaxIoChunkSize(): Long

[readFileBase64] / [writeFileBase64] के लिए हर कॉल की बाइट सीमा, चंक गणित के लिए।

उदाहरण

readFileBase64 / writeFileBase64 की प्रति-कॉल बाइट सीमा (4 MB)। पेज आमतौर पर इस्तेमाल करते हैं WebToApkFS.maxChunkSize().

लौटाता है: (sync) एक संख्या (4194304)। ज़रूरत: इसे कॉल करने के लिए कुछ नहीं; जिन पढ़ने/लिखने वाले मेथड का यह वर्णन करता है, उनके लिए चरण 4 (Access) में Native Folder Access (SAF) पर टिक होना चाहिए।

सार्वजनिक तरीका:

const step = window.WebToApkFS ? WebToApkFS.maxChunkSize() : 0;

सीधा कॉल:

let step = 1048576;
if (window.WebToApk && typeof WebToApk.getMaxIoChunkSize === 'function') {
  step = Number(WebToApk.getMaxIoChunkSize()) || step;
}
console.log('read/write up to', step, 'bytes per call');

हिसाब लगाएँ कि फ़ाइल में कितने ब्रिज कॉल लगेंगे, ताकि कॉपी करने से पहले प्रोग्रेस बार का साइज़ तय हो सके:

function plannedSteps(fileUri) {
  const step = WebToApkFS.maxChunkSize();          // 0 when the helper is not available
  const info = WebToApkFS.stat(fileUri);
  if (!step || !info.ok) return 0;
  const steps = Math.max(1, Math.ceil(info.size / step));
  document.querySelector('#progress').max = steps;
  return steps;
}

एक फ़ाइल को दूसरी में कॉपी करें, एक बार में अधिकतम साइज़ का एक टुकड़ा:

function copyFile(fromUri, toPath) {
  const step = WebToApkFS.maxChunkSize() || 1048576;
  let offset = 0;
  for (;;) {
    const r = WebToApkFS.readBytes(fromUri, offset, step);
    if (!r.ok) return r;
    const w = WebToApkFS.writeBytes(toPath, r.base64, offset, offset === 0 ? 'truncate' : 'patch');
    if (!w.ok) return w;
    offset += r.bytesRead;
    if (r.eof || r.bytesRead <= 0) return { ok: true, bytes: offset };
  }
}
// copyFile(pickedUri, 'photo-backup.jpg')  (a file in the granted folder)

ध्यान दें: इससे बड़ा write chunk too large (max N bytes) के साथ मना कर दिया जाता है; इससे बड़ा read इसी साइज़ तक काट दिया जाता है।

pickFile ब्रिज#

window.WebToApk.pickFile(requestId: String, mimeFilter: String)

सिस्टम का सिंगल-फ़ाइल पिकर (ACTION_OPEN_DOCUMENT) खोलता है। [requestFolderAccess] के उलट यह ठीक एक फ़ाइल की पहुँच देता है, जो एडिटर या श्रेडर को माँगनी चाहिए; पढ़ने/लिखने की पहुँच सेव रहती है ताकि प्रोसेस खत्म होने के बाद भी URI काम करता रहे। जवाब `WebToApkOnFilePickResult(success, fileJson, requestId)` पर आता है; विफलता का `error` "cancelled", "disabled", "no-picker", "foreign_origin", या "busy" होता है (पहले अनुरोध के लिए पिकर पहले से खुला है, जो अपना जवाब खुद रखता है)। @param mimeFilter जैसे "image/\*", "application/pdf", या किसी भी फ़ाइल के लिए ""। (तारे के पहले बैकस्लैश सिर्फ़ इसलिए है क्योंकि Kotlin के ब्लॉक कमेंट नेस्ट होते हैं।)

उदाहरण

सिस्टम पिकर खोलता है, सिर्फ़ एक फ़ाइल के लिए। पेज आमतौर पर Promise हेल्पर WebToApkFS.pickFile(mimeFilter) इस्तेमाल करते हैं; यह कच्चा तरीका कॉलबैक से जवाब देता है।

लौटाता है: अभी कुछ नहीं। बाद में शेल window.WebToApkOnFilePickResult(success, fileJson, requestId) कॉल करता है, जहाँ fileJson सफलता पर JSON स्ट्रिंग {ok, uri, name, size, mime} है, या {ok:false, uri:'', error} जिसमें error = 'cancelled', 'disabled', 'no-picker', 'busy' या 'foreign_origin'। ज़रूरी: बिल्ड करते समय चरण 4 (Access) में Native Folder Access (SAF) टिक करें।

सार्वजनिक तरीका (अनुशंसित):

const f = await WebToApkFS.pickFile('image/*');
if (f.ok) console.log('picked', f.name, f.uri);

कच्चा कॉल, अपने खुद के मिलान के साथ requestId:

function pickRaw(mime) {
  return new Promise(function (resolve) {
    if (!window.WebToApk || typeof WebToApk.pickFile !== 'function') { resolve(null); return; }
    const myId = 'pick_' + Date.now();
    window.WebToApkOnFilePickResult = function (success, fileJson, requestId) {
      if (requestId !== myId) return;
      const r = JSON.parse(fileJson);           // fileJson is a string
      resolve(success && r.ok ? r : null);      // null = cancelled or not allowed
    };
    WebToApk.pickFile(myId, mime || '');        // '' = any file
  });
}

PDF अटैच करें और उसका नाम और साइज़ दिखाएँ, रद्द करने को असली समस्या से अलग पहचानते हुए:

document.querySelector('#attach').addEventListener('click', async function () {
  if (!window.WebToApkFS) return;
  const f = await WebToApkFS.pickFile('application/pdf');
  if (!f.ok) {
    if (f.error === 'cancelled' || f.error === 'busy') return;   // user closed it / one already open
    alert('Could not open the file picker: ' + f.error);         // 'disabled', 'no-picker', ...
    return;
  }
  document.querySelector('#attachment').textContent =
    f.name + ' (' + Math.round(f.size / 1024) + ' KB)';
  localStorage.setItem('attachmentUri', f.uri);   // the grant is kept across restarts
});

टेक्स्ट फ़ाइल खोलें और उसकी सामग्री एडिटर में लोड करें:

async function openTextFile() {
  if (!window.WebToApkFS) return;
  const f = await WebToApkFS.pickFile('text/*');
  if (!f.ok) return;
  const r = WebToApkFS.readAll(f.uri);            // {ok, base64, size} or {ok:false, error}
  if (!r.ok) { alert('Could not read ' + f.name + ': ' + r.error); return; }
  const bytes = Uint8Array.from(atob(r.base64), function (c) { return c.charCodeAt(0); });
  document.querySelector('#editor').value = new TextDecoder('utf-8').decode(bytes);
  document.querySelector('#editor').dataset.uri = f.uri;   // save back to the same file later
}

ध्यान दें: एक समय में सिर्फ़ एक पिक खुल सकता है: पिक खुला होने पर की गई कॉल तुरंत error:'busy' के साथ जवाब पाती है, और खुला पिक अपना जवाब रखता है। अनुमति (पढ़ना और लिखना) तब तक रहती है जब तक आप releaseFileAccess(uri) कॉल न करें। फ़ाइल यहाँ पढ़ी नहीं जाती - इसके लिए readFileBase64 / WebToApkFS.readBytes इस्तेमाल करें uri.

readFileBase64 ब्रिज#

window.WebToApk.readFileBase64(pathOrUri: String, offset: Long, length: Long): String

[offset] से शुरू करके [length] तक कच्चे बाइट पढ़ता है और base64 में लौटाता है। @param pathOrUri [pickFile] से मिला `content://` URI, या [requestFolderAccess] से दिए फ़ोल्डर का सापेक्ष पाथ। @param length पढ़ने के बाइट; हर कॉल में 4 MB तक सीमित। 0 या ऋणात्मक का मतलब "जितना एक कॉल में हो सके"। @return JSON `{ok, base64, bytesRead, offset, size, eof, error}`। `eof` चंक लूप को बताता है कि कब रुकना है, उसे खुद `size` से तुलना किए बिना।

उदाहरण

फ़ाइल में किसी बाइट ऑफ़सेट से कच्चे बाइट पढ़ता है और base64 में लौटाता है। पेज आमतौर पर WebToApkFS.readBytes(pathOrUri, offset, length) इस्तेमाल करते हैं (वही नतीजा, पहले से पार्स किया हुआ) या WebToApkFS.readAll(pathOrUri).

लौटाता है: (sync) एक JSON स्ट्रिंग {ok:true, base64, bytesRead, offset, size, eof} या {ok:false, error}। ज़रूरत: बिल्ड करते समय चरण 4 (Access) में Native Folder Access (SAF) पर टिक करें।

सार्वजनिक तरीका:

const r = WebToApkFS.readBytes(fileUri, 0, 65536);
if (r.ok) console.log(r.bytesRead, 'bytes read, end of file:', r.eof);

कच्चा कॉल - ध्यान दें कि नतीजा एक स्ट्रिंग है जिसे आपको JSON.parse करना होगा, सीधा base64 नहीं:

if (window.WebToApk && typeof WebToApk.readFileBase64 === 'function') {
  const r = JSON.parse(WebToApk.readFileBase64(fileUri, 0, 0));   // length 0 = up to 4 MB
  if (r.ok) {
    const bytes = Uint8Array.from(atob(r.base64), function (c) { return c.charCodeAt(0); });
    console.log('first byte', bytes[0], 'of', r.size);
  } else {
    console.log('read failed:', r.error);
  }
}

सिर्फ़ पहले 8 बाइट पढ़कर जाँचें कि चुनी हुई फ़ाइल सच में PNG है:

function isPng(fileUri) {
  const r = WebToApkFS.readBytes(fileUri, 0, 8);
  if (!r.ok || r.bytesRead < 8) return false;
  const head = atob(r.base64);
  const sig = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a];
  return sig.every(function (b, i) { return head.charCodeAt(i) === b; });
}

बड़ी फ़ाइल को प्रोग्रेस बार के साथ टुकड़ों में पढ़ें, उसे कभी पूरा मेमोरी में रखे बिना:

function scanInChunks(fileUri, onChunk) {
  const step = WebToApkFS.maxChunkSize() || 1048576;
  let offset = 0;
  for (;;) {
    const r = WebToApkFS.readBytes(fileUri, offset, step);
    if (!r.ok) throw new Error('read failed: ' + r.error);
    onChunk(r.base64);                                    // process this piece, then drop it
    offset += r.bytesRead;
    document.querySelector('#progress').value = r.size ? offset / r.size : 1;
    if (r.eof || r.bytesRead <= 0) break;
  }
  return offset;                                          // total bytes read
}

ध्यान दें: pathOrUri pickFile से मिला content:// URI है या requestFolderAccess से मिले फ़ोल्डर के सापेक्ष पाथ। एक कॉल में अधिकतम 4 MB (getMaxIoChunkSize()); eof तक bytesRead पर लूप चलाएँ। त्रुटियाँ: not-found, disabled, cannot-open, offset must be >= 0, offset past end of file (N bytes), foreign_origin (ऐप की अपनी साइट से अलग साइट का पेज)।

releaseFileAccess ब्रिज#

window.WebToApk.releaseFileAccess(uriString: String): String

[pickFile] से ली गई स्थायी सिंगल-फ़ाइल अनुमति को छोड़ता है। Android इस पर सीमा रखता है कि कोई ऐप कितनी URI अनुमतियाँ स्थायी रख सकता है, इसलिए जो ऐप कई फ़ाइलें चुनता है उसे पूरी हो चुकी फ़ाइलों की अनुमति छोड़ देनी चाहिए।

उदाहरण

pickFile से चुनी गई फ़ाइल के लिए रखी गई पढ़ने/लिखने की अनुमति छोड़ता है। पेज आमतौर पर WebToApkFS.releaseFile(uri) इस्तेमाल करते हैं, जो असली बूलियन लौटाता है।

लौटाता है: (सिंक) स्ट्रिंग "true" या "false"। ज़रूरी: बिल्ड करते समय चरण 4 (Access) में Native Folder Access (SAF) टिक करें (वरना हमेशा "false")।

सार्वजनिक तरीका:

if (window.WebToApkFS) WebToApkFS.releaseFile(fileUri);

सीधा कॉल:

if (window.WebToApk && typeof WebToApk.releaseFileAccess === 'function') {
  const released = WebToApk.releaseFileAccess(fileUri) === 'true';   // a string, not a boolean
  console.log(released ? 'access released' : 'nothing to release');
}

ज़्यादा से ज़्यादा 10 की "हाल की फ़ाइलें" सूची रखें और जो फ़ाइल सूची से बाहर हो जाए उसकी अनुमति छोड़ दें:

function rememberRecent(uri) {
  let recent = JSON.parse(localStorage.getItem('recentFiles') || '[]');
  recent = [uri].concat(recent.filter(function (u) { return u !== uri; }));
  while (recent.length > 10) {
    const dropped = recent.pop();
    if (window.WebToApkFS) WebToApkFS.releaseFile(dropped);   // Android caps kept grants
  }
  localStorage.setItem('recentFiles', JSON.stringify(recent));
}

मौजूदा दस्तावेज़ बंद करें: उसकी पहुँच छोड़ें और सेव किया URI भुला दें:

document.querySelector('#close-doc').addEventListener('click', function () {
  const uri = localStorage.getItem('openDocUri');
  if (uri && window.WebToApkFS) {
    const released = WebToApkFS.releaseFile(uri);   // true / false, already a boolean
    console.log(released ? 'grant released' : 'no grant was held for this file');
  }
  localStorage.removeItem('openDocUri');
  document.querySelector('#editor').value = '';
});

ध्यान दें: Android सीमित करता है कि एक ऐप कितनी फ़ाइलों की अनुमति रख सकता है, इसलिए जो फ़ाइलें अब नहीं चाहिए उन्हें छोड़ दें। फ़ाइल को खुद छुआ नहीं जाता।

statFile ब्रिज#

window.WebToApk.statFile(pathOrUri: String): String

फ़ाइल या फ़ोल्डर का मेटाडेटा, उसका कुछ भी पढ़े बिना। सापेक्ष पाथ दिए गए फ़ोल्डर की किसी फ़ाइल या सब-फ़ोल्डर का नाम बताता है; फ़ोल्डर `isDirectory: true` के साथ `size` 0 और `mime` `vnd.android.document/directory` बताता है। @return JSON `{ok, name, size, mime, lastModified, uri, isDirectory, canWrite}`, या `{ok:false, error}` जिसमें `not-found`, `disabled` या `foreign_origin`।

उदाहरण

फ़ाइल का मेटाडेटा (नाम, साइज़, टाइप) उसके बाइट पढ़े बिना पढ़ता है। पेज आमतौर पर WebToApkFS.stat(pathOrUri) इस्तेमाल करते हैं, जो JSON आपके लिए पार्स कर देता है।

लौटाता है: (तुरंत) एक JSON स्ट्रिंग {ok, name, size, mime, lastModified, uri, isDirectory, canWrite}, या {ok:false, error} ('not-found', 'disabled', 'foreign_origin')। ज़रूरत: बिल्ड करते समय चरण 4 (Access) में Native Folder Access (SAF) टिक करें।

सार्वजनिक तरीका:

const info = WebToApkFS.stat(fileUri);
if (info.ok) console.log(info.name, info.size);

सीधा कॉल:

if (window.WebToApk && typeof WebToApk.statFile === 'function') {
  const info = JSON.parse(WebToApk.statFile(fileUri));   // or 'notes/today.txt' in the granted folder
  if (info.ok) console.log(info.name, info.size + ' bytes', info.mime, info.canWrite);
  else console.log('stat failed:', info.error);
}

बहुत बड़ी फ़ाइल को उसका कुछ भी पढ़े बिना पहले ही मना कर दें:

const MAX_BYTES = 20 * 1024 * 1024;
function canImport(fileUri) {
  const info = WebToApkFS.stat(fileUri);
  if (!info.ok) { alert('File not available: ' + info.error); return false; }
  if (info.size > MAX_BYTES) {
    alert(info.name + ' is ' + Math.round(info.size / 1048576) + ' MB; the limit is 20 MB.');
    return false;
  }
  return true;
}

दिए गए फ़ोल्डर का सब-फ़ोल्डर खोजें और "Save here" तभी दिखाएँ जब वह लिखने योग्य हो:

function checkExportFolder() {
  const info = WebToApkFS.stat('exports');          // a path relative to the granted folder
  const btn = document.querySelector('#save-here');
  if (!info.ok) {                                    // 'not-found' = the folder is not there yet
    btn.textContent = 'Create "exports" and save';
    return WebToApkFS.mkdir('exports');
  }
  btn.disabled = !(info.isDirectory && info.canWrite);
  console.log('exports last changed', new Date(info.lastModified).toLocaleString());
  return true;
}

ध्यान दें: pickFile से मिला content:// URI या requestFolderAccess से दिए गए फ़ोल्डर के सापेक्ष पाथ लेता है। यह सिर्फ़ ऐप के अपने पेजों या उसकी कॉन्फ़िगर की गई वेबसाइट को जवाब देता है; किसी और साइट के पेज को मिलता है foreign_origin.

writeFileBase64 ब्रिज#

window.WebToApk.writeFileBase64(pathOrUri: String, base64: String, offset: Long, mode: String): String

कच्चे बाइट लिखता है और बताता है कितने पहुँचे। @param mode `"patch"` [offset] पर लिखता है और बाकी फ़ाइल को जैसा है वैसा रहने देता है (कई बार ओवरराइट करने वाले काम के लिए), `"truncate"` फ़ाइल बदल देता है, `"append"` [offset] को अनदेखा करके अंत में लिखता है। @return JSON `{ok, bytesWritten, offset, size, error}` - `size` fsync के बाद फ़ाइल से दोबारा पढ़ा जाता है, ताकि कॉल करने वाला बूलियन पर भरोसा करने के बजाय लिखने को सच में जाँच सके।

उदाहरण

base64 बाइट फ़ाइल में किसी offset पर (patch), पूरी फ़ाइल पर (truncate), या अंत में (append) लिखता है। पेज आमतौर पर WebToApkFS.writeBytes(pathOrUri, base64, offset, mode) या इस्तेमाल करते हैं WebToApkFS.writeAll(pathOrUri, base64).

लौटाता है: (तुरंत) एक JSON स्ट्रिंग {ok, bytesWritten, offset, size} (size fsync के बाद दोबारा पढ़ा गया) या {ok:false, error}। ज़रूरत: बिल्ड करते समय चरण 4 (Access) में Native Folder Access (SAF) टिक करें।

सार्वजनिक तरीका:

const r = WebToApkFS.writeBytes(fileUri, btoa('hello'), 0, 'truncate');
if (r.ok) console.log('saved', r.bytesWritten, 'bytes');

सीधा कॉल:

if (window.WebToApk && typeof WebToApk.writeFileBase64 === 'function') {
  const r = JSON.parse(WebToApk.writeFileBase64('notes/today.txt', btoa('Buy milk\n'), 0, 'append'));
  if (!r.ok) console.log('write failed:', r.error);
  else console.log('file is now', r.size, 'bytes');
}

कैनवस ड्रॉइंग को यूज़र की चुनी फ़ाइल में PNG के रूप में सेव करें, जो वहाँ था उसे बदलकर:

document.querySelector('#save').addEventListener('click', async function () {
  const f = await WebToApkFS.pickFile('image/png');
  if (!f.ok) return;
  const base64 = document.querySelector('canvas').toDataURL('image/png').split(',')[1];
  const r = WebToApkFS.writeAll(f.uri, base64);   // truncates, then writes in 4 MB chunks
  if (r.ok) console.log('saved', r.bytesWritten, 'bytes to', f.name);
  else alert('Save failed: ' + r.error);
});

किसी फ़ाइल को मिटाने से पहले उसी जगह शून्यों से ओवरराइट करें, और पक्का करें कि हर पास पूरा हुआ:

function zeroFill(fileUri) {
  const info = WebToApkFS.stat(fileUri);
  if (!info.ok) return false;
  const slice = 32768;
  for (let at = 0; at < info.size; at += slice) {
    const len = Math.min(slice, info.size - at);
    const zeros = btoa(String.fromCharCode.apply(null, new Uint8Array(len)));
    const w = WebToApkFS.writeBytes(fileUri, zeros, at, 'patch');   // the rest of the file is kept
    if (!w.ok || w.bytesWritten !== len) { console.log('write failed at', at, w.error); return false; }
  }
  return true;   // each call is fsync'ed before it answers
}

ध्यान दें: मोड 'patch' (खाली होने पर डिफ़ॉल्ट), 'truncate' या 'append' है। एक कॉल में ज़्यादा से ज़्यादा 4 MB डिकोड किए बाइट लिए जाते हैं (getMaxIoChunkSize())। सापेक्ष पाथ, मौजूद न होने पर, दिए गए फ़ोल्डर के अंदर बनता है। त्रुटियाँ: disabled, foreign_origin (स्क्रीन पर दिखता पेज ऐप का अपना नहीं है), bad mode: x, offset must be >= 0, bad base64, chunk too large (max N bytes), not-found, cannot-open, short write.

हर डॉक्स बिल्ड पर ऐप रनटाइम और उसकी उदाहरण फ़ाइलों से बनाया जाता है। Markdown में पढ़ें · सभी फ़ैमिली.

2026-10-03 को शिप किए गए ब्रिज से मिलाकर जाँचा गया।