Files
actual/packages/desktop-client/src/browser-preload.js
github-actions[bot] 95a99200d0 [AI] desktop-client: drop unused absurd-sql dependency
desktop-client no longer imports absurd-sql directly — that plumbing
moved into loot-core's backend-worker module as part of the browser
worker consolidation. The dep was left over; removing it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:00:06 +01:00

225 lines
6.0 KiB
JavaScript

import { startBrowserBackend } from '@actual-app/core/platform/client/browser-preload';
import * as Platform from '@actual-app/core/shared/platform';
import { registerSW } from 'virtual:pwa-register';
// oxlint-disable-next-line typescript-paths/absolute-parent-import
import packageJson from '../package.json';
import SharedBrowserServerWorker from './shared-browser-server.ts?sharedworker';
const backendWorkerUrl = new URL('./browser-server.js', import.meta.url);
// This file installs global variables that the app expects.
// Normally these are already provided by electron, but in a real
// browser environment this is where we initialize the backend and
// everything else.
const IS_DEV = process.env.NODE_ENV === 'development';
const ACTUAL_VERSION = Platform.isPlaywright
? '99.9.9'
: process.env.REACT_APP_REVIEW_ID
? '.preview'
: packageJson.version;
// *** Start the backend ***
//
// The multi-tab coordinator (leader/follower over SharedWorker), direct
// Worker fallback, and sqlite worker bridge now all live in loot-core
// (packages/loot-core/src/platform/client/browser-preload). We only
// hand it the desktop-specific inputs.
const worker = startBrowserBackend({
backendWorkerUrl,
initPayload: {
version: ACTUAL_VERSION,
isDev: IS_DEV,
publicUrl: process.env.PUBLIC_URL,
hash: process.env.REACT_APP_BACKEND_WORKER_HASH,
},
createSharedWorker: () =>
new SharedBrowserServerWorker({ name: 'actual-backend' }),
forceDirectWorker: Platform.isPlaywright,
});
let isUpdateReadyForDownload = false;
let markUpdateReadyForDownload;
const isUpdateReadyForDownloadPromise = new Promise(resolve => {
markUpdateReadyForDownload = () => {
isUpdateReadyForDownload = true;
resolve(true);
};
});
const updateSW = registerSW({
immediate: true,
onNeedRefresh: markUpdateReadyForDownload,
});
global.Actual = {
IS_DEV,
ACTUAL_VERSION,
logToTerminal: (...args) => {
console.log(...args);
},
relaunch: () => {
window.location.reload();
},
reload: () => {
if (window.navigator.serviceWorker == null) return;
// Unregister the service worker handling routing and then reload. This should force the reload
// to query the actual server rather than delegating to the worker
return window.navigator.serviceWorker
.getRegistration('/')
.then(registration => {
if (registration == null) return;
return registration.unregister();
})
.then(() => {
window.location.reload();
});
},
startSyncServer: () => {
// Only for electron app
},
stopSyncServer: () => {
// Only for electron app
},
isSyncServerRunning: () => false,
startOAuthServer: () => {
return '';
},
restartElectronServer: () => {
// Only for electron app
},
openFileDialog: async ({ filters = [] }) => {
const FILE_ACCEPT_OVERRIDES = {
// Safari on iOS requires explicit MIME/UTType values for some extensions to allow selection.
qfx: [
'application/vnd.intu.qfx',
'application/x-qfx',
'application/qfx',
'application/ofx',
'application/x-ofx',
'application/octet-stream',
'com.intuit.qfx',
],
};
return new Promise(resolve => {
let createdElement = false;
// Attempt to reuse an already-created file input.
let input = document.body.querySelector(
'input[id="open-file-dialog-input"]',
);
if (!input) {
createdElement = true;
input = document.createElement('input');
}
input.type = 'file';
input.id = 'open-file-dialog-input';
input.value = null;
const filter = filters.find(filter => filter.extensions);
if (filter) {
input.accept = filter.extensions
.flatMap(ext => {
const normalizedExt = ext.startsWith('.')
? ext.toLowerCase()
: `.${ext.toLowerCase()}`;
const overrides = FILE_ACCEPT_OVERRIDES[ext.toLowerCase()] ?? [];
return [normalizedExt, ...overrides];
})
.join(',');
}
input.style.position = 'absolute';
input.style.top = '0px';
input.style.left = '0px';
input.style.display = 'none';
input.onchange = e => {
const file = e.target.files[0];
const filename = file.name.replace(/.*(\.[^.]*)/, 'file$1');
if (file) {
const reader = new FileReader();
reader.readAsArrayBuffer(file);
reader.onload = async function (ev) {
const filepath = `/uploads/${filename}`;
void window.__actionsForMenu
.uploadFile(filename, ev.target.result)
.then(() => resolve([filepath]));
};
reader.onerror = function () {
alert('Error reading file');
};
}
};
// In Safari the file input has to be in the DOM for change events to
// reliably fire.
if (createdElement) {
document.body.appendChild(input);
}
input.click();
});
},
saveFile: (contents, defaultFilename) => {
const temp = document.createElement('a');
temp.style = 'display: none';
temp.download = defaultFilename;
temp.rel = 'noopener';
const blob = new Blob([contents]);
temp.href = URL.createObjectURL(blob);
temp.dispatchEvent(new MouseEvent('click'));
},
openURLInBrowser: url => {
window.open(url, '_blank');
},
openInFileManager: () => {
// File manager not available in browser
},
onEventFromMain: () => {
// Only for electron app
},
isUpdateReadyForDownload: () => isUpdateReadyForDownload,
waitForUpdateReadyForDownload: () => isUpdateReadyForDownloadPromise,
applyAppUpdate: async () => {
updateSW();
// Wait for the app to reload
await new Promise(() => {
// Do nothing
});
},
ipcConnect: () => {
// Only for electron app
},
getServerSocket: async () => {
return worker;
},
setTheme: theme => {
window.__actionsForMenu.saveGlobalPrefs({ prefs: { theme } });
},
moveBudgetDirectory: () => {
// Only for electron app
},
};