@octane-xplat/files 0.0.1 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Alec Larson
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,32 @@
1
- # @octane-xplat/files
1
+ # `@octane-xplat/files`
2
2
 
3
- Version 0.0.1 is a reservation stub so the automated release can publish this package name. It has no supported API. Use a later release for the implementation.
3
+ File pick, read, and write for Octane xplat apps. iOS/Android run
4
+ `@nativescript-community/ui-document-picker` plus `@nativescript/core`
5
+ filesystem APIs (including `content://` URIs); web uses an `<input
6
+ type="file">` pick and downloads for writes; Linux goes through the desktop
7
+ host bridge (`Gtk.FileDialog`) and falls back to the web flow in a plain
8
+ browser; the macOS AppKit dev host is not wired and throws `unsupported`.
9
+
10
+ ```sh
11
+ pnpm add @octane-xplat/files
12
+ ```
13
+
14
+ ```ts
15
+ import { files } from '@octane-xplat/files'
16
+
17
+ const ref = await files.pick('image/*')
18
+ if (ref) {
19
+ const text = await files.readText(ref)
20
+ files.release(ref)
21
+ }
22
+
23
+ // web: triggers a download; native: writes into the app documents folder
24
+ const out = await files.writeText('packing-list.txt', text)
25
+ ```
26
+
27
+ `FileRef.uri` is opaque — a blob/object URL on web, a filesystem path or
28
+ `content://` URI on native. Pass it back to `files.readText`; do not parse
29
+ it. `release(ref)` cleans up native temp picks and is a no-op elsewhere.
30
+
31
+ Guide: [Using device features](../../docs/platform-services.md);
32
+ per-target availability: [platform notes](../../docs/platform-notes.md).
package/package.json CHANGED
@@ -1,19 +1,70 @@
1
1
  {
2
2
  "name": "@octane-xplat/files",
3
- "version": "0.0.1",
3
+ "version": "0.9.0",
4
4
  "description": "Native + web file pick and read/write for Octane xplat — matching web + NativeScript implementations",
5
+ "keywords": [
6
+ "android",
7
+ "cross-platform",
8
+ "document-picker",
9
+ "file-picker",
10
+ "files",
11
+ "ios",
12
+ "nativescript",
13
+ "octane",
14
+ "web",
15
+ "xplat"
16
+ ],
17
+ "homepage": "https://github.com/octane-xplat/octane-xplat/tree/main/packages/files",
5
18
  "license": "MIT",
6
19
  "repository": {
7
20
  "type": "git",
8
- "url": "https://github.com/octane-xplat/octane-xplat.git",
21
+ "url": "https://github.com/octane-xplat/octane-xplat",
9
22
  "directory": "packages/files"
10
23
  },
11
- "homepage": "https://github.com/octane-xplat/octane-xplat/tree/main/packages/files",
12
- "main": "./index.js",
13
- "types": "./index.d.ts",
14
24
  "files": [
15
- "README.md",
16
- "index.js",
17
- "index.d.ts"
18
- ]
19
- }
25
+ "src"
26
+ ],
27
+ "type": "module",
28
+ "exports": {
29
+ ".": {
30
+ "macos": "./src/index.macos.ts",
31
+ "default": "./src/index.ts"
32
+ },
33
+ "./*": "./src/*"
34
+ },
35
+ "publishConfig": {
36
+ "access": "public"
37
+ },
38
+ "dependencies": {
39
+ "@nativescript-community/ui-document-picker": "1.1.29",
40
+ "@octane-xplat/platform": "0.9.0"
41
+ },
42
+ "devDependencies": {
43
+ "@nativescript-community/octane": "0.2.4",
44
+ "@nativescript/core": "9.1.2",
45
+ "@nativescript/types": "9.0.0",
46
+ "octane": "0.6.3"
47
+ },
48
+ "peerDependencies": {
49
+ "@nativescript-community/octane": ">=0.2.1 <1",
50
+ "@nativescript/core": ">=9.1.0 <10",
51
+ "octane": ">=0.6.3 <1"
52
+ },
53
+ "peerDependenciesMeta": {
54
+ "@nativescript-community/octane": {
55
+ "optional": true
56
+ },
57
+ "@nativescript/core": {
58
+ "optional": true
59
+ }
60
+ },
61
+ "nativescript": {
62
+ "platforms": {
63
+ "android": "9.1.0",
64
+ "ios": "9.1.0"
65
+ }
66
+ },
67
+ "scripts": {
68
+ "test:packed": "node tests/packed-consumer.mjs"
69
+ }
70
+ }
@@ -0,0 +1,2 @@
1
+ // Linux's system webview uses the shared host-aware web implementation.
2
+ export { files } from './files.web'
@@ -0,0 +1,18 @@
1
+ // Files — AppKit host leaf. The dev host has no picker or save panel wired.
2
+ import type { FileRef } from './types'
3
+
4
+ export const files = {
5
+ async pick(): Promise<FileRef | null> {
6
+ throw new Error('unsupported: the AppKit host does not provide a file picker')
7
+ },
8
+ async pickMultiple(): Promise<FileRef[]> {
9
+ throw new Error('unsupported: the AppKit host does not provide a file picker')
10
+ },
11
+ async readText(_file: FileRef): Promise<string> {
12
+ throw new Error('unsupported: AppKit file access is not wired')
13
+ },
14
+ async writeText(_name: string, _text: string): Promise<FileRef> {
15
+ throw new Error('unsupported: the AppKit host does not provide a save panel')
16
+ },
17
+ release(_file: FileRef): void {},
18
+ }
package/src/files.ts ADDED
@@ -0,0 +1,133 @@
1
+ // Files — native leaf. FileRef wraps an opaque native URI. App-document paths
2
+ // are still used for writes; picked Android SAF references may be content://
3
+ // URIs and must be read through ContentResolver.
4
+ import { openFilePicker } from '@nativescript-community/ui-document-picker'
5
+ import { Application, File, knownFolders, path } from '@nativescript/core'
6
+ import type { FileRef } from './types'
7
+
8
+ const docs = () => knownFolders.documents()
9
+
10
+ function pickerTypes(accept: string): { extensions: string[]; mimeTypes: string[] } {
11
+ const values = accept
12
+ .split(',')
13
+ .map((value) => value.trim())
14
+ .filter(Boolean)
15
+
16
+ return {
17
+ extensions: values.filter((value) => value.startsWith('.')).map((value) => value.slice(1)),
18
+ // `*/*` is the default, not a useful iOS UTType MIME value. Leaving it
19
+ // out lets the upstream picker use its public.data fallback.
20
+ mimeTypes: values.filter((value) => value.includes('/') && value !== '*/*'),
21
+ }
22
+ }
23
+
24
+ function androidDisplayName(uri: string, nativeUri: any): string | undefined {
25
+ if (!Application.android || !uri.startsWith('content://')) {
26
+ return undefined
27
+ }
28
+
29
+ try {
30
+ const activity = Application.android.foregroundActivity ?? Application.android.startActivity
31
+ // Android accepts null for projection/filter args; its generated
32
+ // declarations omit that nullable bridge contract.
33
+ const noColumns = null as unknown as string[]
34
+ const noFilter = null as unknown as string
35
+ const cursor = activity
36
+ ?.getContentResolver()
37
+ ?.query(nativeUri ?? android.net.Uri.parse(uri), noColumns, noFilter, noColumns, noFilter)
38
+
39
+ if (!cursor) {
40
+ return undefined
41
+ }
42
+
43
+ try {
44
+ if (!cursor.moveToFirst()) {
45
+ return undefined
46
+ }
47
+
48
+ const index = cursor.getColumnIndex('display_name')
49
+ return index >= 0 ? cursor.getString(index) : undefined
50
+ } finally {
51
+ cursor.close()
52
+ }
53
+ } catch {
54
+ return undefined
55
+ }
56
+ }
57
+
58
+ function fallbackName(uri: string): string {
59
+ const segment = uri.split('/').pop() ?? ''
60
+ try {
61
+ return decodeURIComponent(segment) || 'document'
62
+ } catch {
63
+ return segment || 'document'
64
+ }
65
+ }
66
+
67
+ async function readContentUri(uri: string): Promise<string> {
68
+ const activity = Application.android?.foregroundActivity ?? Application.android?.startActivity
69
+ const input = activity?.getContentResolver()?.openInputStream(android.net.Uri.parse(uri))
70
+
71
+ if (!input) {
72
+ throw new Error(`Unable to open picked file: ${uri}`)
73
+ }
74
+
75
+ const reader = new java.io.BufferedReader(new java.io.InputStreamReader(input))
76
+ const lines: string[] = []
77
+ try {
78
+ let line: string | null
79
+ while ((line = reader.readLine()) !== null) {
80
+ lines.push(line)
81
+ }
82
+
83
+ return lines.join('\n')
84
+ } finally {
85
+ reader.close()
86
+ }
87
+ }
88
+
89
+ export const files = {
90
+ /** Opens the platform document picker and returns its opaque native URI. */
91
+ async pick(accept = '*/*', opts?: { startingFolder?: string }): Promise<FileRef | null> {
92
+ const { extensions, mimeTypes } = pickerTypes(accept)
93
+ const result = await openFilePicker({
94
+ extensions,
95
+ mimeTypes,
96
+ multipleSelection: false,
97
+ permissions: { read: true, persistable: true },
98
+ startingFolder: opts?.startingFolder,
99
+ })
100
+
101
+ const uri = result.files?.[0]
102
+
103
+ if (!uri) {
104
+ return null
105
+ }
106
+
107
+ const name = androidDisplayName(uri, result.android) ?? fallbackName(uri)
108
+ return { name, uri }
109
+ },
110
+ async readText(ref: FileRef): Promise<string> {
111
+ if (ref.uri.startsWith('content://')) {
112
+ return readContentUri(ref.uri)
113
+ }
114
+
115
+ return File.fromPath(ref.uri).readText()
116
+ },
117
+ async writeText(name: string, text: string): Promise<FileRef> {
118
+ const p = path.join(docs().path, name)
119
+ const f = File.fromPath(p)
120
+ f.writeTextSync(text)
121
+ return { name, uri: p }
122
+ },
123
+ release(ref: FileRef): void {
124
+ const cachePrefix = knownFolders.temp().path + '/'
125
+ if (!ref.uri.startsWith(cachePrefix)) {
126
+ return
127
+ }
128
+
129
+ try {
130
+ File.fromPath(ref.uri).removeSync()
131
+ } catch {}
132
+ },
133
+ }
@@ -0,0 +1,63 @@
1
+ // Files — web leaf. Desktop webview hosts own real pickers and file:// refs;
2
+ // outside a host this uses opaque object URLs and browser downloads.
3
+ import { desktopHost } from '@octane-xplat/platform/host/web'
4
+ import type { FileRef } from './types'
5
+
6
+ export const files = {
7
+ async pick(accept = '*/*', opts?: { startingFolder?: string }): Promise<FileRef | null> {
8
+ const host = desktopHost()
9
+ if (host && (await host.supports('files', 'pick'))) {
10
+ return host.files.pick(accept, { startingFolder: opts?.startingFolder })
11
+ }
12
+
13
+ return new Promise((resolve) => {
14
+ const input = document.createElement('input')
15
+ input.type = 'file'
16
+ input.accept = accept
17
+ input.onchange = () => {
18
+ const f = input.files?.[0]
19
+ resolve(f ? { name: f.name, uri: URL.createObjectURL(f) } : null)
20
+ }
21
+
22
+ input.oncancel = () => resolve(null)
23
+ input.click()
24
+ })
25
+ },
26
+ async readText(ref: FileRef): Promise<string> {
27
+ const host = desktopHost()
28
+ if (host && (await host.supports('files', 'readText'))) {
29
+ const text = await host.files.readText(ref.uri)
30
+ if (text === null) {
31
+ throw new Error(`Unable to read host file: ${ref.uri}`)
32
+ }
33
+
34
+ return text
35
+ }
36
+
37
+ return (await fetch(ref.uri)).text()
38
+ },
39
+ /** Desktop hosts write the selected path; plain web starts a download. */
40
+ async writeText(name: string, text: string): Promise<FileRef> {
41
+ const host = desktopHost()
42
+ if (host && (await host.supports('files', 'writeText'))) {
43
+ const file = await host.files.writeText(name, text)
44
+ if (!file) {
45
+ throw new Error('Host file save was cancelled')
46
+ }
47
+
48
+ return file
49
+ }
50
+
51
+ const uri = URL.createObjectURL(new Blob([text], { type: 'text/plain' }))
52
+ const a = document.createElement('a')
53
+ a.href = uri
54
+ a.download = name
55
+ a.click()
56
+ return { name, uri }
57
+ },
58
+ release(ref: FileRef): void {
59
+ if (ref.uri.startsWith('blob:')) {
60
+ URL.revokeObjectURL(ref.uri)
61
+ }
62
+ },
63
+ }
@@ -0,0 +1,2 @@
1
+ export * from './types'
2
+ export { files } from './files.macos'
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export * from './types'
2
+ export { files } from './files'
package/src/types.ts ADDED
@@ -0,0 +1,8 @@
1
+ export interface FileRef {
2
+ name: string
3
+ /**
4
+ * Opaque reference: blob/object URL on web, filesystem path on native.
5
+ * `files.writeText()` downloads on web and writes a file on native.
6
+ */
7
+ uri: string
8
+ }
package/index.d.ts DELETED
@@ -1 +0,0 @@
1
- export {}
package/index.js DELETED
@@ -1,2 +0,0 @@
1
- // Reservation stub; implementation will arrive in a later release.
2
- module.exports = {}