UNPKG

@ayonli/jsext

Version:

A JavaScript extension package for building strong and modern applications.

554 lines (550 loc) 19.9 kB
'use strict'; var env = require('../env.js'); var runtime = require('../runtime.js'); var reader = require('../reader.js'); var async = require('../async.js'); var filetype = require('../filetype.js'); var fs = require('../fs.js'); var fs_util = require('../fs/util.js'); var object = require('../object.js'); var path = require('../path.js'); var event = require('../event.js'); var dialog_progress = require('./progress.js'); /** * Opens the file picker dialog and pick a file, this function returns the * file's path or a `FileSystemFileHandle` in the browser. * * NOTE: Browser support is limited to the chromium-based browsers. * * @example * ```ts * // default usage * import { pickFile } from "@ayonli/jsext/dialog"; * * // Node.js, Deno, Bun * const filename = await pickFile() as string | null; * * // Browser (Chrome) * const handle = await pickFile() as FileSystemFileHandle | null; * ``` * * @example * ```ts * // filter by MIME type * import { pickFile } from "@ayonli/jsext/dialog"; * * // Node.js, Deno, Bun * const filename = await pickFile({ type: "image/*" }) as string | null; * * // Browser (Chrome) * const handle = await pickFile({ type: "image/*" }) as FileSystemFileHandle | null; * ``` * * @example * ```ts * // pick for save * import { pickFile } from "@ayonli/jsext/dialog"; * * // Node.js, Deno, Bun * const filename = await pickFile({ * forSave: true, * defaultName: "hello.txt", * }) as string | null; * * // Browser (Chrome) * const handle = await pickFile({ * forSave: true, * defaultName: "hello.txt", * }) as FileSystemFileHandle | null; * ``` */ async function pickFile(options = {}) { if (typeof globalThis["showOpenFilePicker"] === "function") { const { browserPickFile } = await Promise.resolve().then(function () { return require('./browser/file.js'); }); return await browserPickFile(options.type, { forSave: options.forSave, defaultName: options.defaultName, }); } else if (env.isDeno || env.isNodeLike) { const { isWSL, which } = await Promise.resolve().then(function () { return require('../cli.js'); }); const _platform = runtime.platform(); if (_platform === "darwin") { const { macPickFile } = await Promise.resolve().then(function () { return require('./terminal/file/mac.js'); }); return await macPickFile(options.title, { type: options.type, forSave: options === null || options === void 0 ? void 0 : options.forSave, defaultName: options === null || options === void 0 ? void 0 : options.defaultName, }); } else if (_platform === "windows" || isWSL()) { const { windowsPickFile } = await Promise.resolve().then(function () { return require('./terminal/file/windows.js'); }); return await windowsPickFile(options.title, { type: options.type, forSave: options === null || options === void 0 ? void 0 : options.forSave, defaultName: options === null || options === void 0 ? void 0 : options.defaultName, }); } else if (_platform === "linux" || await which("zenity")) { const { linuxPickFile } = await Promise.resolve().then(function () { return require('./terminal/file/linux.js'); }); return await linuxPickFile(options.title, { type: options.type, forSave: options === null || options === void 0 ? void 0 : options.forSave, defaultName: options === null || options === void 0 ? void 0 : options.defaultName, }); } } throw new Error("Unsupported platform"); } /** * Opens the file picker dialog and pick multiple files, this function returns * the paths or `FileSystemFileHandle` objects in the browser of the files * selected. * * NOTE: Browser support is limited to the chromium-based browsers. * * @example * ```ts * // default usage * import { pickFiles } from "@ayonli/jsext/dialog"; * * // Node.js, Deno, Bun * const filenames = await pickFiles() as string[]; * * // Browser (Chrome) * const handles = await pickFiles() as FileSystemFileHandle[]; * ``` * * @example * ```ts * // filter by MIME type * import { pickFiles } from "@ayonli/jsext/dialog"; * * // Node.js, Deno, Bun * const filenames = await pickFiles({ type: "image/*" }) as string[]; * * // Browser (Chrome) * const handles = await pickFiles({ type: "image/*" }) as FileSystemFileHandle[]; * ``` */ async function pickFiles(options = {}) { if (typeof globalThis["showOpenFilePicker"] === "function") { const { browserPickFiles } = await Promise.resolve().then(function () { return require('./browser/file.js'); }); return await browserPickFiles(options.type); } else if (env.isDeno || env.isNodeLike) { const { isWSL, which } = await Promise.resolve().then(function () { return require('../cli.js'); }); const _platform = runtime.platform(); if (_platform === "darwin") { const { macPickFiles } = await Promise.resolve().then(function () { return require('./terminal/file/mac.js'); }); return await macPickFiles(options.title, options.type); } else if (_platform === "windows" || isWSL()) { const { windowsPickFiles } = await Promise.resolve().then(function () { return require('./terminal/file/windows.js'); }); return await windowsPickFiles(options.title, options.type); } else if (_platform === "linux" || await which("zenity")) { const { linuxPickFiles } = await Promise.resolve().then(function () { return require('./terminal/file/linux.js'); }); return await linuxPickFiles(options.title, options.type); } } throw new Error("Unsupported platform"); } /** * Opens the file picker dialog and pick a directory, this function returns the * directory's path or `FileSystemDirectoryHandle` in the browser. * * NOTE: Browser support is limited to the chromium-based browsers. * * @example * ```ts * import { pickDirectory } from "@ayonli/jsext/dialog"; * * // Node.js, Deno, Bun * const dirname = await pickDirectory() as string | null; * * // Browser (Chrome) * const handle = await pickDirectory() as FileSystemDirectoryHandle | null; * ``` */ async function pickDirectory(options = {}) { if (typeof globalThis["showDirectoryPicker"] === "function") { const { browserPickFolder } = await Promise.resolve().then(function () { return require('./browser/file.js'); }); return await browserPickFolder(); } else if (env.isDeno || env.isNodeLike) { const { isWSL, which } = await Promise.resolve().then(function () { return require('../cli.js'); }); const _platform = runtime.platform(); if (_platform === "darwin") { const { macPickFolder } = await Promise.resolve().then(function () { return require('./terminal/file/mac.js'); }); return await macPickFolder(options.title); } else if (_platform === "windows" || isWSL()) { const { windowsPickFolder } = await Promise.resolve().then(function () { return require('./terminal/file/windows.js'); }); return await windowsPickFolder(options.title); } else if (_platform === "linux" || await which("zenity")) { const { linuxPickFolder } = await Promise.resolve().then(function () { return require('./terminal/file/linux.js'); }); return await linuxPickFolder(options.title); } } throw new Error("Unsupported platform"); } async function openFile(options = {}) { const { title = "", type = "", multiple = false, directory = false } = options; if (directory) { return await openDirectory({ title }); } else if (multiple) { return await openFiles({ title, type }); } if (typeof globalThis["showOpenFilePicker"] === "function") { const { browserPickFile } = await Promise.resolve().then(function () { return require('./browser/file.js'); }); const handle = await browserPickFile(type); return handle ? await handle.getFile().then(fs_util.fixFileType) : null; } else if (env.isBrowserWindow) { const input = document.createElement("input"); input.type = "file"; input.accept = type !== null && type !== void 0 ? type : ""; return await new Promise(resolve => { input.onchange = () => { var _a; const file = (_a = input.files) === null || _a === void 0 ? void 0 : _a[0]; resolve(file ? fs_util.fixFileType(file) : null); }; input.oncancel = () => { resolve(null); }; if (typeof input.showPicker === "function") { input.showPicker(); } else { input.click(); } }); } else if (env.isDeno || env.isNodeLike) { let filename = await pickFile({ title, type }); if (filename) { return await fs.readFileAsFile(filename); } else { return null; } } else { throw new Error("Unsupported runtime"); } } /** * Opens the file picker dialog and selects multiple files to open. * * @example * ```ts * // default usage * import { openFiles } from "@ayonli/jsext/dialog"; * * const files = await openFiles(); * * if (files.length > 0) { * console.log(`You selected: ${files.map(file => file.name).join(", ")}`); * } * ``` * * @example * ```ts * // filter by MIME type * import { openFiles } from "@ayonli/jsext/dialog"; * * const files = await openFiles({ type: "image/*" }); * * if (files.length > 0) { * console.log(`You selected: ${files.map(file => file.name).join(", ")}`); * console.assert(files.every(file => file.type.startsWith("image/"))); * } * ``` */ async function openFiles(options = {}) { if (typeof globalThis["showOpenFilePicker"] === "function") { const { browserPickFiles } = await Promise.resolve().then(function () { return require('./browser/file.js'); }); const handles = await browserPickFiles(options.type); const files = []; for (const handle of handles) { const file = await handle.getFile(); files.push(fs_util.fixFileType(file)); } return files; } else if (env.isBrowserWindow) { const input = document.createElement("input"); input.type = "file"; input.multiple = true; input.accept = options.type || ""; return await new Promise(resolve => { input.onchange = () => { const files = input.files; resolve(files ? [...files].map(fs_util.fixFileType) : []); }; input.oncancel = () => { resolve([]); }; if (typeof input.showPicker === "function") { input.showPicker(); } else { input.click(); } }); } else if (env.isDeno || env.isNodeLike) { const filenames = await pickFiles(options); return await Promise.all(filenames.map(path => fs.readFileAsFile(path))); } else { throw new Error("Unsupported runtime"); } } /** * Opens the directory picker dialog and selects all its files to open. * * @example * ```ts * import { openDirectory } from "@ayonli/jsext/dialog"; * * const files = await openDirectory(); * * for (const file of files) { * console.log(`File name: ${file.name}, path: ${file.webkitRelativePath}`); * } * ``` */ async function openDirectory(options = {}) { if (typeof globalThis["showDirectoryPicker"] === "function") { const { browserPickFolder } = await Promise.resolve().then(function () { return require('./browser/file.js'); }); const dir = await browserPickFolder(); const files = []; if (!dir) { return files; } for await (const entry of fs.readDir(dir, { recursive: true })) { if (entry.kind === "file") { const file = await entry.handle.getFile(); Object.defineProperty(file, "webkitRelativePath", { configurable: true, enumerable: true, writable: false, value: entry.relativePath.replace(/\\/g, "/"), }); files.push(fs_util.fixFileType(file)); } } return files; } else if (env.isBrowserWindow) { const input = document.createElement("input"); input.type = "file"; input.webkitdirectory = true; return await new Promise(resolve => { input.onchange = () => { const files = input.files; resolve(files ? [...files].map(fs_util.fixFileType) : []); }; input.oncancel = () => { resolve([]); }; if (typeof input.showPicker === "function") { input.showPicker(); } else { input.click(); } }); } else if (env.isDeno || env.isNodeLike) { const dirname = await pickDirectory(options); if (dirname) { const files = []; for await (const entry of fs.readDir(dirname, { recursive: true })) { if (entry.kind === "file") { const path$1 = path.join(dirname, entry.relativePath); const file = await fs.readFileAsFile(path$1); Object.defineProperty(file, "webkitRelativePath", { configurable: true, enumerable: true, writable: false, value: entry.relativePath.replace(/\\/g, "/"), }); files.push(fs_util.fixFileType(file)); } } return files; } else { return []; } } else { throw new Error("Unsupported runtime"); } } async function saveFile(file, options = {}) { var _a; if (env.isBrowserWindow) { const a = document.createElement("a"); if (file instanceof ReadableStream) { const type = options.type || "application/octet-stream"; a.href = await reader.readAsObjectURL(file, type); a.download = options.name || "Unnamed" + (filetype.getExtensions(type)[0] || ""); } else if (file instanceof File) { a.href = URL.createObjectURL(file); a.download = options.name || file.name || "Unnamed" + (filetype.getExtensions(file.type)[0] || ""); } else if (file instanceof Blob) { a.href = URL.createObjectURL(file); a.download = options.name || "Unnamed" + (filetype.getExtensions(file.type)[0] || ""); } else { const type = options.type || "application/octet-stream"; const blob = new Blob([file], { type }); a.href = URL.createObjectURL(blob); a.download = options.name || "Unnamed" + (filetype.getExtensions(type)[0] || ""); } a.click(); } else if (env.isDeno || env.isNodeLike) { const { title } = options; let filename; if (typeof Blob === "function" && file instanceof Blob) { filename = await pickFile({ title, type: options.type || file.type, forSave: true, defaultName: options.name || ((_a = object.as(file, File)) === null || _a === void 0 ? void 0 : _a.name), }); } else { filename = await pickFile({ title, type: options.type, forSave: true, defaultName: options.name, }); } if (filename) { await fs.writeFile(filename, file, object.pick(options, ["signal"])); } } else { throw new Error("Unsupported runtime"); } } /** * Downloads the file of the given URL to the file system. * * In the CLI, this function will open a dialog to let the user choose the * location where the file will be saved. In the browser, the file will be saved * to the default download location, or the browser will prompt the user to * choose a location. * * NOTE: This function depends on the Fetch API and Web Streams API, in Node.js, * it requires Node.js v18.0 or above. * * @example * ```ts * import { downloadFile } from "@ayonli/jsext/dialog"; * * await downloadFile("https://ayonli.github.io/jsext/README.md"); * ``` */ async function downloadFile(url, options = {}) { var _a; const src = typeof url === "object" ? url.href : url; const name = options.name || path.basename(src); if (env.isBrowserWindow) { const a = document.createElement("a"); a.href = src; a.download = name; a.click(); return; } else if (!env.isDeno && !env.isNodeLike || typeof fetch !== "function") { throw new Error("Unsupported runtime"); } const dest = await pickFile({ title: options.title, type: options.type, forSave: true, defaultName: name, }); if (!dest) // user canceled return; const task = async.asyncTask(); let signal = (_a = options.signal) !== null && _a !== void 0 ? _a : null; let result; let updateProgress; if (options.showProgress) { const ctrl = new AbortController(); signal = ctrl.signal; result = dialog_progress.default("Downloading...", async (set) => { updateProgress = set; return await task; }, () => { ctrl.abort(); throw new Error("Download canceled"); }); } else { result = task; } const res = await fetch(src, { signal }); if (!res.ok) { throw new Error(`Failed to download: ${src}`); } const size = parseInt(res.headers.get("Content-Length") || "0", 10); let stream = res.body; if (options.onProgress || options.showProgress) { const { onProgress } = options; let loaded = 0; const transform = new TransformStream({ transform(chunk, controller) { controller.enqueue(chunk); loaded += chunk.byteLength; if (onProgress) { try { onProgress === null || onProgress === void 0 ? void 0 : onProgress(event.createProgressEvent("progress", { lengthComputable: !!size, loaded, total: size !== null && size !== void 0 ? size : 0, })); } catch (_a) { // ignore } } if (updateProgress && size) { updateProgress({ percent: loaded / size, }); } }, }); stream = stream.pipeThrough(transform); } fs.writeFile(dest, stream, { signal: signal }).then(() => { task.resolve(); }).catch(err => { task.reject(err); }); await result; } exports.downloadFile = downloadFile; exports.openDirectory = openDirectory; exports.openFile = openFile; exports.openFiles = openFiles; exports.pickDirectory = pickDirectory; exports.pickFile = pickFile; exports.pickFiles = pickFiles; exports.saveFile = saveFile; //# sourceMappingURL=file.js.map