Improved error logging when file icon extraction fails

This commit is contained in:
Oliver Schwendener 2024-02-23 14:27:37 +01:00
parent 98f8f81900
commit db4cde8be2
36 changed files with 360 additions and 103 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -11,6 +11,7 @@ export type ContextBridge = {
on: IpcRenderer["on"];
};
resetChache: () => Promise<void>;
copyTextToClipboard: (textToCopy: string) => void;
extensionDisabled: (extensionId: string) => void;
extensionEnabled: (extensionId: string) => void;

View File

@ -25,6 +25,11 @@ export class ExtensionManagerModule {
await extensionManager.populateSearchIndex();
ipcMain.handle("resetCache", async () => {
await dependencyRegistry.get("FileImageGenerator").clearCache();
await extensionManager.populateSearchIndex();
});
ipcMain.on("getExtensionTranslations", (event) => {
event.returnValue = extensionManager.getSupportedExtensions().map((extension) => ({
extensionId: extension.id,

View File

@ -1,5 +1,6 @@
export interface FileSystemUtility {
createFolderIfDoesntExist(folderPath: string): Promise<void>;
clearFolder(folderPath: string): Promise<void>;
pathExists(fileOrFolderPath: string): Promise<boolean>;
readJsonFile<T>(filePath: string): Promise<T>;
readJsonFileSync<T>(filePath: string): T;

View File

@ -5,14 +5,21 @@ import {
mkdir,
readFile,
readFileSync,
readdir,
statSync,
unlink,
writeFile,
writeFileSync,
} from "fs";
import { join } from "path";
import type { FileSystemUtility } from "./Contract";
export class NodeJsFileSystemUtility implements FileSystemUtility {
public async clearFolder(folderPath: string): Promise<void> {
const filePaths = await this.readDirectory(folderPath);
await Promise.all(filePaths.map((filePath) => this.removeFile(filePath)));
}
public async createFolderIfDoesntExist(folderPath: string): Promise<void> {
const exists = await this.pathExists(folderPath);
@ -110,4 +117,16 @@ export class NodeJsFileSystemUtility implements FileSystemUtility {
private readFileSync(filePath: string): Buffer {
return readFileSync(filePath);
}
private readDirectory(folderPath: string): Promise<string[]> {
return new Promise((resolve, reject) => {
readdir(folderPath, (error, fileNames) => {
if (error) {
reject(error);
} else {
resolve(fileNames.map((fileName) => join(folderPath, fileName)));
}
});
});
}
}

View File

@ -2,4 +2,5 @@ import type { Image } from "@common/Core/Image";
export interface FileImageGenerator {
getImage(filePath: string): Promise<Image>;
clearCache: () => Promise<void>;
}

View File

@ -8,6 +8,32 @@ import { FileImageGenerator } from "./FileImageGenerator";
describe(FileImageGenerator, () => {
const cacheFolderPath = "cacheFolderPath";
it("should clear cache folder if it exists", async () => {
const pathExistsMock = vi.fn().mockReturnValue(true);
const clearFolderMock = vi.fn().mockReturnValue(Promise.resolve());
const fileSystemUtility = <FileSystemUtility>{
pathExists: (f) => pathExistsMock(f),
clearFolder: (f) => clearFolderMock(f),
};
const fileImageGenerator = new FileImageGenerator(cacheFolderPath, fileSystemUtility);
await fileImageGenerator.clearCache();
expect(pathExistsMock).toHaveBeenCalledWith(cacheFolderPath);
expect(clearFolderMock).toHaveBeenCalledWith(cacheFolderPath);
});
it("should do nothing if cache folder does not exist", async () => {
const pathExistsMock = vi.fn().mockReturnValue(false);
const fileSystemUtility = <FileSystemUtility>{ pathExists: (f) => pathExistsMock(f) };
const fileImageGenerator = new FileImageGenerator(cacheFolderPath, fileSystemUtility);
await fileImageGenerator.clearCache();
expect(pathExistsMock).toHaveBeenCalledWith(cacheFolderPath);
});
it("should create the cached file if it doesn't exist and return the image", async () => {
const pathExistsMock = vi.fn().mockReturnValue(false);
const writePngMock = vi.fn().mockReturnValue(Promise.resolve());

View File

@ -11,6 +11,14 @@ export class FileImageGenerator implements FileImageGeneratorInterface {
private readonly fileSystemUtility: FileSystemUtility,
) {}
public async clearCache(): Promise<void> {
const exists = await this.fileSystemUtility.pathExists(this.cacheFolderPath);
if (exists) {
await this.fileSystemUtility.clearFolder(this.cacheFolderPath);
}
}
public async getImage(filePath: string): Promise<Image> {
const cachedPngFilePath = await this.ensureCachedPngFileExists(filePath);
@ -23,7 +31,13 @@ export class FileImageGenerator implements FileImageGeneratorInterface {
const exists = await this.fileSystemUtility.pathExists(cachedPngFilePath);
if (!exists) {
await this.fileSystemUtility.writePng(getFileIcon(filePath), cachedPngFilePath);
const buffer = getFileIcon(filePath);
if (!buffer.byteLength) {
throw new Error(`getFileIcon returned Buffer with length 0`);
}
await this.fileSystemUtility.writePng(buffer, cachedPngFilePath);
}
return cachedPngFilePath;

View File

@ -1,3 +1,4 @@
import type { BrowserWindowNotifier } from "@Core/BrowserWindowNotifier";
import type { Clock } from "@Core/Clock/Clock";
import { describe, expect, it, vi } from "vitest";
import { Logger } from "./Logger";
@ -7,45 +8,65 @@ describe(Logger, () => {
it("should log an error", () => {
const getCurrentTimeAsStringMock = vi.fn().mockReturnValue(nowAsString);
const notifyMock = vi.fn();
const logger = new Logger(<Clock>{ getCurrentTimeAsString: () => getCurrentTimeAsStringMock() });
const logger = new Logger(
<Clock>{ getCurrentTimeAsString: () => getCurrentTimeAsStringMock() },
<BrowserWindowNotifier>{ notify: (c) => notifyMock(c) },
);
logger.error("This is an error");
expect(logger.getLogs()).toEqual([`[${nowAsString}][ERROR] This is an error`]);
expect(getCurrentTimeAsStringMock).toHaveBeenCalledOnce();
expect(notifyMock).toHaveBeenCalledWith("logsUpdated");
});
it("should log a debug message", () => {
const getCurrentTimeAsStringMock = vi.fn().mockReturnValue(nowAsString);
const notifyMock = vi.fn();
const logger = new Logger(<Clock>{ getCurrentTimeAsString: () => getCurrentTimeAsStringMock() });
const logger = new Logger(
<Clock>{ getCurrentTimeAsString: () => getCurrentTimeAsStringMock() },
<BrowserWindowNotifier>{ notify: (c) => notifyMock(c) },
);
logger.debug("This is a debug message");
expect(logger.getLogs()).toEqual([`[${nowAsString}][DEBUG] This is a debug message`]);
expect(getCurrentTimeAsStringMock).toHaveBeenCalledOnce();
expect(notifyMock).toHaveBeenCalledWith("logsUpdated");
});
it("should log a info message", () => {
const getCurrentTimeAsStringMock = vi.fn().mockReturnValue(nowAsString);
const notifyMock = vi.fn();
const logger = new Logger(<Clock>{ getCurrentTimeAsString: () => getCurrentTimeAsStringMock() });
const logger = new Logger(
<Clock>{ getCurrentTimeAsString: () => getCurrentTimeAsStringMock() },
<BrowserWindowNotifier>{ notify: (c) => notifyMock(c) },
);
logger.info("This is a info message");
expect(logger.getLogs()).toEqual([`[${nowAsString}][INFO] This is a info message`]);
expect(getCurrentTimeAsStringMock).toHaveBeenCalledOnce();
expect(notifyMock).toHaveBeenCalledWith("logsUpdated");
});
it("should log a warning", () => {
const getCurrentTimeAsStringMock = vi.fn().mockReturnValue(nowAsString);
const notifyMock = vi.fn();
const logger = new Logger(<Clock>{ getCurrentTimeAsString: () => getCurrentTimeAsStringMock() });
const logger = new Logger(
<Clock>{ getCurrentTimeAsString: () => getCurrentTimeAsStringMock() },
<BrowserWindowNotifier>{ notify: (c) => notifyMock(c) },
);
logger.warn("This is a warning");
expect(logger.getLogs()).toEqual([`[${nowAsString}][WARNING] This is a warning`]);
expect(getCurrentTimeAsStringMock).toHaveBeenCalledOnce();
expect(notifyMock).toHaveBeenCalledWith("logsUpdated");
});
});

View File

@ -1,10 +1,14 @@
import type { BrowserWindowNotifier } from "@Core/BrowserWindowNotifier";
import type { Clock } from "../Clock";
import type { Logger as LoggerInterface } from "./Contract";
export class Logger implements LoggerInterface {
private logs: string[] = [];
public constructor(private readonly clock: Clock) {}
public constructor(
private readonly clock: Clock,
private readonly browserWindowNotifier: BrowserWindowNotifier,
) {}
public error(message: string): void {
this.writeLog("ERROR", message);
@ -28,5 +32,6 @@ export class Logger implements LoggerInterface {
private writeLog(logLevel: string, message: string) {
this.logs.push(`[${this.clock.getCurrentTimeAsString()}][${logLevel}] ${message}`);
this.browserWindowNotifier.notify("logsUpdated");
}
}

View File

@ -5,9 +5,8 @@ import { Logger } from "./Logger";
export class LoggerModule {
public static bootstrap(dependencyRegistry: DependencyRegistry<Dependencies>) {
const ipcMain = dependencyRegistry.get("IpcMain");
const clock = dependencyRegistry.get("Clock");
const logger = new Logger(clock);
const logger = new Logger(dependencyRegistry.get("Clock"), dependencyRegistry.get("BrowserWindowNotifier"));
dependencyRegistry.register("Logger", logger);
ipcMain.on("getLogs", (event) => (event.returnValue = logger.getLogs()));

View File

@ -21,7 +21,7 @@ export class ApplicationSearch implements Extension {
};
public constructor(
private readonly currentOperatingSystem: OperatingSystem,
private readonly operatingSystem: OperatingSystem,
private readonly applicationRepository: ApplicationRepository,
private readonly settings: Settings,
private readonly assetPathResolver: AssetPathResolver,
@ -34,7 +34,7 @@ export class ApplicationSearch implements Extension {
public isSupported(): boolean {
const supportedOperatingSystems: OperatingSystem[] = ["Windows", "macOS"];
return supportedOperatingSystems.includes(this.currentOperatingSystem);
return supportedOperatingSystems.includes(this.operatingSystem);
}
public getSettingDefaultValue<T>(key: string): T {
@ -51,8 +51,14 @@ export class ApplicationSearch implements Extension {
}
public getImage(): Image {
const fileNames: Record<OperatingSystem, string> = {
Linux: null, // not supported,
macOS: "macos-applications.png",
Windows: "windows-applications.png",
};
return {
url: `file://${this.assetPathResolver.getExtensionAssetPath(this.id, "macos-applications.png")}`,
url: `file://${this.assetPathResolver.getExtensionAssetPath(this.id, fileNames[this.operatingSystem])}`,
};
}

View File

@ -23,12 +23,14 @@ export class ApplicationSearchModule implements ExtensionModule {
dependencyRegistry.get("FileImageGenerator"),
dependencyRegistry.get("Logger"),
settings,
dependencyRegistry.get("AssetPathResolver"),
),
Windows: new WindowsApplicationRepository(
dependencyRegistry.get("PowershellUtility"),
settings,
dependencyRegistry.get("FileImageGenerator"),
dependencyRegistry.get("Logger"),
dependencyRegistry.get("AssetPathResolver"),
),
Linux: undefined, // not supported
};

View File

@ -1,3 +1,4 @@
import type { AssetPathResolver } from "@Core/AssetPathResolver";
import type { FileImageGenerator } from "@Core/ImageGenerator";
import type { Logger } from "@Core/Logger";
import type { PowershellUtility } from "@Core/PowershellUtility";
@ -15,6 +16,7 @@ export class WindowsApplicationRepository implements ApplicationRepository {
private readonly settings: Settings,
private readonly fileImageGenerator: FileImageGenerator,
private readonly logger: Logger,
private readonly assetPathResolver: AssetPathResolver,
) {}
public async getApplications(): Promise<Application[]> {
@ -40,7 +42,14 @@ export class WindowsApplicationRepository implements ApplicationRepository {
const appIcons = await this.getAppIcons(windowsApplicationRetrieverResults.map(({ FullName }) => FullName));
return windowsApplicationRetrieverResults.map(
({ BaseName, FullName }) => new Application(BaseName, FullName, appIcons[FullName]),
({ BaseName, FullName }) =>
new Application(
BaseName,
FullName,
appIcons[FullName] ?? {
url: `file://${this.assetPathResolver.getExtensionAssetPath("ApplicationSearch", "windows-generic-app-icon.png")}`,
},
),
);
}

View File

@ -1,3 +1,4 @@
import type { AssetPathResolver } from "@Core/AssetPathResolver";
import type { CommandlineUtility } from "@Core/CommandlineUtility";
import type { FileImageGenerator } from "@Core/ImageGenerator";
import type { Logger } from "@Core/Logger";
@ -13,6 +14,7 @@ export class MacOsApplicationRepository implements ApplicationRepository {
private readonly fileImageGenerator: FileImageGenerator,
private readonly logger: Logger,
private readonly settings: Settings,
private readonly assetPathResolver: AssetPathResolver,
) {}
public async getApplications(): Promise<Application[]> {
@ -21,7 +23,16 @@ export class MacOsApplicationRepository implements ApplicationRepository {
return filePaths
.filter((filePath) => !!icons[filePath])
.map((filePath) => new Application(parse(filePath).name, filePath, icons[filePath]));
.map(
(filePath) =>
new Application(
parse(filePath).name,
filePath,
icons[filePath] ?? {
url: `file://${this.assetPathResolver.getExtensionAssetPath("ApplicationSearch", "macos-generic-app-icon.png")}`,
},
),
);
}
private async getAllFilePaths(): Promise<string[]> {

View File

@ -26,6 +26,8 @@ import * as Extensions from "./Extensions";
dependencyRegistry.register("SafeStorage", Electron.safeStorage);
// Core Modules
Core.EventEmitterModule.bootstrap(dependencyRegistry);
Core.EventSubscriberModule.bootstrap(dependencyRegistry);
Core.RandomStringProviderModule.bootstrap(dependencyRegistry);
Core.SafeStorageEncryptionModule.bootstrap(dependencyRegistry);
Core.AssetPathResolverModule.bootstrap(dependencyRegistry);
@ -33,10 +35,8 @@ import * as Extensions from "./Extensions";
Core.ClipboardModule.bootstrap(dependencyRegistry);
Core.ClockModule.bootstrap(dependencyRegistry);
Core.AboutUeliModule.bootstrap(dependencyRegistry);
Core.LoggerModule.bootstrap(dependencyRegistry);
Core.EventEmitterModule.bootstrap(dependencyRegistry);
Core.EventSubscriberModule.bootstrap(dependencyRegistry);
Core.BrowserWindowNotifierModule.bootstrap(dependencyRegistry);
Core.LoggerModule.bootstrap(dependencyRegistry);
Core.CommandlineUtilityModule.bootstrap(dependencyRegistry);
Core.FileSystemUtilityModule.bootstrap(dependencyRegistry);
await Core.PowershellUtilityModule.bootstrap(dependencyRegistry);

View File

@ -6,6 +6,7 @@ const contextBridgeImplementation: ContextBridge = {
on: (channel, listener) => ipcRenderer.on(channel, listener),
},
resetChache: () => ipcRenderer.invoke("resetCache"),
copyTextToClipboard: (textToCopy) => ipcRenderer.send("copyTextToClipboard", { textToCopy }),
extensionDisabled: (extensionId) => ipcRenderer.send("extensionDisabled", { extensionId }),
extensionEnabled: (extensionId) => ipcRenderer.send("extensionEnabled", { extensionId }),

View File

@ -102,7 +102,16 @@ export const getCoreTranslations = (): { namespace: string; translations: Transl
translations: {
"en-US": {
title: "Debug",
resetCache: "Reset cache",
resetCacheHint: "This might help when troubleshooting issues with icons.",
resettingCache: "Resetting cache...",
resetCacheDialogTitle: "Are you sure?",
resetCacheDialogContent: "You are about to reset the cache. This might take a few seconds.",
resetCacheCancel: "Cancel",
resetCacheConfirm: "Reset Cache",
resetAllSettings: "Reset all settings",
resetAllSettingsHint:
"This resets all settings to their default values. You will lose all settings that you changed.",
resetAllSettingsButton: "Reset",
resetAllSettingsDialogTitle: "Are you sure?",
resetAllSettingsDialogContent: "You are about to reset all settings to their default values.",
@ -111,7 +120,17 @@ export const getCoreTranslations = (): { namespace: string; translations: Transl
},
"de-CH": {
title: "Fehlerbehebung",
resetCache: "Zwischenspeicher zurücksetzen",
resetCacheHint: "Dies kann bei der Behebung von Problemen mit Symbolen hilfreich sein.",
resettingCache: "Zwischenspeicher wird zurückgesetzt...",
resetCacheDialogTitle: "Bist du sicher?",
resetCacheDialogContent:
"Du bist dabei, den Cache zurückzusetzen. Dies kann ein paar Sekunden dauern.",
resetCacheCancel: "Abbrechen",
resetCacheConfirm: "Zwischenspeicher zurücksetzen",
resetAllSettings: "Alle Einstellungen zurücksetzen",
resetAllSettingsHint:
"Dadurch werden alle Einstellungen auf ihre Standardwerte zurückgesetzt. Du verlierst alle Einstellungen, die du geändert hast.",
resetAllSettingsButton: "Zurücksetzen",
resetAllSettingsDialogTitle: "Bist du sicher?",
resetAllSettingsDialogContent:

View File

@ -53,7 +53,7 @@ export const FavoritesList = ({ invokeSearchResultItem }: FavoritesListProps) =>
style={{ width: "100%" }}
src={getImageUrl({
image: searchResultItem.image,
onDarkBackground: contextBridge.themeShouldUseDarkColors(),
shouldPreferDarkColors: contextBridge.themeShouldUseDarkColors(),
})}
alt="App Name Document"
/>

View File

@ -103,7 +103,7 @@ export const SearchResultListItem = ({
}}
src={getImageUrl({
image: searchResultItem.image,
onDarkBackground: contextBridge.themeShouldUseDarkColors(),
shouldPreferDarkColors: contextBridge.themeShouldUseDarkColors(),
})}
/>
</div>

View File

@ -58,7 +58,7 @@ export const Navigation = ({ settingsPages, enabledExtensions }: NavigationProps
style={{ maxWidth: "100%", maxHeight: "100%" }}
src={getImageUrl({
image: e.image,
onDarkBackground: contextBridge.themeShouldUseDarkColors(),
shouldPreferDarkColors: contextBridge.themeShouldUseDarkColors(),
})}
/>
</div>

View File

@ -1,73 +0,0 @@
import {
Button,
Dialog,
DialogActions,
DialogBody,
DialogContent,
DialogSurface,
DialogTitle,
DialogTrigger,
Field,
} from "@fluentui/react-components";
import { useTranslation } from "react-i18next";
import { useContextBridge, useTheme } from "../../Hooks";
import { Section } from "../Section";
import { SectionList } from "../SectionList";
export const Debug = () => {
const { t } = useTranslation();
const ns = "settingsDebug";
const { contextBridge } = useContextBridge();
const { theme } = useTheme();
return (
<SectionList>
<Section>
<Field label={t("resetAllSettings", { ns })}>
<Dialog>
<DialogTrigger disableButtonEnhancement>
<div>
<Button>{t("resetAllSettingsButton", { ns })}</Button>
</div>
</DialogTrigger>
<DialogSurface>
<DialogBody>
<DialogTitle>{t("resetAllSettingsDialogTitle", { ns })}</DialogTitle>
<DialogContent>{t("resetAllSettingsDialogContent", { ns })}</DialogContent>
<DialogActions>
<DialogTrigger disableButtonEnhancement>
<Button appearance="secondary">{t("resetAllSettingsCancel", { ns })}</Button>
</DialogTrigger>
<Button onClick={() => contextBridge.resetAllSettings()} appearance="primary">
{t("resetAllSettingsConfirm", { ns })}
</Button>
</DialogActions>
</DialogBody>
</DialogSurface>
</Dialog>
</Field>
</Section>
<Section>
<Field label="Logs">
<textarea
id="logs"
readOnly
value={contextBridge.getLogs().join("\n\n")}
style={{
height: 150,
width: "100%",
fontFamily: theme.fontFamilyMonospace,
fontSize: theme.fontSizeBase200,
resize: "vertical",
background: theme.colorNeutralBackground1,
color: theme.colorNeutralForeground1,
borderRadius: theme.borderRadiusMedium,
padding: 10,
boxSizing: "border-box",
}}
/>
</Field>
</Section>
</SectionList>
);
};

View File

@ -0,0 +1,21 @@
import { Section } from "../../Section";
import { SectionList } from "../../SectionList";
import { Logs } from "./Logs";
import { ResetCache } from "./ResetCache";
import { ResetSettings } from "./ResetSettings";
export const Debug = () => {
return (
<SectionList>
<Section>
<ResetCache />
</Section>
<Section>
<ResetSettings />
</Section>
<Section>
<Logs />
</Section>
</SectionList>
);
};

View File

@ -0,0 +1,38 @@
import { useContextBridge, useTheme } from "@Core/Hooks";
import { Field } from "@fluentui/react-components";
import { useEffect, useState } from "react";
export const Logs = () => {
const { contextBridge } = useContextBridge();
const { theme } = useTheme();
const [logs, setLogs] = useState<string[]>(contextBridge.getLogs());
useEffect(() => {
contextBridge.ipcRenderer.on("logsUpdated", () => {
setLogs(contextBridge.getLogs());
});
});
return (
<Field label="Logs">
<textarea
id="logs"
readOnly
value={logs.join("\n\n")}
style={{
height: 150,
width: "100%",
fontFamily: theme.fontFamilyMonospace,
fontSize: theme.fontSizeBase200,
resize: "vertical",
background: theme.colorNeutralBackground1,
color: theme.colorNeutralForeground1,
borderRadius: theme.borderRadiusMedium,
padding: 10,
boxSizing: "border-box",
}}
/>
</Field>
);
};

View File

@ -0,0 +1,83 @@
import { useContextBridge } from "@Core/Hooks";
import {
Button,
Dialog,
DialogActions,
DialogBody,
DialogContent,
DialogSurface,
DialogTitle,
DialogTrigger,
Field,
Spinner,
} from "@fluentui/react-components";
import { useState } from "react";
import { useTranslation } from "react-i18next";
export const ResetCache = () => {
const { t } = useTranslation();
const ns = "settingsDebug";
const { contextBridge } = useContextBridge();
const [dialogIsOpen, setDialogIsOpen] = useState<boolean>(false);
const [resetIsRunning, setResetIsRunning] = useState<boolean>(false);
const resetChache = async () => {
setResetIsRunning(true);
try {
await contextBridge.resetChache();
} catch (error) {
// do nothing
} finally {
setResetIsRunning(false);
setDialogIsOpen(false);
}
};
return (
<Field label={t("resetCache", { ns })} hint={t("resetCacheHint", { ns })}>
<Dialog open={dialogIsOpen}>
<DialogTrigger disableButtonEnhancement>
<div>
<Button onClick={() => setDialogIsOpen(true)}>{t("resetCache", { ns })}</Button>
</div>
</DialogTrigger>
<DialogSurface>
<DialogBody>
<DialogTitle>
{resetIsRunning ? t("resettingCache", { ns }) : t("resetCacheDialogTitle", { ns })}
</DialogTitle>
<DialogContent>
{resetIsRunning ? (
<div
style={{
height: 100,
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<Spinner />
</div>
) : (
t("resetCacheDialogContent", { ns })
)}
</DialogContent>
<DialogActions>
<Button
disabled={resetIsRunning}
appearance="secondary"
onClick={() => setDialogIsOpen(false)}
>
{t("resetCacheCancel", { ns })}
</Button>
<Button disabled={resetIsRunning} onClick={() => resetChache()} appearance="primary">
{t("resetCacheConfirm", { ns })}
</Button>
</DialogActions>
</DialogBody>
</DialogSurface>
</Dialog>
</Field>
);
};

View File

@ -0,0 +1,45 @@
import { useContextBridge } from "@Core/Hooks";
import {
Button,
Dialog,
DialogActions,
DialogBody,
DialogContent,
DialogSurface,
DialogTitle,
DialogTrigger,
Field,
} from "@fluentui/react-components";
import { useTranslation } from "react-i18next";
export const ResetSettings = () => {
const { t } = useTranslation();
const ns = "settingsDebug";
const { contextBridge } = useContextBridge();
return (
<Field label={t("resetAllSettings", { ns })} hint={t("resetAllSettingsHint", { ns })}>
<Dialog>
<DialogTrigger disableButtonEnhancement>
<div>
<Button>{t("resetAllSettingsButton", { ns })}</Button>
</div>
</DialogTrigger>
<DialogSurface>
<DialogBody>
<DialogTitle>{t("resetAllSettingsDialogTitle", { ns })}</DialogTitle>
<DialogContent>{t("resetAllSettingsDialogContent", { ns })}</DialogContent>
<DialogActions>
<DialogTrigger disableButtonEnhancement>
<Button appearance="secondary">{t("resetAllSettingsCancel", { ns })}</Button>
</DialogTrigger>
<Button onClick={() => contextBridge.resetAllSettings()} appearance="primary">
{t("resetAllSettingsConfirm", { ns })}
</Button>
</DialogActions>
</DialogBody>
</DialogSurface>
</Dialog>
</Field>
);
};

View File

@ -0,0 +1 @@
export * from "./Debug";

View File

@ -101,7 +101,8 @@ export const Extensions = () => {
style={{ maxWidth: "100%", maxHeight: "100%" }}
src={getImageUrl({
image,
onDarkBackground: contextBridge.themeShouldUseDarkColors(),
shouldPreferDarkColors:
contextBridge.themeShouldUseDarkColors(),
})}
/>
</div>

View File

@ -50,7 +50,7 @@ export const Favorites = () => {
style={{ maxWidth: "100%", maxHeight: "100%" }}
src={getImageUrl({
image: s.image,
onDarkBackground: contextBridge.themeShouldUseDarkColors(),
shouldPreferDarkColors: contextBridge.themeShouldUseDarkColors(),
})}
/>
</div>

View File

@ -99,7 +99,7 @@ export const SearchEngine = () => {
style={{ width: 16, height: 16 }}
src={getImageUrl({
image,
onDarkBackground: contextBridge.themeShouldUseDarkColors(),
shouldPreferDarkColors: contextBridge.themeShouldUseDarkColors(),
})}
/>
}

View File

@ -6,7 +6,7 @@ describe(getImageUrl, () => {
expect(
getImageUrl({
image: { url: "neutral", urlOnDarkBackground: "dark", urlOnLightBackground: "light" },
onDarkBackground: true,
shouldPreferDarkColors: true,
}),
).toBe("dark");
});
@ -15,7 +15,7 @@ describe(getImageUrl, () => {
expect(
getImageUrl({
image: { url: "neutral", urlOnDarkBackground: "dark", urlOnLightBackground: "light" },
onDarkBackground: false,
shouldPreferDarkColors: false,
}),
).toBe("light");
});
@ -24,7 +24,7 @@ describe(getImageUrl, () => {
expect(
getImageUrl({
image: { url: "neutral", urlOnLightBackground: "light" },
onDarkBackground: true,
shouldPreferDarkColors: true,
}),
).toBe("neutral");
});
@ -33,7 +33,7 @@ describe(getImageUrl, () => {
expect(
getImageUrl({
image: { url: "neutral", urlOnDarkBackground: "dark" },
onDarkBackground: false,
shouldPreferDarkColors: false,
}),
).toBe("neutral");
});

View File

@ -1,5 +1,5 @@
import type { Image } from "@common/Core/Image";
export const getImageUrl = ({ image, onDarkBackground }: { image: Image; onDarkBackground: boolean }) => {
return (onDarkBackground ? image.urlOnDarkBackground : image.urlOnLightBackground) ?? image.url;
export const getImageUrl = ({ image, shouldPreferDarkColors }: { image: Image; shouldPreferDarkColors: boolean }) => {
return shouldPreferDarkColors ? image.urlOnDarkBackground ?? image.url : image.urlOnLightBackground ?? image.url;
};

View File

@ -49,7 +49,7 @@ export const DeeplTranslator = ({ contextBridge, goBack }: ExtensionProps) => {
alt="DeepL Logo"
src={getImageUrl({
image: contextBridge.getExtension(extensionId).image,
onDarkBackground: contextBridge.themeShouldUseDarkColors(),
shouldPreferDarkColors: contextBridge.themeShouldUseDarkColors(),
})}
style={{ width: 24 }}
/>

View File

@ -69,7 +69,8 @@ export const ShortcutsSettings = () => {
style={{ maxWidth: "100%", maxHeight: "100%" }}
src={getImageUrl({
image: searchResultItem.image,
onDarkBackground: contextBridge.themeShouldUseDarkColors(),
shouldPreferDarkColors:
contextBridge.themeShouldUseDarkColors(),
})}
/>
</div>