feat(pwa): add install orientation setting (#900)
This commit is contained in:
@@ -116,6 +116,7 @@ const VisualSectionContent: React.FC = () => {
|
|||||||
return <OpenChamberVisualSettings visibleSettings={[
|
return <OpenChamberVisualSettings visibleSettings={[
|
||||||
'theme',
|
'theme',
|
||||||
'pwaInstallName',
|
'pwaInstallName',
|
||||||
|
'pwaOrientation',
|
||||||
'timeFormat',
|
'timeFormat',
|
||||||
'weekStart',
|
'weekStart',
|
||||||
'fontSize',
|
'fontSize',
|
||||||
|
|||||||
@@ -95,12 +95,34 @@ const MERMAID_RENDERING_OPTIONS: Option<'svg' | 'ascii'>[] = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const DEFAULT_PWA_INSTALL_NAME = 'OpenChamber - AI Coding Assistant';
|
const DEFAULT_PWA_INSTALL_NAME = 'OpenChamber - AI Coding Assistant';
|
||||||
|
const PWA_ORIENTATION_OPTIONS: Option<'system' | 'portrait' | 'landscape'>[] = [
|
||||||
|
{
|
||||||
|
id: 'system',
|
||||||
|
label: 'Follow system',
|
||||||
|
description: 'Respect the device rotation setting.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'portrait',
|
||||||
|
label: 'Portrait lock',
|
||||||
|
description: 'Install the app locked to portrait.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'landscape',
|
||||||
|
label: 'Landscape lock',
|
||||||
|
description: 'Install the app locked to landscape.',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
type PwaInstallNameWindow = Window & {
|
type PwaInstallNameWindow = Window & {
|
||||||
__OPENCHAMBER_SET_PWA_INSTALL_NAME__?: (value: string) => string;
|
__OPENCHAMBER_SET_PWA_INSTALL_NAME__?: (value: string) => string;
|
||||||
|
__OPENCHAMBER_SET_PWA_ORIENTATION__?: (value: 'system' | 'portrait' | 'landscape') => 'system' | 'portrait' | 'landscape';
|
||||||
__OPENCHAMBER_UPDATE_PWA_MANIFEST__?: () => void;
|
__OPENCHAMBER_UPDATE_PWA_MANIFEST__?: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const normalizePwaOrientation = (value: unknown): 'system' | 'portrait' | 'landscape' => {
|
||||||
|
return value === 'portrait' || value === 'landscape' ? value : 'system';
|
||||||
|
};
|
||||||
|
|
||||||
const USER_MESSAGE_RENDERING_OPTIONS: Option<'markdown' | 'plain'>[] = [
|
const USER_MESSAGE_RENDERING_OPTIONS: Option<'markdown' | 'plain'>[] = [
|
||||||
{
|
{
|
||||||
id: 'markdown',
|
id: 'markdown',
|
||||||
@@ -196,7 +218,7 @@ const normalizeUserMessageRenderingMode = (mode: unknown): 'markdown' | 'plain'
|
|||||||
return mode === 'markdown' ? 'markdown' : 'plain';
|
return mode === 'markdown' ? 'markdown' : 'plain';
|
||||||
};
|
};
|
||||||
|
|
||||||
export type VisibleSetting = 'theme' | 'pwaInstallName' | 'timeFormat' | 'weekStart' | 'fontSize' | 'terminalFontSize' | 'spacing' | 'inputBarOffset' | 'mermaidRendering' | 'userMessageRendering' | 'chatRenderMode' | 'messageTransport' | 'activityRenderMode' | 'stickyUserHeader' | 'diffLayout' | 'mobileStatusBar' | 'dotfiles' | 'reasoning' | 'showToolFileIcons' | 'expandedTools' | 'queueMode' | 'terminalQuickKeys' | 'persistDraft' | 'inputSpellcheck' | 'reportUsage';
|
export type VisibleSetting = 'theme' | 'pwaInstallName' | 'pwaOrientation' | 'timeFormat' | 'weekStart' | 'fontSize' | 'terminalFontSize' | 'spacing' | 'inputBarOffset' | 'mermaidRendering' | 'userMessageRendering' | 'chatRenderMode' | 'messageTransport' | 'activityRenderMode' | 'stickyUserHeader' | 'diffLayout' | 'mobileStatusBar' | 'dotfiles' | 'reasoning' | 'showToolFileIcons' | 'expandedTools' | 'queueMode' | 'terminalQuickKeys' | 'persistDraft' | 'inputSpellcheck' | 'reportUsage';
|
||||||
|
|
||||||
interface OpenChamberVisualSettingsProps {
|
interface OpenChamberVisualSettingsProps {
|
||||||
/** Which settings to show. If undefined, shows all. */
|
/** Which settings to show. If undefined, shows all. */
|
||||||
@@ -419,7 +441,7 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
|
|||||||
};
|
};
|
||||||
|
|
||||||
const isVSCode = isVSCodeRuntime();
|
const isVSCode = isVSCodeRuntime();
|
||||||
const hasAppearanceSettings = (shouldShow('theme') || shouldShow('pwaInstallName') || shouldShow('timeFormat') || shouldShow('weekStart')) && !isVSCode;
|
const hasAppearanceSettings = (shouldShow('theme') || shouldShow('pwaInstallName') || shouldShow('pwaOrientation') || shouldShow('timeFormat') || shouldShow('weekStart')) && !isVSCode;
|
||||||
const hasLayoutSettings = shouldShow('fontSize') || shouldShow('terminalFontSize') || shouldShow('spacing') || shouldShow('inputBarOffset');
|
const hasLayoutSettings = shouldShow('fontSize') || shouldShow('terminalFontSize') || shouldShow('spacing') || shouldShow('inputBarOffset');
|
||||||
const hasNavigationSettings = shouldShow('terminalQuickKeys') && !isMobile;
|
const hasNavigationSettings = shouldShow('terminalQuickKeys') && !isMobile;
|
||||||
const hasBehaviorSettings = shouldShow('mermaidRendering')
|
const hasBehaviorSettings = shouldShow('mermaidRendering')
|
||||||
@@ -439,7 +461,9 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
|
|||||||
|| (!isMobile && shouldShow('inputSpellcheck'));
|
|| (!isMobile && shouldShow('inputSpellcheck'));
|
||||||
|
|
||||||
const showPwaInstallNameSetting = shouldShow('pwaInstallName') && isWebRuntime() && browserTab && !isDesktopShell() && !isVSCode;
|
const showPwaInstallNameSetting = shouldShow('pwaInstallName') && isWebRuntime() && browserTab && !isDesktopShell() && !isVSCode;
|
||||||
|
const showPwaOrientationSetting = shouldShow('pwaOrientation') && isWebRuntime() && !isDesktopShell() && !isVSCode;
|
||||||
const [pwaInstallName, setPwaInstallName] = React.useState('');
|
const [pwaInstallName, setPwaInstallName] = React.useState('');
|
||||||
|
const [pwaOrientation, setPwaOrientation] = React.useState<'system' | 'portrait' | 'landscape'>('system');
|
||||||
|
|
||||||
const applyPwaInstallName = React.useCallback(async (value: string) => {
|
const applyPwaInstallName = React.useCallback(async (value: string) => {
|
||||||
if (typeof window === 'undefined') {
|
if (typeof window === 'undefined') {
|
||||||
@@ -462,8 +486,28 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
|
|||||||
win.__OPENCHAMBER_UPDATE_PWA_MANIFEST__?.();
|
win.__OPENCHAMBER_UPDATE_PWA_MANIFEST__?.();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const applyPwaOrientation = React.useCallback(async (value: 'system' | 'portrait' | 'landscape') => {
|
||||||
|
if (typeof window === 'undefined') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const win = window as PwaInstallNameWindow;
|
||||||
|
const normalized = normalizePwaOrientation(value);
|
||||||
|
|
||||||
|
await updateDesktopSettings({ pwaOrientation: normalized });
|
||||||
|
|
||||||
|
if (typeof win.__OPENCHAMBER_SET_PWA_ORIENTATION__ === 'function') {
|
||||||
|
const resolved = win.__OPENCHAMBER_SET_PWA_ORIENTATION__(normalized);
|
||||||
|
setPwaOrientation(resolved);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setPwaOrientation(normalized);
|
||||||
|
win.__OPENCHAMBER_UPDATE_PWA_MANIFEST__?.();
|
||||||
|
}, []);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (typeof window === 'undefined' || !showPwaInstallNameSetting) {
|
if (typeof window === 'undefined' || (!showPwaInstallNameSetting && !showPwaOrientationSetting)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -487,13 +531,24 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
|
|||||||
const settings = await response.json().catch(() => ({}));
|
const settings = await response.json().catch(() => ({}));
|
||||||
const raw = typeof settings?.pwaAppName === 'string' ? settings.pwaAppName : '';
|
const raw = typeof settings?.pwaAppName === 'string' ? settings.pwaAppName : '';
|
||||||
const normalized = raw.trim().replace(/\s+/g, ' ').slice(0, 64);
|
const normalized = raw.trim().replace(/\s+/g, ' ').slice(0, 64);
|
||||||
|
const orientation = normalizePwaOrientation(settings?.pwaOrientation);
|
||||||
|
|
||||||
if (!cancelled) {
|
if (!cancelled) {
|
||||||
setPwaInstallName(normalized || DEFAULT_PWA_INSTALL_NAME);
|
if (showPwaInstallNameSetting) {
|
||||||
|
setPwaInstallName(normalized || DEFAULT_PWA_INSTALL_NAME);
|
||||||
|
}
|
||||||
|
if (showPwaOrientationSetting) {
|
||||||
|
setPwaOrientation(orientation);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
if (!cancelled) {
|
if (!cancelled) {
|
||||||
setPwaInstallName(DEFAULT_PWA_INSTALL_NAME);
|
if (showPwaInstallNameSetting) {
|
||||||
|
setPwaInstallName(DEFAULT_PWA_INSTALL_NAME);
|
||||||
|
}
|
||||||
|
if (showPwaOrientationSetting) {
|
||||||
|
setPwaOrientation('system');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -503,7 +558,7 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
|
|||||||
return () => {
|
return () => {
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
};
|
};
|
||||||
}, [showPwaInstallNameSetting]);
|
}, [showPwaInstallNameSetting, showPwaOrientationSetting]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
@@ -689,6 +744,50 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{showPwaOrientationSetting && (
|
||||||
|
<div className="py-1.5 space-y-1.5">
|
||||||
|
<div className="flex min-w-0 flex-col">
|
||||||
|
<span className="typography-ui-label text-foreground">Install Orientation</span>
|
||||||
|
<span className="typography-meta text-muted-foreground">Used by the installed web app. Reinstall the PWA after changing this.</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex w-full max-w-[18rem] items-center gap-2">
|
||||||
|
<Select
|
||||||
|
value={pwaOrientation}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
const orientation = normalizePwaOrientation(value);
|
||||||
|
setPwaOrientation(orientation);
|
||||||
|
void applyPwaOrientation(orientation);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectTrigger aria-label="PWA install orientation" className="w-full">
|
||||||
|
<SelectValue placeholder="Select orientation" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{PWA_ORIENTATION_OPTIONS.map((option) => (
|
||||||
|
<SelectItem key={option.id} value={option.id}>
|
||||||
|
{option.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<Button size="sm"
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => {
|
||||||
|
setPwaOrientation('system');
|
||||||
|
void applyPwaOrientation('system');
|
||||||
|
}}
|
||||||
|
disabled={pwaOrientation === 'system'}
|
||||||
|
className="h-7 w-7 px-0 text-muted-foreground hover:text-foreground"
|
||||||
|
aria-label="Reset install orientation"
|
||||||
|
title="Reset"
|
||||||
|
>
|
||||||
|
<RiRestartLine className="h-3.5 w-3.5" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -119,6 +119,7 @@ export type DesktopSettings = {
|
|||||||
gitProviderId?: string;
|
gitProviderId?: string;
|
||||||
gitModelId?: string;
|
gitModelId?: string;
|
||||||
pwaAppName?: string;
|
pwaAppName?: string;
|
||||||
|
pwaOrientation?: 'system' | 'portrait' | 'landscape';
|
||||||
inputSpellcheckEnabled?: boolean;
|
inputSpellcheckEnabled?: boolean;
|
||||||
showToolFileIcons?: boolean;
|
showToolFileIcons?: boolean;
|
||||||
showExpandedBashTools?: boolean;
|
showExpandedBashTools?: boolean;
|
||||||
|
|||||||
+49
-6
@@ -27,6 +27,7 @@
|
|||||||
const defaultAppName = 'OpenChamber - AI Coding Assistant';
|
const defaultAppName = 'OpenChamber - AI Coding Assistant';
|
||||||
const defaultShortName = 'OpenChamber';
|
const defaultShortName = 'OpenChamber';
|
||||||
const pwaNameStorageKey = 'openchamber.pwaName';
|
const pwaNameStorageKey = 'openchamber.pwaName';
|
||||||
|
const pwaOrientationStorageKey = 'openchamber.pwaOrientation';
|
||||||
const pwaRecentSessionsStorageKey = 'openchamber.pwaRecentSessions';
|
const pwaRecentSessionsStorageKey = 'openchamber.pwaRecentSessions';
|
||||||
|
|
||||||
const normalizePwaName = (value, fallback) => {
|
const normalizePwaName = (value, fallback) => {
|
||||||
@@ -47,6 +48,13 @@
|
|||||||
return value.length > maxLength ? value.slice(0, maxLength) : value;
|
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 = () => {
|
const getStoredInstallName = () => {
|
||||||
try {
|
try {
|
||||||
const storedName = localStorage.getItem(pwaNameStorageKey);
|
const storedName = localStorage.getItem(pwaNameStorageKey);
|
||||||
@@ -70,6 +78,25 @@
|
|||||||
return normalizedName || defaultAppName;
|
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 = () => {
|
const getQueryInstallNameOverride = () => {
|
||||||
try {
|
try {
|
||||||
const params = new URLSearchParams(location.search);
|
const params = new URLSearchParams(location.search);
|
||||||
@@ -158,8 +185,13 @@
|
|||||||
return shortcuts;
|
return shortcuts;
|
||||||
};
|
};
|
||||||
|
|
||||||
const buildManifest = (appName, recentSessions) => {
|
const buildManifest = (appName, recentSessions, orientationPreference) => {
|
||||||
const shortName = appName === defaultAppName ? defaultShortName : truncate(appName, 30);
|
const shortName = appName === defaultAppName ? defaultShortName : truncate(appName, 30);
|
||||||
|
const manifestOrientation = orientationPreference === 'portrait'
|
||||||
|
? 'portrait-primary'
|
||||||
|
: orientationPreference === 'landscape'
|
||||||
|
? 'landscape-primary'
|
||||||
|
: null;
|
||||||
return {
|
return {
|
||||||
name: appName,
|
name: appName,
|
||||||
short_name: shortName,
|
short_name: shortName,
|
||||||
@@ -168,7 +200,7 @@
|
|||||||
start_url: `${baseUrl}/`,
|
start_url: `${baseUrl}/`,
|
||||||
scope: `${baseUrl}/`,
|
scope: `${baseUrl}/`,
|
||||||
display: 'standalone',
|
display: 'standalone',
|
||||||
orientation: 'portrait-primary',
|
...(manifestOrientation ? { orientation: manifestOrientation } : {}),
|
||||||
background_color: '#151313',
|
background_color: '#151313',
|
||||||
theme_color: '#edb449',
|
theme_color: '#edb449',
|
||||||
icons: [
|
icons: [
|
||||||
@@ -187,11 +219,14 @@
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const buildManifestEndpointUrl = (installNameOverride = null) => {
|
const buildManifestEndpointUrl = (installNameOverride = null, orientationOverride = null) => {
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
if (typeof installNameOverride === 'string') {
|
if (typeof installNameOverride === 'string') {
|
||||||
params.set('appName', installNameOverride);
|
params.set('appName', installNameOverride);
|
||||||
}
|
}
|
||||||
|
if (typeof orientationOverride === 'string') {
|
||||||
|
params.set('orientation', normalizePwaOrientation(orientationOverride, 'system'));
|
||||||
|
}
|
||||||
const search = params.toString();
|
const search = params.toString();
|
||||||
return `${baseUrl}/manifest.webmanifest${search ? `?${search}` : ''}`;
|
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 resolvedFallbackName = typeof installNameOverride === 'string' ? installNameOverride : getStoredInstallName();
|
||||||
|
const resolvedOrientation = typeof orientationOverride === 'string'
|
||||||
|
? normalizePwaOrientation(orientationOverride, 'system')
|
||||||
|
: getStoredOrientation();
|
||||||
const recentSessions = parseRecentSessionShortcuts();
|
const recentSessions = parseRecentSessionShortcuts();
|
||||||
const manifest = buildManifest(resolvedFallbackName, recentSessions);
|
const manifest = buildManifest(resolvedFallbackName, recentSessions, resolvedOrientation);
|
||||||
const manifestUrl = buildManifestEndpointUrl(installNameOverride);
|
const manifestUrl = buildManifestEndpointUrl(installNameOverride, resolvedOrientation);
|
||||||
const requestVersion = ++manifestRequestVersion;
|
const requestVersion = ++manifestRequestVersion;
|
||||||
|
|
||||||
const useEndpoint = await canUseManifestEndpoint(manifestUrl, requestVersion);
|
const useEndpoint = await canUseManifestEndpoint(manifestUrl, requestVersion);
|
||||||
@@ -290,6 +328,11 @@
|
|||||||
void updateManifest(resolvedName);
|
void updateManifest(resolvedName);
|
||||||
return resolvedName;
|
return resolvedName;
|
||||||
};
|
};
|
||||||
|
window.__OPENCHAMBER_SET_PWA_ORIENTATION__ = (value) => {
|
||||||
|
const resolvedOrientation = setStoredOrientation(value);
|
||||||
|
void updateManifest(null, resolvedOrientation);
|
||||||
|
return resolvedOrientation;
|
||||||
|
};
|
||||||
window.__OPENCHAMBER_UPDATE_PWA_MANIFEST__ = () => {
|
window.__OPENCHAMBER_UPDATE_PWA_MANIFEST__ = () => {
|
||||||
refreshManifestFromStorage();
|
refreshManifestFromStorage();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,7 +6,6 @@
|
|||||||
"display": "standalone",
|
"display": "standalone",
|
||||||
"background_color": "#151313",
|
"background_color": "#151313",
|
||||||
"theme_color": "#edb449",
|
"theme_color": "#edb449",
|
||||||
"orientation": "portrait-primary",
|
|
||||||
"icons": [
|
"icons": [
|
||||||
{ "src": "/pwa-192.png", "sizes": "192x192", "type": "image/png", "purpose": "any" },
|
{ "src": "/pwa-192.png", "sizes": "192x192", "type": "image/png", "purpose": "any" },
|
||||||
{ "src": "/pwa-512.png", "sizes": "512x512", "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 normalizePwaAppName = (...args) => settingsHelpers.normalizePwaAppName(...args);
|
||||||
|
const normalizePwaOrientation = (...args) => settingsHelpers.normalizePwaOrientation(...args);
|
||||||
const sanitizeSettingsUpdate = (...args) => settingsHelpers.sanitizeSettingsUpdate(...args);
|
const sanitizeSettingsUpdate = (...args) => settingsHelpers.sanitizeSettingsUpdate(...args);
|
||||||
const mergePersistedSettings = (...args) => settingsHelpers.mergePersistedSettings(...args);
|
const mergePersistedSettings = (...args) => settingsHelpers.mergePersistedSettings(...args);
|
||||||
const formatSettingsResponse = (...args) => settingsHelpers.formatSettingsResponse(...args);
|
const formatSettingsResponse = (...args) => settingsHelpers.formatSettingsResponse(...args);
|
||||||
@@ -759,6 +760,7 @@ const staticRoutesRuntime = createStaticRoutesRuntime({
|
|||||||
getOpenCodeAuthHeaders,
|
getOpenCodeAuthHeaders,
|
||||||
readSettingsFromDiskMigrated,
|
readSettingsFromDiskMigrated,
|
||||||
normalizePwaAppName,
|
normalizePwaAppName,
|
||||||
|
normalizePwaOrientation,
|
||||||
});
|
});
|
||||||
const featureRoutesRuntime = createFeatureRoutesRuntime({
|
const featureRoutesRuntime = createFeatureRoutesRuntime({
|
||||||
clientReloadDelayMs: CLIENT_RELOAD_DELAY_MS,
|
clientReloadDelayMs: CLIENT_RELOAD_DELAY_MS,
|
||||||
|
|||||||
@@ -1,4 +1,13 @@
|
|||||||
const DEFAULT_PWA_APP_NAME = 'OpenChamber - AI Coding Assistant';
|
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) => {
|
export const registerPwaManifestRoute = (app, dependencies) => {
|
||||||
const {
|
const {
|
||||||
@@ -8,6 +17,7 @@ export const registerPwaManifestRoute = (app, dependencies) => {
|
|||||||
getOpenCodeAuthHeaders,
|
getOpenCodeAuthHeaders,
|
||||||
readSettingsFromDiskMigrated,
|
readSettingsFromDiskMigrated,
|
||||||
normalizePwaAppName,
|
normalizePwaAppName,
|
||||||
|
normalizePwaOrientation,
|
||||||
} = dependencies;
|
} = dependencies;
|
||||||
|
|
||||||
const recentPwaSessionsCache = new Map();
|
const recentPwaSessionsCache = new Map();
|
||||||
@@ -180,18 +190,26 @@ export const registerPwaManifestRoute = (app, dependencies) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const queryOverrideName = normalizePwaAppName(queryValueRaw, '');
|
const queryOverrideName = normalizePwaAppName(queryValueRaw, '');
|
||||||
|
const hasOrientationOverride = typeof req.query?.orientation === 'string';
|
||||||
|
const queryOverrideOrientation = normalizePwaOrientation(req.query?.orientation, 'system');
|
||||||
|
|
||||||
let storedName = '';
|
let storedName = '';
|
||||||
|
let storedOrientation = 'system';
|
||||||
try {
|
try {
|
||||||
const settings = await readSettingsFromDiskMigrated();
|
const settings = await readSettingsFromDiskMigrated();
|
||||||
storedName = normalizePwaAppName(settings?.pwaAppName, '');
|
storedName = normalizePwaAppName(settings?.pwaAppName, '');
|
||||||
|
storedOrientation = normalizePwaOrientation(settings?.pwaOrientation, 'system');
|
||||||
} catch {
|
} catch {
|
||||||
storedName = '';
|
storedName = '';
|
||||||
|
storedOrientation = 'system';
|
||||||
}
|
}
|
||||||
|
|
||||||
const appName = hasQueryOverride
|
const appName = hasQueryOverride
|
||||||
? (queryOverrideName || DEFAULT_PWA_APP_NAME)
|
? (queryOverrideName || DEFAULT_PWA_APP_NAME)
|
||||||
: (storedName || 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 shortName = appName.length > 30 ? appName.slice(0, 30) : appName;
|
||||||
const recentSessionShortcuts = await getRecentPwaSessionShortcuts(req);
|
const recentSessionShortcuts = await getRecentPwaSessionShortcuts(req);
|
||||||
@@ -206,7 +224,7 @@ export const registerPwaManifestRoute = (app, dependencies) => {
|
|||||||
display: 'standalone',
|
display: 'standalone',
|
||||||
background_color: '#151313',
|
background_color: '#151313',
|
||||||
theme_color: '#edb449',
|
theme_color: '#edb449',
|
||||||
orientation: 'any',
|
...(manifestOrientation ? { orientation: manifestOrientation } : {}),
|
||||||
icons: [
|
icons: [
|
||||||
{ src: '/pwa-192.png', sizes: '192x192', type: 'image/png', purpose: 'any' },
|
{ src: '/pwa-192.png', sizes: '192x192', type: 'image/png', purpose: 'any' },
|
||||||
{ src: '/pwa-512.png', sizes: '512x512', 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;
|
} = dependencies;
|
||||||
|
|
||||||
const PWA_APP_NAME_MAX_LENGTH = 64;
|
const PWA_APP_NAME_MAX_LENGTH = 64;
|
||||||
|
const PWA_ORIENTATION_VALUES = new Set(['system', 'portrait', 'landscape']);
|
||||||
|
|
||||||
const normalizePwaAppName = (value, fallback = '') => {
|
const normalizePwaAppName = (value, fallback = '') => {
|
||||||
if (typeof value !== 'string') {
|
if (typeof value !== 'string') {
|
||||||
@@ -30,6 +31,17 @@ export const createSettingsHelpers = (dependencies) => {
|
|||||||
return normalized.slice(0, PWA_APP_NAME_MAX_LENGTH);
|
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) => {
|
const sanitizeSettingsUpdate = (payload) => {
|
||||||
if (!payload || typeof payload !== 'object') {
|
if (!payload || typeof payload !== 'object') {
|
||||||
return {};
|
return {};
|
||||||
@@ -292,6 +304,9 @@ export const createSettingsHelpers = (dependencies) => {
|
|||||||
if (typeof candidate.pwaAppName === 'string') {
|
if (typeof candidate.pwaAppName === 'string') {
|
||||||
result.pwaAppName = normalizePwaAppName(candidate.pwaAppName, undefined);
|
result.pwaAppName = normalizePwaAppName(candidate.pwaAppName, undefined);
|
||||||
}
|
}
|
||||||
|
if (typeof candidate.pwaOrientation === 'string') {
|
||||||
|
result.pwaOrientation = normalizePwaOrientation(candidate.pwaOrientation, undefined);
|
||||||
|
}
|
||||||
if (typeof candidate.toolCallExpansion === 'string') {
|
if (typeof candidate.toolCallExpansion === 'string') {
|
||||||
const mode = candidate.toolCallExpansion.trim();
|
const mode = candidate.toolCallExpansion.trim();
|
||||||
if (mode === 'collapsed' || mode === 'activity' || mode === 'detailed' || mode === 'changes') {
|
if (mode === 'collapsed' || mode === 'activity' || mode === 'detailed' || mode === 'changes') {
|
||||||
@@ -602,11 +617,13 @@ export const createSettingsHelpers = (dependencies) => {
|
|||||||
const bookmarks = normalizeStringArray(settings.securityScopedBookmarks);
|
const bookmarks = normalizeStringArray(settings.securityScopedBookmarks);
|
||||||
const hasManagedRemoteTunnelToken = typeof settings?.managedRemoteTunnelToken === 'string' && settings.managedRemoteTunnelToken.trim().length > 0;
|
const hasManagedRemoteTunnelToken = typeof settings?.managedRemoteTunnelToken === 'string' && settings.managedRemoteTunnelToken.trim().length > 0;
|
||||||
const pwaAppName = normalizePwaAppName(settings?.pwaAppName, '');
|
const pwaAppName = normalizePwaAppName(settings?.pwaAppName, '');
|
||||||
|
const pwaOrientation = normalizePwaOrientation(settings?.pwaOrientation, 'system');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...sanitized,
|
...sanitized,
|
||||||
hasManagedRemoteTunnelToken,
|
hasManagedRemoteTunnelToken,
|
||||||
...(pwaAppName ? { pwaAppName } : {}),
|
...(pwaAppName ? { pwaAppName } : {}),
|
||||||
|
pwaOrientation,
|
||||||
approvedDirectories: approved,
|
approvedDirectories: approved,
|
||||||
securityScopedBookmarks: bookmarks,
|
securityScopedBookmarks: bookmarks,
|
||||||
pinnedDirectories: normalizeStringArray(settings.pinnedDirectories),
|
pinnedDirectories: normalizeStringArray(settings.pinnedDirectories),
|
||||||
@@ -622,6 +639,7 @@ export const createSettingsHelpers = (dependencies) => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
normalizePwaAppName,
|
normalizePwaAppName,
|
||||||
|
normalizePwaOrientation,
|
||||||
sanitizeSettingsUpdate,
|
sanitizeSettingsUpdate,
|
||||||
mergePersistedSettings,
|
mergePersistedSettings,
|
||||||
formatSettingsResponse,
|
formatSettingsResponse,
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export const createStaticRoutesRuntime = (dependencies) => {
|
|||||||
getOpenCodeAuthHeaders,
|
getOpenCodeAuthHeaders,
|
||||||
readSettingsFromDiskMigrated,
|
readSettingsFromDiskMigrated,
|
||||||
normalizePwaAppName,
|
normalizePwaAppName,
|
||||||
|
normalizePwaOrientation,
|
||||||
} = dependencies;
|
} = dependencies;
|
||||||
|
|
||||||
const resolveDistPath = () => {
|
const resolveDistPath = () => {
|
||||||
@@ -43,6 +44,7 @@ export const createStaticRoutesRuntime = (dependencies) => {
|
|||||||
getOpenCodeAuthHeaders,
|
getOpenCodeAuthHeaders,
|
||||||
readSettingsFromDiskMigrated,
|
readSettingsFromDiskMigrated,
|
||||||
normalizePwaAppName,
|
normalizePwaAppName,
|
||||||
|
normalizePwaOrientation,
|
||||||
});
|
});
|
||||||
|
|
||||||
app.get(/^(?!\/api|.*\.(js|css|svg|png|jpg|jpeg|gif|ico|woff|woff2|ttf|eot|map)).*$/, (_req, res) => {
|
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