mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 21:15:32 +09:00
176 lines
5.2 KiB
TypeScript
176 lines
5.2 KiB
TypeScript
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
import fs from 'node:fs';
|
|
import {createRequire} from 'node:module';
|
|
import path from 'node:path';
|
|
import {
|
|
DESKTOP_APP_NAME,
|
|
WINDOWS_APP_USER_MODEL_ID,
|
|
WINDOWS_LEGACY_APP_USER_MODEL_IDS,
|
|
WINDOWS_SHORTCUT_AUTHOR,
|
|
WINDOWS_TOAST_ACTIVATOR_CLSID,
|
|
} from '@electron/common/DesktopIdentity';
|
|
|
|
const requireModule = createRequire(import.meta.url);
|
|
|
|
interface WindowsShortcutRepairPaths {
|
|
authorShortcut: string;
|
|
currentDir: string;
|
|
currentExe: string;
|
|
rootAppDir: string;
|
|
rootShortcut: string;
|
|
}
|
|
|
|
interface CreateShortcutOptions {
|
|
lnkPath: string;
|
|
target: string;
|
|
args?: string;
|
|
appUserModelId?: string;
|
|
toastActivatorClsid?: string;
|
|
iconPath?: string;
|
|
iconIndex?: number;
|
|
workingDir?: string;
|
|
description?: string;
|
|
}
|
|
|
|
function getProgramsDir(): string | null {
|
|
const appData = process.env.APPDATA;
|
|
if (!appData) return null;
|
|
return path.join(appData, 'Microsoft', 'Windows', 'Start Menu', 'Programs');
|
|
}
|
|
|
|
function getWindowsShortcutRepairPaths(): WindowsShortcutRepairPaths | null {
|
|
const currentExe = process.execPath;
|
|
const currentDir = path.dirname(currentExe);
|
|
if (path.basename(currentDir).toLowerCase() !== 'current') {
|
|
return null;
|
|
}
|
|
const rootAppDir = path.dirname(currentDir);
|
|
if (!fs.existsSync(path.join(rootAppDir, 'Update.exe'))) {
|
|
return null;
|
|
}
|
|
const programsDir = getProgramsDir();
|
|
if (!programsDir) return null;
|
|
const shortcutName = `${DESKTOP_APP_NAME}.lnk`;
|
|
return {
|
|
authorShortcut: path.join(programsDir, WINDOWS_SHORTCUT_AUTHOR, shortcutName),
|
|
currentDir,
|
|
currentExe,
|
|
rootAppDir,
|
|
rootShortcut: path.join(programsDir, shortcutName),
|
|
};
|
|
}
|
|
|
|
function isInFluxerRoot(candidate: string, rootDir: string): boolean {
|
|
if (!candidate) return false;
|
|
let fullPath: string;
|
|
try {
|
|
fullPath = path.resolve(candidate);
|
|
} catch {
|
|
return false;
|
|
}
|
|
const normalized = fullPath.replace(/\\+$/, '');
|
|
const normalizedRoot = rootDir.replace(/\\+$/, '');
|
|
return (
|
|
normalized.toLowerCase() === normalizedRoot.toLowerCase() ||
|
|
normalized.toLowerCase().startsWith(normalizedRoot.toLowerCase() + path.sep)
|
|
);
|
|
}
|
|
|
|
interface ShellLinkBinding {
|
|
createShortcut: ((opts: CreateShortcutOptions) => Promise<void>) | null;
|
|
loadError: Error | null;
|
|
}
|
|
|
|
let cachedBinding: ShellLinkBinding | null = null;
|
|
|
|
function loadWinShell(): ShellLinkBinding | null {
|
|
if (cachedBinding) return cachedBinding;
|
|
try {
|
|
cachedBinding = requireModule('@fluxer/win-shell') as ShellLinkBinding;
|
|
} catch (error) {
|
|
console.warn('[WindowsShortcuts] Failed to load @fluxer/win-shell', error);
|
|
return null;
|
|
}
|
|
if (cachedBinding.loadError) {
|
|
console.warn('[WindowsShortcuts] @fluxer/win-shell load error', cachedBinding.loadError);
|
|
}
|
|
return cachedBinding;
|
|
}
|
|
|
|
function lnkContainsString(lnkPath: string, needle: string): boolean {
|
|
let buf: Buffer;
|
|
try {
|
|
buf = fs.readFileSync(lnkPath);
|
|
} catch {
|
|
return false;
|
|
}
|
|
const utf16 = Buffer.from(needle, 'utf16le');
|
|
if (buf.includes(utf16)) return true;
|
|
return buf.includes(Buffer.from(needle, 'utf8'));
|
|
}
|
|
|
|
async function repairOneShortcut(
|
|
shortcutPath: string,
|
|
repair: WindowsShortcutRepairPaths,
|
|
createShortcut: (opts: CreateShortcutOptions) => Promise<void>,
|
|
): Promise<void> {
|
|
if (!fs.existsSync(shortcutPath)) return;
|
|
const rootDir = path.resolve(repair.rootAppDir);
|
|
const alreadyCurrent = lnkContainsString(shortcutPath, repair.currentExe);
|
|
const pointsAtFluxer = lnkContainsString(shortcutPath, rootDir) || isInFluxerRoot(shortcutPath, rootDir);
|
|
const hasLegacyAumid = WINDOWS_LEGACY_APP_USER_MODEL_IDS.some((legacyAumid) =>
|
|
lnkContainsString(shortcutPath, legacyAumid),
|
|
);
|
|
if (!pointsAtFluxer) return;
|
|
if (alreadyCurrent && !hasLegacyAumid) return;
|
|
try {
|
|
await createShortcut({
|
|
lnkPath: shortcutPath,
|
|
target: repair.currentExe,
|
|
appUserModelId: WINDOWS_APP_USER_MODEL_ID,
|
|
toastActivatorClsid: WINDOWS_TOAST_ACTIVATOR_CLSID,
|
|
workingDir: repair.currentDir,
|
|
iconPath: repair.currentExe,
|
|
iconIndex: 0,
|
|
});
|
|
} catch (error) {
|
|
console.warn('[WindowsShortcuts] Failed to rewrite shortcut', {shortcutPath, error});
|
|
}
|
|
}
|
|
|
|
async function repairWindowsShortcutsAsync(repairPaths: WindowsShortcutRepairPaths): Promise<void> {
|
|
const binding = loadWinShell();
|
|
if (!binding?.createShortcut) {
|
|
console.warn('[WindowsShortcuts] @fluxer/win-shell unavailable; skipping shortcut repair');
|
|
return;
|
|
}
|
|
const {createShortcut} = binding;
|
|
const shortcutPaths: Array<string> = [repairPaths.authorShortcut, repairPaths.rootShortcut];
|
|
const desktopDir = process.env.USERPROFILE ? path.join(process.env.USERPROFILE, 'Desktop') : '';
|
|
if (desktopDir) shortcutPaths.push(path.join(desktopDir, `${DESKTOP_APP_NAME}.lnk`));
|
|
if (process.env.APPDATA) {
|
|
const pinnedDir = path.join(
|
|
process.env.APPDATA,
|
|
'Microsoft',
|
|
'Internet Explorer',
|
|
'Quick Launch',
|
|
'User Pinned',
|
|
'TaskBar',
|
|
);
|
|
shortcutPaths.push(path.join(pinnedDir, `${DESKTOP_APP_NAME}.lnk`));
|
|
}
|
|
for (const shortcutPath of shortcutPaths) {
|
|
await repairOneShortcut(shortcutPath, repairPaths, createShortcut);
|
|
}
|
|
}
|
|
|
|
export function repairWindowsShortcuts(): void {
|
|
if (process.platform !== 'win32') return;
|
|
const repairPaths = getWindowsShortcutRepairPaths();
|
|
if (!repairPaths) return;
|
|
repairWindowsShortcutsAsync(repairPaths).catch((error) => {
|
|
console.warn('[WindowsShortcuts] Failed to repair Fluxer shortcuts', error);
|
|
});
|
|
}
|