feat(pwa): add install orientation setting (#900)
This commit is contained in:
+49
-6
@@ -27,6 +27,7 @@
|
||||
const defaultAppName = 'OpenChamber - AI Coding Assistant';
|
||||
const defaultShortName = 'OpenChamber';
|
||||
const pwaNameStorageKey = 'openchamber.pwaName';
|
||||
const pwaOrientationStorageKey = 'openchamber.pwaOrientation';
|
||||
const pwaRecentSessionsStorageKey = 'openchamber.pwaRecentSessions';
|
||||
|
||||
const normalizePwaName = (value, fallback) => {
|
||||
@@ -47,6 +48,13 @@
|
||||
return value.length > maxLength ? value.slice(0, maxLength) : value;
|
||||
};
|
||||
|
||||
const normalizePwaOrientation = (value, fallback = 'system') => {
|
||||
if (value === 'portrait' || value === 'landscape' || value === 'system') {
|
||||
return value;
|
||||
}
|
||||
return fallback;
|
||||
};
|
||||
|
||||
const getStoredInstallName = () => {
|
||||
try {
|
||||
const storedName = localStorage.getItem(pwaNameStorageKey);
|
||||
@@ -70,6 +78,25 @@
|
||||
return normalizedName || defaultAppName;
|
||||
};
|
||||
|
||||
const getStoredOrientation = () => {
|
||||
try {
|
||||
const storedOrientation = localStorage.getItem(pwaOrientationStorageKey);
|
||||
return normalizePwaOrientation(storedOrientation, 'system');
|
||||
} catch {
|
||||
return 'system';
|
||||
}
|
||||
};
|
||||
|
||||
const setStoredOrientation = (value) => {
|
||||
const normalizedOrientation = normalizePwaOrientation(value, 'system');
|
||||
try {
|
||||
localStorage.setItem(pwaOrientationStorageKey, normalizedOrientation);
|
||||
} catch {
|
||||
return 'system';
|
||||
}
|
||||
return normalizedOrientation;
|
||||
};
|
||||
|
||||
const getQueryInstallNameOverride = () => {
|
||||
try {
|
||||
const params = new URLSearchParams(location.search);
|
||||
@@ -158,8 +185,13 @@
|
||||
return shortcuts;
|
||||
};
|
||||
|
||||
const buildManifest = (appName, recentSessions) => {
|
||||
const buildManifest = (appName, recentSessions, orientationPreference) => {
|
||||
const shortName = appName === defaultAppName ? defaultShortName : truncate(appName, 30);
|
||||
const manifestOrientation = orientationPreference === 'portrait'
|
||||
? 'portrait-primary'
|
||||
: orientationPreference === 'landscape'
|
||||
? 'landscape-primary'
|
||||
: null;
|
||||
return {
|
||||
name: appName,
|
||||
short_name: shortName,
|
||||
@@ -168,7 +200,7 @@
|
||||
start_url: `${baseUrl}/`,
|
||||
scope: `${baseUrl}/`,
|
||||
display: 'standalone',
|
||||
orientation: 'portrait-primary',
|
||||
...(manifestOrientation ? { orientation: manifestOrientation } : {}),
|
||||
background_color: '#151313',
|
||||
theme_color: '#edb449',
|
||||
icons: [
|
||||
@@ -187,11 +219,14 @@
|
||||
};
|
||||
};
|
||||
|
||||
const buildManifestEndpointUrl = (installNameOverride = null) => {
|
||||
const buildManifestEndpointUrl = (installNameOverride = null, orientationOverride = null) => {
|
||||
const params = new URLSearchParams();
|
||||
if (typeof installNameOverride === 'string') {
|
||||
params.set('appName', installNameOverride);
|
||||
}
|
||||
if (typeof orientationOverride === 'string') {
|
||||
params.set('orientation', normalizePwaOrientation(orientationOverride, 'system'));
|
||||
}
|
||||
const search = params.toString();
|
||||
return `${baseUrl}/manifest.webmanifest${search ? `?${search}` : ''}`;
|
||||
};
|
||||
@@ -257,11 +292,14 @@
|
||||
}
|
||||
};
|
||||
|
||||
const updateManifest = async (installNameOverride = null) => {
|
||||
const updateManifest = async (installNameOverride = null, orientationOverride = null) => {
|
||||
const resolvedFallbackName = typeof installNameOverride === 'string' ? installNameOverride : getStoredInstallName();
|
||||
const resolvedOrientation = typeof orientationOverride === 'string'
|
||||
? normalizePwaOrientation(orientationOverride, 'system')
|
||||
: getStoredOrientation();
|
||||
const recentSessions = parseRecentSessionShortcuts();
|
||||
const manifest = buildManifest(resolvedFallbackName, recentSessions);
|
||||
const manifestUrl = buildManifestEndpointUrl(installNameOverride);
|
||||
const manifest = buildManifest(resolvedFallbackName, recentSessions, resolvedOrientation);
|
||||
const manifestUrl = buildManifestEndpointUrl(installNameOverride, resolvedOrientation);
|
||||
const requestVersion = ++manifestRequestVersion;
|
||||
|
||||
const useEndpoint = await canUseManifestEndpoint(manifestUrl, requestVersion);
|
||||
@@ -290,6 +328,11 @@
|
||||
void updateManifest(resolvedName);
|
||||
return resolvedName;
|
||||
};
|
||||
window.__OPENCHAMBER_SET_PWA_ORIENTATION__ = (value) => {
|
||||
const resolvedOrientation = setStoredOrientation(value);
|
||||
void updateManifest(null, resolvedOrientation);
|
||||
return resolvedOrientation;
|
||||
};
|
||||
window.__OPENCHAMBER_UPDATE_PWA_MANIFEST__ = () => {
|
||||
refreshManifestFromStorage();
|
||||
};
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
"display": "standalone",
|
||||
"background_color": "#151313",
|
||||
"theme_color": "#edb449",
|
||||
"orientation": "portrait-primary",
|
||||
"icons": [
|
||||
{ "src": "/pwa-192.png", "sizes": "192x192", "type": "image/png", "purpose": "any" },
|
||||
{ "src": "/pwa-512.png", "sizes": "512x512", "type": "image/png", "purpose": "any" },
|
||||
|
||||
@@ -279,6 +279,7 @@ const settingsHelpers = createSettingsHelpers({
|
||||
});
|
||||
|
||||
const normalizePwaAppName = (...args) => settingsHelpers.normalizePwaAppName(...args);
|
||||
const normalizePwaOrientation = (...args) => settingsHelpers.normalizePwaOrientation(...args);
|
||||
const sanitizeSettingsUpdate = (...args) => settingsHelpers.sanitizeSettingsUpdate(...args);
|
||||
const mergePersistedSettings = (...args) => settingsHelpers.mergePersistedSettings(...args);
|
||||
const formatSettingsResponse = (...args) => settingsHelpers.formatSettingsResponse(...args);
|
||||
@@ -759,6 +760,7 @@ const staticRoutesRuntime = createStaticRoutesRuntime({
|
||||
getOpenCodeAuthHeaders,
|
||||
readSettingsFromDiskMigrated,
|
||||
normalizePwaAppName,
|
||||
normalizePwaOrientation,
|
||||
});
|
||||
const featureRoutesRuntime = createFeatureRoutesRuntime({
|
||||
clientReloadDelayMs: CLIENT_RELOAD_DELAY_MS,
|
||||
|
||||
@@ -1,4 +1,13 @@
|
||||
const DEFAULT_PWA_APP_NAME = 'OpenChamber - AI Coding Assistant';
|
||||
const mapPwaOrientationToManifest = (value) => {
|
||||
if (value === 'portrait') {
|
||||
return 'portrait-primary';
|
||||
}
|
||||
if (value === 'landscape') {
|
||||
return 'landscape-primary';
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const registerPwaManifestRoute = (app, dependencies) => {
|
||||
const {
|
||||
@@ -8,6 +17,7 @@ export const registerPwaManifestRoute = (app, dependencies) => {
|
||||
getOpenCodeAuthHeaders,
|
||||
readSettingsFromDiskMigrated,
|
||||
normalizePwaAppName,
|
||||
normalizePwaOrientation,
|
||||
} = dependencies;
|
||||
|
||||
const recentPwaSessionsCache = new Map();
|
||||
@@ -180,18 +190,26 @@ export const registerPwaManifestRoute = (app, dependencies) => {
|
||||
}
|
||||
|
||||
const queryOverrideName = normalizePwaAppName(queryValueRaw, '');
|
||||
const hasOrientationOverride = typeof req.query?.orientation === 'string';
|
||||
const queryOverrideOrientation = normalizePwaOrientation(req.query?.orientation, 'system');
|
||||
|
||||
let storedName = '';
|
||||
let storedOrientation = 'system';
|
||||
try {
|
||||
const settings = await readSettingsFromDiskMigrated();
|
||||
storedName = normalizePwaAppName(settings?.pwaAppName, '');
|
||||
storedOrientation = normalizePwaOrientation(settings?.pwaOrientation, 'system');
|
||||
} catch {
|
||||
storedName = '';
|
||||
storedOrientation = 'system';
|
||||
}
|
||||
|
||||
const appName = hasQueryOverride
|
||||
? (queryOverrideName || DEFAULT_PWA_APP_NAME)
|
||||
: (storedName || DEFAULT_PWA_APP_NAME);
|
||||
const manifestOrientation = mapPwaOrientationToManifest(
|
||||
hasOrientationOverride ? queryOverrideOrientation : storedOrientation
|
||||
);
|
||||
|
||||
const shortName = appName.length > 30 ? appName.slice(0, 30) : appName;
|
||||
const recentSessionShortcuts = await getRecentPwaSessionShortcuts(req);
|
||||
@@ -206,7 +224,7 @@ export const registerPwaManifestRoute = (app, dependencies) => {
|
||||
display: 'standalone',
|
||||
background_color: '#151313',
|
||||
theme_color: '#edb449',
|
||||
orientation: 'any',
|
||||
...(manifestOrientation ? { orientation: manifestOrientation } : {}),
|
||||
icons: [
|
||||
{ src: '/pwa-192.png', sizes: '192x192', type: 'image/png', purpose: 'any' },
|
||||
{ src: '/pwa-512.png', sizes: '512x512', type: 'image/png', purpose: 'any' },
|
||||
|
||||
@@ -18,6 +18,7 @@ export const createSettingsHelpers = (dependencies) => {
|
||||
} = dependencies;
|
||||
|
||||
const PWA_APP_NAME_MAX_LENGTH = 64;
|
||||
const PWA_ORIENTATION_VALUES = new Set(['system', 'portrait', 'landscape']);
|
||||
|
||||
const normalizePwaAppName = (value, fallback = '') => {
|
||||
if (typeof value !== 'string') {
|
||||
@@ -30,6 +31,17 @@ export const createSettingsHelpers = (dependencies) => {
|
||||
return normalized.slice(0, PWA_APP_NAME_MAX_LENGTH);
|
||||
};
|
||||
|
||||
const normalizePwaOrientation = (value, fallback = 'system') => {
|
||||
if (typeof value !== 'string') {
|
||||
return fallback;
|
||||
}
|
||||
const normalized = value.trim();
|
||||
if (PWA_ORIENTATION_VALUES.has(normalized)) {
|
||||
return normalized;
|
||||
}
|
||||
return fallback;
|
||||
};
|
||||
|
||||
const sanitizeSettingsUpdate = (payload) => {
|
||||
if (!payload || typeof payload !== 'object') {
|
||||
return {};
|
||||
@@ -292,6 +304,9 @@ export const createSettingsHelpers = (dependencies) => {
|
||||
if (typeof candidate.pwaAppName === 'string') {
|
||||
result.pwaAppName = normalizePwaAppName(candidate.pwaAppName, undefined);
|
||||
}
|
||||
if (typeof candidate.pwaOrientation === 'string') {
|
||||
result.pwaOrientation = normalizePwaOrientation(candidate.pwaOrientation, undefined);
|
||||
}
|
||||
if (typeof candidate.toolCallExpansion === 'string') {
|
||||
const mode = candidate.toolCallExpansion.trim();
|
||||
if (mode === 'collapsed' || mode === 'activity' || mode === 'detailed' || mode === 'changes') {
|
||||
@@ -602,11 +617,13 @@ export const createSettingsHelpers = (dependencies) => {
|
||||
const bookmarks = normalizeStringArray(settings.securityScopedBookmarks);
|
||||
const hasManagedRemoteTunnelToken = typeof settings?.managedRemoteTunnelToken === 'string' && settings.managedRemoteTunnelToken.trim().length > 0;
|
||||
const pwaAppName = normalizePwaAppName(settings?.pwaAppName, '');
|
||||
const pwaOrientation = normalizePwaOrientation(settings?.pwaOrientation, 'system');
|
||||
|
||||
return {
|
||||
...sanitized,
|
||||
hasManagedRemoteTunnelToken,
|
||||
...(pwaAppName ? { pwaAppName } : {}),
|
||||
pwaOrientation,
|
||||
approvedDirectories: approved,
|
||||
securityScopedBookmarks: bookmarks,
|
||||
pinnedDirectories: normalizeStringArray(settings.pinnedDirectories),
|
||||
@@ -622,6 +639,7 @@ export const createSettingsHelpers = (dependencies) => {
|
||||
|
||||
return {
|
||||
normalizePwaAppName,
|
||||
normalizePwaOrientation,
|
||||
sanitizeSettingsUpdate,
|
||||
mergePersistedSettings,
|
||||
formatSettingsResponse,
|
||||
|
||||
@@ -12,6 +12,7 @@ export const createStaticRoutesRuntime = (dependencies) => {
|
||||
getOpenCodeAuthHeaders,
|
||||
readSettingsFromDiskMigrated,
|
||||
normalizePwaAppName,
|
||||
normalizePwaOrientation,
|
||||
} = dependencies;
|
||||
|
||||
const resolveDistPath = () => {
|
||||
@@ -43,6 +44,7 @@ export const createStaticRoutesRuntime = (dependencies) => {
|
||||
getOpenCodeAuthHeaders,
|
||||
readSettingsFromDiskMigrated,
|
||||
normalizePwaAppName,
|
||||
normalizePwaOrientation,
|
||||
});
|
||||
|
||||
app.get(/^(?!\/api|.*\.(js|css|svg|png|jpg|jpeg|gif|ico|woff|woff2|ttf|eot|map)).*$/, (_req, res) => {
|
||||
|
||||
Reference in New Issue
Block a user