fix(launcher): open_in_os win routing, display override, and onsite ext fix

- open_in_os='win' now routes to Windows launch profiles (pptxwin/pptwin/odpwin/pdfwin)
  via WIN_EXTENSION_MAP in get_launch_profile() — was silently ignored before
- Display override migrated from non-existent cfg_json backend field to localStorage
  ($events_loc.launcher.file_display_overrides) — only visible in edit mode; TODO added
  for proper backend column when event_file gains cfg_json
- Onsite mode WIN extension rename now covers all 4 types (pptx, ppt, odp, pdf)
  instead of only pptx/ppt
- open_in_os button shows LoaderCircle spinner during API call
- Remove cfg_json from properties_to_save (column does not exist on event_file table)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Scott Idem
2026-05-22 15:58:27 -04:00
parent a26ea8b49c
commit c76fb8f2b5
2 changed files with 90 additions and 79 deletions

View File

@@ -539,7 +539,6 @@ export const properties_to_save = [
'filename',
'extension',
'open_in_os',
'cfg_json',
'lu_file_purpose_id',
'lu_event_file_purpose_name',
'file_purpose',

View File

@@ -93,6 +93,14 @@ let open_file_status: null | string = $state(null);
let open_file_status_message: null | string = $state(null);
let open_file_error_detail: string | null = $state(null);
let open_in_os_loading: boolean = $state(false);
/** Reactive display override for this file — stored in $events_loc (localStorage) not in the backend. */
const current_display_override = $derived.by(() => {
const overrides = (($events_loc.launcher as Record<string, unknown>)?.file_display_overrides ?? {}) as Record<string, string>;
return (overrides[event_file_id] ?? null) as 'extend' | 'mirror' | 'none' | null;
});
/** State for the native test mode debug popup */
let test_mode_popup_open: boolean = $state(false);
let test_mode_popup_data: Record<string, any> | null = $state(null);
@@ -135,7 +143,11 @@ function get_launch_profile(
native_device?.launch_profiles ??
null;
const local_profiles = ($events_loc as any).launcher?.launch_profiles ?? null;
const display_override = file_obj?.cfg_json?.display_override ?? null;
// Display override is stored per-device in $events_loc — not in the backend (event_file has no JSON column).
// This is intentional: display mode is a room/device preference, not a global file property.
const launcher_kv = $events_loc.launcher as Record<string, unknown>;
const file_display_overrides = (launcher_kv?.file_display_overrides ?? {}) as Record<string, string>;
const display_override = (file_display_overrides[event_file_id] ?? null) as 'extend' | 'mirror' | 'none' | null;
// open_in_os = 'win' routes to the Windows-variant profile for apps that have one.
// These profiles target Windows PowerPoint / LibreOffice / Acrobat running via Parallels or CrossOver.
@@ -469,12 +481,11 @@ async function handle_open_file() {
open_file_status_message = 'Downloading (Onsite Mode)...';
open_file_error_detail = null;
// Append 'win' to the filename for extensions that have Windows file associations
// (pptx→pptxwin, ppt→pptwin, odp→odpwin, pdf→pdfwin). Must match WIN_EXTENSION_MAP.
const WIN_ONSITE_EXTS = ['pptx', 'ppt', 'odp', 'pdf'];
let filename = event_file_obj.filename;
if (
(event_file_obj.extension === 'ppt' ||
event_file_obj.extension === 'pptx') &&
event_file_obj.open_in_os === 'win'
) {
if (event_file_obj.open_in_os === 'win' && WIN_ONSITE_EXTS.includes(event_file_obj.extension)) {
filename = event_file_obj.filename + 'win';
}
@@ -620,7 +631,7 @@ function prevent_default<T extends Event>(fn: (event: T) => void) {
click={handle_open_file}>
{#snippet label()}
{@const file_id = event_file_obj.hosted_file_id}
<span class="shrink border-r border-gray-400 pr-1 text-xs">
<span class="shrink border-r border-surface-300-700 pr-1 text-xs">
{#await ae_promises[event_file_id]}
<LoaderCircle
size="1em"
@@ -634,26 +645,28 @@ function prevent_default<T extends Event>(fn: (event: T) => void) {
{/if}
</span>
{:then result}
<span class=" font-mono">
{#if is_url}
<Link2 size="1em" class="mx-0.5 inline {!is_online ? 'text-warning-500' : ''}" />
<span class:text-warning-500={!is_online}>url</span>
{#if !is_online}<WifiOff size="0.85em" class="mx-0.5 inline text-warning-500" title="Network offline" />{/if}
<Link2 size="1em" class="inline opacity-50 {!is_online ? 'text-warning-900-100' : ''}" />
<span class:text-warning-900-100={!is_online}>url</span>
{#if !is_online}<WifiOff size="0.85em" class="inline text-warning-900-100" title="Network offline" />{/if}
{:else}
{@const FileIcon =
ae_util.file_extension_icon_lucide(
event_file_obj.extension
)}
<FileIcon size="1em" class="mx-0.5 inline" />
<FileIcon size="1em" class="inline opacity-50" />
{event_file_obj.extension}
{#if result === null || result === false}
<span class="text-error-500"
<span class="text-error-900-100"
><TriangleAlert
size="1em"
class="mx-1 inline" />Failed!</span>
class="inline" />Failed!</span>
{/if}
{/if}
</span>
{:catch error}
<span class="text-error-500" title={error?.message}
<span class="text-error-900-100" title={error?.message}
><AlertCircle
size="1em"
class="mx-0.5 inline" />Error!</span>
@@ -687,79 +700,71 @@ function prevent_default<T extends Event>(fn: (event: T) => void) {
<button
type="button"
onclick={async () => {
let new_val: string | null;
if (!event_file_obj?.open_in_os) new_val = 'win';
else if (event_file_obj?.open_in_os == 'win') new_val = 'mac';
else new_val = null;
await api.update_ae_obj({
api_cfg: $ae_api,
obj_type: 'event_file',
obj_id: event_file_id,
fields: { open_in_os: new_val }
});
events_func.load_ae_obj_id__event_file({
api_cfg: $ae_api,
event_file_id: event_file_obj?.event_file_id,
log_lvl
});
}}
class="btn btn-sm group transition-all"
class:preset-tonal-warning={event_file_obj?.open_in_os == 'win'}
class:preset-tonal-success={event_file_obj?.open_in_os == 'mac'}
disabled={!$ae_loc.trusted_access}
title={`Open in OS: ${
event_file_obj?.open_in_os
? event_file_obj.open_in_os.toUpperCase()
: 'None'
}`}
>
{#if event_file_obj?.open_in_os == 'win'}
<!-- <Monitor
size="1em"
class="m-1" /> -->
Win
{:else if event_file_obj?.open_in_os == 'mac'}
<!-- <Laptop
size="1em"
class="m-1" /> -->
Mac
{:else}
<FolderOpen size="1em" class="m-1" />
{/if}
</button>
{#if $ae_loc.trusted_access}
<!-- Display override: per-file display_mode override for this file only.
null = use profile default, 'extend' = force extend, 'mirror' = force mirror.
Stored in event_file.cfg_json.display_override. Cycles null → extend → mirror → null.
Settable from any device — takes effect when the file is opened in native mode. -->
<button
type="button"
onclick={async () => {
const cur = event_file_obj?.cfg_json?.display_override ?? null;
const next: string | null = !cur ? 'extend' : cur === 'extend' ? 'mirror' : null;
const new_cfg = { ...(event_file_obj.cfg_json ?? {}), display_override: next };
// Optimistic update — don't wait for the liveQuery round-trip
event_file_obj = { ...event_file_obj, cfg_json: new_cfg };
open_in_os_loading = true;
try {
let new_val: string | null;
if (!event_file_obj?.open_in_os) new_val = 'win';
else if (event_file_obj?.open_in_os == 'win') new_val = 'mac';
else new_val = null;
await api.update_ae_obj({
api_cfg: $ae_api,
obj_type: 'event_file',
obj_id: event_file_id,
fields: { cfg_json: new_cfg }
fields: { open_in_os: new_val }
});
events_func.load_ae_obj_id__event_file({
api_cfg: $ae_api,
event_file_id: event_file_obj?.event_file_id,
log_lvl
});
} finally {
open_in_os_loading = false;
}
}}
class="btn btn-sm group transition-all"
class:preset-tonal-warning={event_file_obj?.open_in_os == 'win'}
class:preset-tonal-success={event_file_obj?.open_in_os == 'mac'}
disabled={!$ae_loc.trusted_access || open_in_os_loading}
title={`Open in OS: ${
event_file_obj?.open_in_os == 'win' ? 'Windows' : event_file_obj?.open_in_os == 'mac' ? 'macOS' : event_file_obj?.open_in_os == 'linux' ? 'Linux' : '--not set--'
}`}
>
{#if open_in_os_loading}
<LoaderCircle size="1em" class="m-1 animate-spin" />
{:else if event_file_obj?.open_in_os == 'win'}
Win
{:else if event_file_obj?.open_in_os == 'mac'}
Mac
{:else}
<FolderOpen size="1em" class="m-1" />
{/if}
</button>
{#if $ae_loc.edit_mode}
<!-- Display override (temporary — local/device only, stored in $events_loc).
Cycles null → extend → mirror → null. Instant write, no API call.
TODO: replace with backend cfg_json once event_file gains a JSON column. -->
<button
type="button"
onclick={() => {
const cur = current_display_override;
const next: 'extend' | 'mirror' | null = !cur ? 'extend' : cur === 'extend' ? 'mirror' : null;
const launcher = $events_loc.launcher as Record<string, unknown>;
const new_overrides = { ...((launcher?.file_display_overrides ?? {}) as Record<string, string>) };
if (next === null) {
delete new_overrides[event_file_id];
} else {
new_overrides[event_file_id] = next;
}
launcher.file_display_overrides = new_overrides;
}}
class="btn btn-sm transition-all"
class:preset-tonal-primary={event_file_obj?.cfg_json?.display_override === 'extend'}
class:preset-tonal-warning={event_file_obj?.cfg_json?.display_override === 'mirror'}
title={`Display override: ${event_file_obj?.cfg_json?.display_override ?? 'default'}`}>
{#if event_file_obj?.cfg_json?.display_override === 'extend'}
class:preset-tonal-primary={current_display_override === 'extend'}
class:preset-tonal-warning={current_display_override === 'mirror'}
title={`Display override: ${current_display_override ?? 'default'}`}>
{#if current_display_override === 'extend'}
Ext
{:else if event_file_obj?.cfg_json?.display_override === 'mirror'}
{:else if current_display_override === 'mirror'}
Mir
{:else}
<Monitor size="1em" class="m-1" />
@@ -769,8 +774,13 @@ function prevent_default<T extends Event>(fn: (event: T) => void) {
<span
class="event_file_created_on preset-filled-surface-100-900 flex w-24 flex-row items-center justify-end gap-1 rounded px-1 py-0.5 text-center text-xs md:w-44"
class:hidden={hide_created_on}>
<CalendarDays size="0.85em" class="inline" />
class:hidden={hide_created_on}
title={`Created on:\n${ae_util.iso_datetime_formatter(
event_file_obj.created_on,
'datetime_long'
)}`}
>
<CalendarDays size="0.85em" class="inline opacity-50" />
<span class="w-18"
>{ae_util.iso_datetime_formatter(
event_file_obj.created_on,
@@ -780,8 +790,10 @@ function prevent_default<T extends Event>(fn: (event: T) => void) {
<span
class="event_file_size preset-filled-surface-100-900 flex min-w-20 w-22 max-w-28 flex-row items-center justify-end gap-1 rounded py-0.5 text-center text-xs"
class:hidden={hide_size}>
<Save size="0.85em" class="inline" />
class:hidden={hide_size}
title={`File size:\n${event_file_obj.file_size ? ae_util.format_bytes(event_file_obj.file_size) : 'Unknown size'}\nBytes: ${event_file_obj.file_size}`}
>
<Save size="0.85em" class="inline opacity-50" />
{#if event_file_obj.file_size}{ae_util.format_bytes(
event_file_obj.file_size
)}{/if}