Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions packages/methods/sparkling-media/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,21 @@
},
"main": "dist/index.js",
"types": "dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"./web": {
"types": "./dist/src/web/index.d.ts",
"default": "./dist/src/web/index.js"
}
},
"typesVersions": {
"*": {
"web": ["dist/src/web/index.d.ts"]
}
},
"files": [
"index.ts",
"src",
Expand Down
128 changes: 128 additions & 0 deletions packages/methods/sparkling-media/src/web/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
// Copyright (c) 2025 TikTok Pte. Ltd.
// Licensed under the Apache License Version 2.0 that can be found in the
// LICENSE file in the root directory of this source tree.

import { registerWebMethod } from 'sparkling-method/web-registry';
import type { WebMethodHandler } from 'sparkling-method';

registerWebMethod('media.chooseMedia', (params, callback) => {
const data = params.data as Record<string, unknown> | null;
const mediaTypes = data?.mediaTypes as string[] | undefined;
const sourceType = data?.sourceType as string | undefined;
const maxCount = (data?.maxCount as number) ?? 1;

// Build accept string from mediaTypes
const acceptParts: string[] = [];
if (mediaTypes?.includes('image')) acceptParts.push('image/*');
if (mediaTypes?.includes('video')) acceptParts.push('video/*');
const accept = acceptParts.length ? acceptParts.join(',') : 'image/*,video/*';

const input = document.createElement('input');
input.type = 'file';
input.accept = accept;
input.multiple = maxCount > 1;

// Camera source: use capture attribute (mobile browsers only)
if (sourceType === 'camera') {
input.capture = (data?.cameraType as string) === 'front' ? 'user' : 'environment';
}

input.onchange = () => {
const files = Array.from(input.files ?? []);
if (!files.length) {
callback({ code: 0, msg: 'No file selected' });
return;
}

const results = files.slice(0, maxCount).map(file => ({
tempFilePath: URL.createObjectURL(file),
size: file.size,
type: file.type,
name: file.name,
}));

callback({ code: 1, msg: 'ok', data: results });
};

// Handle user cancelling the file picker
input.addEventListener('cancel', () => {
callback({ code: 0, msg: 'User cancelled' });
});

input.click();
});

registerWebMethod('media.downloadFile', (params, callback) => {
const data = params.data as Record<string, unknown> | null;
const url = data?.url as string | undefined;
const extension = data?.extension as string | undefined;
const headers = data?.header as Record<string, string> | undefined;

if (!url) {
callback({ code: 0, msg: 'url is required' });
return;
}

fetch(url, { headers })
.then(res => {
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.blob();
})
.then(blob => {
const objectUrl = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = objectUrl;
a.download = `download.${extension || 'bin'}`;
a.click();
URL.revokeObjectURL(objectUrl);
callback({ code: 1, msg: 'ok', data: { tempFilePath: objectUrl } });
})
.catch(e => {
callback({ code: 0, msg: `Download failed: ${e}` });
});
});

// Shared implementation for uploadFile and uploadImage (same API shape)
const uploadFileImpl: WebMethodHandler = (params, callback) => {
const data = params.data as Record<string, unknown> | null;
const url = data?.url as string | undefined;
const filePath = data?.filePath as string | undefined;
const headers = data?.header as Record<string, string> | undefined;
const extraParams = data?.params as Record<string, unknown> | undefined;

if (!url) {
callback({ code: 0, msg: 'url is required' });
return;
}

// filePath on web is a blob URL from chooseMedia
const filePromise = filePath
? fetch(filePath).then(r => r.blob())
: Promise.resolve(new Blob());

filePromise
.then(blob => {
const formData = new FormData();
formData.append('file', blob);
if (extraParams && typeof extraParams === 'object') {
for (const [k, v] of Object.entries(extraParams)) {
formData.append(k, String(v));
}
}
return fetch(url, { method: 'POST', headers, body: formData });
})
.then(res => {
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
})
.then(json => callback({ code: 1, msg: 'ok', data: json }))
.catch(e => callback({ code: 0, msg: `Upload failed: ${e}` }));
};

registerWebMethod('media.uploadFile', uploadFileImpl);
registerWebMethod('media.uploadImage', uploadFileImpl);

registerWebMethod('media.saveDataURL', (_params, callback) => {
// No web API to save directly to device photo album
callback({ code: 0, msg: 'media.saveDataURL is not supported on web' });
});
2 changes: 1 addition & 1 deletion packages/methods/sparkling-navigation/module.config.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "sparkling-navigation",
"platforms": ["android", "ios"],
"platforms": ["android", "ios", "web"],
"version": "1.0.0",
"description": "Router methods for navigation and page management in Sparkling apps",
"methods": {
Expand Down
15 changes: 15 additions & 0 deletions packages/methods/sparkling-navigation/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,21 @@
},
"main": "dist/index.js",
"types": "dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"./web": {
"types": "./dist/src/web/index.d.ts",
"default": "./dist/src/web/index.js"
}
},
"typesVersions": {
"*": {
"web": ["dist/src/web/index.d.ts"]
}
},
"files": [
"index.ts",
"src",
Expand Down
88 changes: 88 additions & 0 deletions packages/methods/sparkling-navigation/src/web/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// Copyright (c) 2022 TikTok Pte. Ltd.
// Licensed under the Apache License Version 2.0 that can be found in the
// LICENSE file in the root directory of this source tree.

import { registerWebMethod } from 'sparkling-method/web-registry';

/**
* How web navigation is actually performed. The default host drives the
* browser History API and assumes the Lynx app owns the whole page (e.g.
* `sparkling-web-shell`). Embedders that render Lynx cards inside a larger page
* (such as the go-web `<Go>` preview) can override this with `setRouterWebHost`
* so navigation stays scoped to the card instead of the top-level document.
*/
export interface RouterWebHost {
open(pageName: string, scheme: string): void;
close(): void;
}

const defaultHost: RouterWebHost = {
open(pageName, scheme) {
const state = { page: pageName, scheme };
window.history.pushState(state, '', `?page=${encodeURIComponent(pageName)}`);
// Notify a full-page host (e.g. the web shell) to swap its <lynx-view>.
window.dispatchEvent(
new CustomEvent('sparkling:navigate', { detail: { page: pageName, state } }),
);
},
close() {
window.history.back();
},
};

let host: RouterWebHost = defaultHost;

/**
* Override how `router.open` / `router.close` navigate on web. Call with a
* host that navigates within an embedded card to avoid touching global
* `window.history`.
*/
export function setRouterWebHost(next: RouterWebHost): void {
host = next;
}

/** Extract the target page name (no extension) from a router scheme. */
function parsePageName(scheme: string): string | null {
const url = new URL(scheme);
const bundleParam = url.searchParams.get('bundle');
const urlParam = url.searchParams.get('url');
if (urlParam) {
// Dev mode: url param is a full URL, extract the basename.
const urlPath = new URL(urlParam).pathname;
return urlPath.replace(/^\//, '').replace(/\.lynx\.bundle$/, '');
}
if (bundleParam) {
return bundleParam.replace(/\.lynx\.bundle$/, '');
}
return null;
}

registerWebMethod('router.open', (params, callback) => {
const scheme = (params.data as Record<string, unknown>)?.scheme as string | undefined;

if (!scheme) {
callback({ code: 0, msg: 'scheme is required' });
return;
}

try {
const pageName = parsePageName(scheme);
if (!pageName) {
callback({ code: 0, msg: 'No bundle or url param in scheme' });
return;
}
host.open(pageName, scheme);
callback({ code: 1, msg: 'ok' });
} catch (e) {
callback({ code: 0, msg: `Failed to parse scheme: ${e}` });
}
});

registerWebMethod('router.close', (_params, callback) => {
try {
host.close();
callback({ code: 1, msg: 'ok' });
} catch (e) {
callback({ code: 0, msg: `Failed to close: ${e}` });
}
});
15 changes: 15 additions & 0 deletions packages/methods/sparkling-storage/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,21 @@
},
"main": "dist/index.js",
"types": "dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"./web": {
"types": "./dist/src/web/index.d.ts",
"default": "./dist/src/web/index.js"
}
},
"typesVersions": {
"*": {
"web": ["dist/src/web/index.d.ts"]
}
},
"files": [
"index.ts",
"src",
Expand Down
67 changes: 67 additions & 0 deletions packages/methods/sparkling-storage/src/web/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// Copyright (c) 2022 TikTok Pte. Ltd.
// Licensed under the Apache License Version 2.0 that can be found in the
// LICENSE file in the root directory of this source tree.

import { registerWebMethod } from 'sparkling-method/web-registry';

function storageKey(key: string, biz?: string): string {
return biz ? `sparkling:${biz}:${key}` : `sparkling:${key}`;
}

registerWebMethod('storage.getItem', (params, callback) => {
const data = params.data as Record<string, unknown> | null;
const key = data?.key as string | undefined;
const biz = data?.biz as string | undefined;

if (!key) {
callback({ code: 0, msg: 'key is required' });
return;
}

try {
const value = localStorage.getItem(storageKey(key, biz));
callback({ code: 1, msg: 'ok', data: value });
} catch (e) {
callback({ code: 0, msg: `localStorage error: ${e}` });
}
});

registerWebMethod('storage.setItem', (params, callback) => {
const data = params.data as Record<string, unknown> | null;
const key = data?.key as string | undefined;
const value = data?.data;
const biz = data?.biz as string | undefined;

if (!key) {
callback({ code: 0, msg: 'key is required' });
return;
}

try {
localStorage.setItem(
storageKey(key, biz),
typeof value === 'string' ? value : JSON.stringify(value),
);
callback({ code: 1, msg: 'ok' });
} catch (e) {
callback({ code: 0, msg: `localStorage error: ${e}` });
}
});

registerWebMethod('storage.removeItem', (params, callback) => {
const data = params.data as Record<string, unknown> | null;
const key = data?.key as string | undefined;
const biz = data?.biz as string | undefined;

if (!key) {
callback({ code: 0, msg: 'key is required' });
return;
}

try {
localStorage.removeItem(storageKey(key, biz));
callback({ code: 1, msg: 'ok' });
} catch (e) {
callback({ code: 0, msg: `localStorage error: ${e}` });
}
});
15 changes: 15 additions & 0 deletions packages/sparkling-method/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,21 @@
},
"main": "dist/index.js",
"types": "dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"./web-registry": {
"types": "./dist/web-registry.d.ts",
"default": "./dist/web-registry.js"
}
},
"typesVersions": {
"*": {
"web-registry": ["dist/web-registry.d.ts"]
}
},
"files": [
"dist",
"ios",
Expand Down
Loading