Improve desktop icon quality and generation
This commit refactors the icon and logo generation process by replacing multiple dependencies with ImageMagick. This simplifies the build process and enhances maintainability. Key changes: - Remove unnecessary icon files for macOS (.icns) and Linux (size-specific PNGs). Electron-builder can now auto-generate these from a single `logo.png` starting from version 19.54.0, see: - electron-userland/electron-builder#1682 - electron-userland/electron-builder#2533 - Retain `ico` generation with multiple sizes to fix pixelated/bad looking icons on Windows, see: - electron-userland/electron-builder#7328 - electron-userland/electron-builder#3867 - Replaced `svgexport`, `icon-gen`, and `electron-icon-builder` dependencies with ImageMagick, addressing issues with outdated dependencies and unreliable CI/CD builds. - Move electron-builder build resources to `src/presentation/electron/build` for better project structure. - Improve `electron-builder` configuration file by making it importable/reusable without prebuilding the Electron application.
4
.github/workflows/checks.scripts.yaml
vendored
@@ -15,6 +15,10 @@ jobs:
|
|||||||
-
|
-
|
||||||
name: Checkout
|
name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
-
|
||||||
|
name: Install ImageMagick on macOS
|
||||||
|
if: matrix.os == 'macos'
|
||||||
|
run: brew install imagemagick
|
||||||
-
|
-
|
||||||
name: Setup node
|
name: Setup node
|
||||||
uses: ./.github/actions/setup-node
|
uses: ./.github/actions/setup-node
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
# build
|
|
||||||
|
|
||||||
This folder contains files that are used by Electron to serve the desktop version.
|
|
||||||
|
|
||||||
Icons are created from the main logo file and should not be changed manually, see [related documentation](./../img/README.md).
|
|
||||||
|
Before Width: | Height: | Size: 71 KiB |
|
Before Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 553 B |
|
Before Width: | Height: | Size: 963 B |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 2.9 KiB |
@@ -23,8 +23,10 @@ The presentation layer uses an event-driven architecture for bidirectional react
|
|||||||
- [**`styles/`**](./../src/presentation/assets/styles/): Contains shared styles.
|
- [**`styles/`**](./../src/presentation/assets/styles/): Contains shared styles.
|
||||||
- [**`main.scss`**](./../src/presentation/assets/styles/main.scss): Main Sass file, imported by other components as single entrypoint..
|
- [**`main.scss`**](./../src/presentation/assets/styles/main.scss): Main Sass file, imported by other components as single entrypoint..
|
||||||
- [**`electron/`**](./../src/presentation/electron/): Contains Electron code.
|
- [**`electron/`**](./../src/presentation/electron/): Contains Electron code.
|
||||||
- [`/main/` **`index.ts`**](./../src/presentation/main.ts): Main entry for Electron, managing application windows and lifecycle events.
|
- [`/main/` **`index.ts`**](./../src/presentation/electron/main/index.ts): Main entry for Electron, managing application windows and lifecycle events.
|
||||||
- [`/preload/` **`index.ts`**](./../src/presentation/main.ts): Script executed before the renderer, securing Node.js features for renderer use.
|
- [`/preload/` **`index.ts`**](./../src/presentation/electron/preload/index.ts): Script executed before the renderer, securing Node.js features for renderer use.
|
||||||
|
- [**`/shared/`**](./../src/presentation/electron/shared/): Shared logic between different Electron processes.
|
||||||
|
- [**`/build/`**](./../src/presentation/electron/build/): `electron-builder` build resources directory, [README.md](./../src/presentation/electron/build/README.md).
|
||||||
- [**`/vite.config.ts`**](./../vite.config.ts): Contains Vite configurations for building web application.
|
- [**`/vite.config.ts`**](./../vite.config.ts): Contains Vite configurations for building web application.
|
||||||
- [**`/electron.vite.config.ts`**](./../electron.vite.config.ts): Contains Vite configurations for building desktop applications.
|
- [**`/electron.vite.config.ts`**](./../electron.vite.config.ts): Contains Vite configurations for building desktop applications.
|
||||||
- [**`/postcss.config.cjs`**](./../postcss.config.cjs): Contains PostCSS configurations for Vite.
|
- [**`/postcss.config.cjs`**](./../postcss.config.cjs): Contains PostCSS configurations for Vite.
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/* eslint-disable no-template-curly-in-string */
|
/* eslint-disable no-template-curly-in-string */
|
||||||
|
|
||||||
const { join } = require('node:path');
|
const { join, resolve } = require('node:path');
|
||||||
const { readdirSync } = require('fs');
|
const { readdirSync, existsSync } = require('node:fs');
|
||||||
const { electronBundled, electronUnbundled } = require('./dist-dirs.json');
|
const { electronBundled, electronUnbundled } = require('./dist-dirs.json');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -17,6 +17,7 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
directories: {
|
directories: {
|
||||||
output: electronBundled,
|
output: electronBundled,
|
||||||
|
buildResources: resolvePathFromProjectRoot('src/presentation/electron/build'),
|
||||||
},
|
},
|
||||||
extraMetadata: {
|
extraMetadata: {
|
||||||
main: findMainEntryFile(
|
main: findMainEntryFile(
|
||||||
@@ -53,10 +54,18 @@ module.exports = {
|
|||||||
* Finds by accommodating different JS file extensions and module formats.
|
* Finds by accommodating different JS file extensions and module formats.
|
||||||
*/
|
*/
|
||||||
function findMainEntryFile(parentDirectory) {
|
function findMainEntryFile(parentDirectory) {
|
||||||
const files = readdirSync(parentDirectory);
|
const absoluteParentDirectory = resolvePathFromProjectRoot(parentDirectory);
|
||||||
|
if (!existsSync(absoluteParentDirectory)) {
|
||||||
|
return null; // Avoid disrupting other processes such `npm install`.
|
||||||
|
}
|
||||||
|
const files = readdirSync(absoluteParentDirectory);
|
||||||
const entryFile = files.find((file) => /^index\.(cjs|mjs|js)$/.test(file));
|
const entryFile = files.find((file) => /^index\.(cjs|mjs|js)$/.test(file));
|
||||||
if (!entryFile) {
|
if (!entryFile) {
|
||||||
throw new Error(`Main entry file not found in ${parentDirectory}.`);
|
throw new Error(`Main entry file not found in ${absoluteParentDirectory}.`);
|
||||||
}
|
}
|
||||||
return join(parentDirectory, entryFile);
|
return join(parentDirectory, entryFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolvePathFromProjectRoot(pathSegment) {
|
||||||
|
return resolve(__dirname, pathSegment);
|
||||||
|
}
|
||||||
|
|||||||
3920
package-lock.json
generated
@@ -62,13 +62,11 @@
|
|||||||
"electron": "^29.3.0",
|
"electron": "^29.3.0",
|
||||||
"electron-builder": "^24.13.3",
|
"electron-builder": "^24.13.3",
|
||||||
"electron-devtools-installer": "^3.2.0",
|
"electron-devtools-installer": "^3.2.0",
|
||||||
"electron-icon-builder": "^2.0.1",
|
|
||||||
"electron-vite": "^2.1.0",
|
"electron-vite": "^2.1.0",
|
||||||
"eslint": "8.57.0",
|
"eslint": "8.57.0",
|
||||||
"eslint-plugin-cypress": "^2.15.1",
|
"eslint-plugin-cypress": "^2.15.1",
|
||||||
"eslint-plugin-vue": "^9.25.0",
|
"eslint-plugin-vue": "^9.25.0",
|
||||||
"eslint-plugin-vuejs-accessibility": "^2.2.1",
|
"eslint-plugin-vuejs-accessibility": "^2.2.1",
|
||||||
"icon-gen": "^4.0.0",
|
|
||||||
"jsdom": "^24.0.0",
|
"jsdom": "^24.0.0",
|
||||||
"markdownlint-cli": "^0.39.0",
|
"markdownlint-cli": "^0.39.0",
|
||||||
"postcss": "^8.4.38",
|
"postcss": "^8.4.38",
|
||||||
@@ -78,7 +76,6 @@
|
|||||||
"remark-validate-links": "^13.0.1",
|
"remark-validate-links": "^13.0.1",
|
||||||
"sass": "^1.75.0",
|
"sass": "^1.75.0",
|
||||||
"start-server-and-test": "^2.0.3",
|
"start-server-and-test": "^2.0.3",
|
||||||
"svgexport": "^0.4.2",
|
|
||||||
"terser": "^5.30.3",
|
"terser": "^5.30.3",
|
||||||
"tslib": "^2.6.2",
|
"tslib": "^2.6.2",
|
||||||
"typescript": "^5.4.5",
|
"typescript": "^5.4.5",
|
||||||
|
|||||||
@@ -2,94 +2,119 @@
|
|||||||
* Description:
|
* Description:
|
||||||
* This script updates the logo images across the project based on the primary
|
* This script updates the logo images across the project based on the primary
|
||||||
* logo file ('img/logo.svg' file).
|
* logo file ('img/logo.svg' file).
|
||||||
|
*
|
||||||
* It handles the creation and update of various icon sizes for different purposes,
|
* It handles the creation and update of various icon sizes for different purposes,
|
||||||
* including desktop launcher icons, tray icons, and web favicons from a single source
|
* including desktop launcher icons, tray icons, and web favicons from a single source
|
||||||
* SVG logo file.
|
* SVG logo file.
|
||||||
*
|
*
|
||||||
* Usage:
|
* Usage:
|
||||||
* node ./scripts/logo-update.js
|
* node ./scripts/logo-update.js
|
||||||
|
*
|
||||||
|
* Notes:
|
||||||
|
* ImageMagick must be installed and accessible in the system's PATH
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { resolve, join } from 'node:path';
|
import { resolve, join, dirname } from 'node:path';
|
||||||
import { rm, mkdtemp, stat } from 'node:fs/promises';
|
import { stat } from 'node:fs/promises';
|
||||||
import { spawn } from 'node:child_process';
|
import { spawn } from 'node:child_process';
|
||||||
import { URL, fileURLToPath } from 'node:url';
|
import { URL, fileURLToPath } from 'node:url';
|
||||||
|
import electronBuilderConfig from '../electron-builder.cjs';
|
||||||
|
|
||||||
class Paths {
|
class ImageAssetPaths {
|
||||||
constructor(selfDirectory) {
|
constructor(currentScriptDirectory) {
|
||||||
const projectRoot = resolve(selfDirectory, '../');
|
const projectRoot = resolve(currentScriptDirectory, '../');
|
||||||
this.sourceImage = join(projectRoot, 'img/logo.svg');
|
this.sourceImage = join(projectRoot, 'img/logo.svg');
|
||||||
this.publicDirectory = join(projectRoot, 'src/presentation/public');
|
this.publicDirectory = join(projectRoot, 'src/presentation/public');
|
||||||
this.electronBuildDirectory = join(projectRoot, 'build');
|
this.electronBuildResourcesDirectory = electronBuilderConfig.directories.buildResources;
|
||||||
|
}
|
||||||
|
|
||||||
|
get electronTrayIconFile() {
|
||||||
|
return join(this.publicDirectory, 'icon.png');
|
||||||
|
}
|
||||||
|
|
||||||
|
get webFaviconFile() {
|
||||||
|
return join(this.publicDirectory, 'favicon.ico');
|
||||||
}
|
}
|
||||||
|
|
||||||
toString() {
|
toString() {
|
||||||
return `Source image: ${this.sourceImage}\n`
|
return `Source image: ${this.sourceImage}`
|
||||||
+ `Public directory: ${this.publicDirectory}\n`
|
+ `\nPublic directory: ${this.publicDirectory}`
|
||||||
+ `Electron build directory: ${this.electronBuildDirectory}`;
|
+ `\n\t Electron tray icon file: ${this.electronTrayIconFile}`
|
||||||
|
+ `\n\t Web favicon file: ${this.webFaviconFile}`
|
||||||
|
+ `\nElectron build directory: ${this.electronBuildResourcesDirectory}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
const paths = new Paths(getCurrentScriptDirectory());
|
const paths = new ImageAssetPaths(getCurrentScriptDirectory());
|
||||||
console.log(`Paths:\n\t${paths.toString().replaceAll('\n', '\n\t')}`);
|
console.log(`Paths:\n\t${paths.toString().replaceAll('\n', '\n\t')}`);
|
||||||
await updateDesktopLauncherAndTrayIcon(paths.sourceImage, paths.publicDirectory);
|
const convertCommand = await findAvailableImageMagickCommand();
|
||||||
await updateWebFavicon(paths.sourceImage, paths.publicDirectory);
|
await generateDesktopAndTrayIcons(
|
||||||
await updateDesktopIcons(paths.sourceImage, paths.electronBuildDirectory);
|
paths.sourceImage,
|
||||||
|
paths.electronTrayIconFile,
|
||||||
|
convertCommand,
|
||||||
|
);
|
||||||
|
await generateWebFavicon(
|
||||||
|
paths.sourceImage,
|
||||||
|
paths.webFaviconFile,
|
||||||
|
convertCommand,
|
||||||
|
);
|
||||||
|
await generateDesktopIcons(
|
||||||
|
paths.sourceImage,
|
||||||
|
paths.electronBuildResourcesDirectory,
|
||||||
|
convertCommand,
|
||||||
|
);
|
||||||
console.log('🎉 (Re)created icons successfully.');
|
console.log('🎉 (Re)created icons successfully.');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateDesktopLauncherAndTrayIcon(sourceImage, publicFolder) {
|
async function generateDesktopAndTrayIcons(sourceImage, targetFile, convertCommand) {
|
||||||
|
// Reference: https://web.archive.org/web/20240502124306/https://www.electronjs.org/docs/latest/api/tray
|
||||||
|
console.log(`Updating desktop launcher and tray icon at ${targetFile}.`);
|
||||||
await ensureFileExists(sourceImage);
|
await ensureFileExists(sourceImage);
|
||||||
await ensureFolderExists(publicFolder);
|
await ensureParentFolderExists(targetFile);
|
||||||
const electronTrayIconFile = join(publicFolder, 'icon.png');
|
await convertFromSvgToPng(
|
||||||
console.log(`Updating desktop launcher and tray icon at ${electronTrayIconFile}.`);
|
convertCommand,
|
||||||
await runCommand(
|
|
||||||
'npx',
|
|
||||||
'svgexport',
|
|
||||||
sourceImage,
|
sourceImage,
|
||||||
electronTrayIconFile,
|
targetFile,
|
||||||
|
'512x512',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateWebFavicon(sourceImage, faviconFolder) {
|
async function generateWebFavicon(sourceImage, faviconFilePath, convertCommand) {
|
||||||
console.log('Updating favicon');
|
console.log(`Updating favicon at ${faviconFilePath}.`);
|
||||||
await ensureFileExists(sourceImage);
|
await ensureFileExists(sourceImage);
|
||||||
await ensureFolderExists(faviconFolder);
|
await ensureParentFolderExists(faviconFilePath);
|
||||||
await runCommand(
|
await convertFromSvgToIco(
|
||||||
'npx',
|
convertCommand,
|
||||||
'icon-gen',
|
sourceImage,
|
||||||
`--input ${sourceImage}`,
|
faviconFilePath,
|
||||||
`--output ${faviconFolder}`,
|
[16, 24, 32, 48, 64, 128, 256],
|
||||||
'--ico',
|
|
||||||
'--ico-name \'favicon\'',
|
|
||||||
'--report',
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateDesktopIcons(sourceImage, electronIconsDir) {
|
async function generateDesktopIcons(sourceImage, electronBuildResourcesDirectory, convertCommand) {
|
||||||
|
console.log(`Creating Electron icon files to ${electronBuildResourcesDirectory}.`);
|
||||||
|
// Reference: https://web.archive.org/web/20240501103645/https://www.electron.build/icons.html
|
||||||
|
await ensureFolderExists(electronBuildResourcesDirectory);
|
||||||
await ensureFileExists(sourceImage);
|
await ensureFileExists(sourceImage);
|
||||||
await ensureFolderExists(electronIconsDir);
|
const electronMainIconFile = join(electronBuildResourcesDirectory, 'icon.png');
|
||||||
const temporaryDir = await mkdtemp('icon-');
|
await convertFromSvgToPng(
|
||||||
const temporaryPngFile = join(temporaryDir, 'icon.png');
|
convertCommand,
|
||||||
console.log(`Converting from SVG (${sourceImage}) to PNG: ${temporaryPngFile}`); // required by `icon-builder`
|
|
||||||
await runCommand(
|
|
||||||
'npx',
|
|
||||||
'svgexport',
|
|
||||||
sourceImage,
|
sourceImage,
|
||||||
temporaryPngFile,
|
electronMainIconFile,
|
||||||
'1024:1024',
|
'1024x1024', // Should be at least 512x512
|
||||||
);
|
);
|
||||||
console.log(`Creating electron icons to ${electronIconsDir}.`);
|
// Relying on `electron-builder`s conversion from png to ico results in pixelated look on Windows
|
||||||
await runCommand(
|
// 10 and 11 according to tests, see:
|
||||||
'npx',
|
// - https://web.archive.org/web/20240502114650/https://github.com/electron-userland/electron-builder/issues/7328
|
||||||
'electron-icon-builder',
|
// - https://web.archive.org/web/20240502115448/https://github.com/electron-userland/electron-builder/issues/3867
|
||||||
`--input="${temporaryPngFile}"`,
|
const electronWindowsIconFile = join(electronBuildResourcesDirectory, 'icon.ico');
|
||||||
`--output="${electronIconsDir}"`,
|
await convertFromSvgToIco(
|
||||||
'--flatten',
|
convertCommand,
|
||||||
|
sourceImage,
|
||||||
|
electronWindowsIconFile,
|
||||||
|
[16, 24, 32, 48, 64, 128, 256],
|
||||||
);
|
);
|
||||||
console.log('Cleaning up temporary directory.');
|
|
||||||
await rm(temporaryDir, { recursive: true, force: true });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function ensureFileExists(filePath) {
|
async function ensureFileExists(filePath) {
|
||||||
@@ -100,12 +125,60 @@ async function ensureFileExists(filePath) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function ensureFolderExists(folderPath) {
|
async function ensureFolderExists(folderPath) {
|
||||||
|
if (!folderPath) {
|
||||||
|
throw new Error('Path is missing');
|
||||||
|
}
|
||||||
const path = await stat(folderPath);
|
const path = await stat(folderPath);
|
||||||
if (!path.isDirectory()) {
|
if (!path.isDirectory()) {
|
||||||
throw new Error(`Not a directory: ${folderPath}`);
|
throw new Error(`Not a directory: ${folderPath}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ensureParentFolderExists(filePath) {
|
||||||
|
return ensureFolderExists(dirname(filePath));
|
||||||
|
}
|
||||||
|
|
||||||
|
const BaseImageMagickConvertArguments = Object.freeze([
|
||||||
|
'-background none', // Transparent, so they do not get filled with white.
|
||||||
|
'-strip', // Strip metadata.
|
||||||
|
'-gravity Center', // Center the image when there's empty space
|
||||||
|
]);
|
||||||
|
|
||||||
|
async function convertFromSvgToIco(
|
||||||
|
convertCommand,
|
||||||
|
inputFile,
|
||||||
|
outputFile,
|
||||||
|
sizes,
|
||||||
|
) {
|
||||||
|
await runCommand(
|
||||||
|
convertCommand,
|
||||||
|
...BaseImageMagickConvertArguments,
|
||||||
|
`-density ${Math.max(...sizes).toString()}`, // High enough for sharpness
|
||||||
|
`-define icon:auto-resize=${sizes.map((s) => s.toString()).join(',')}`, // Automatically store multiple sizes in an ico image
|
||||||
|
'-compress None',
|
||||||
|
inputFile,
|
||||||
|
outputFile,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function convertFromSvgToPng(
|
||||||
|
convertCommand,
|
||||||
|
inputFile,
|
||||||
|
outputFile,
|
||||||
|
size = undefined,
|
||||||
|
) {
|
||||||
|
await runCommand(
|
||||||
|
convertCommand,
|
||||||
|
...BaseImageMagickConvertArguments,
|
||||||
|
...(size === undefined ? [] : [
|
||||||
|
`-resize ${size}`,
|
||||||
|
`-density ${size}`, // High enough for sharpness
|
||||||
|
]),
|
||||||
|
inputFile,
|
||||||
|
outputFile,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async function runCommand(...args) {
|
async function runCommand(...args) {
|
||||||
const command = args.join(' ');
|
const command = args.join(' ');
|
||||||
console.log(`Running command: ${command}`);
|
console.log(`Running command: ${command}`);
|
||||||
@@ -135,4 +208,27 @@ function getCurrentScriptDirectory() {
|
|||||||
return fileURLToPath(new URL('.', import.meta.url));
|
return fileURLToPath(new URL('.', import.meta.url));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function findAvailableImageMagickCommand() {
|
||||||
|
// Reference: https://web.archive.org/web/20240502120041/https://imagemagick.org/script/convert.php
|
||||||
|
const potentialBaseCommands = [
|
||||||
|
'convert', // Legacy command, usually available on Linux/macOS installations
|
||||||
|
'magick convert', // Newer command, available on Windows installations
|
||||||
|
];
|
||||||
|
for (const baseCommand of potentialBaseCommands) {
|
||||||
|
const testCommand = `${baseCommand} -version`;
|
||||||
|
try {
|
||||||
|
await runCommand(testCommand); // eslint-disable-line no-await-in-loop
|
||||||
|
console.log(`Confirmed: ImageMagick command '${baseCommand}' is available and operational.`);
|
||||||
|
return baseCommand;
|
||||||
|
} catch (err) {
|
||||||
|
console.log(`Error: The command '${baseCommand}' is not found or failed to execute. Detailed error: ${err.message}"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Error([
|
||||||
|
'Unable to locate any operational ImageMagick command.',
|
||||||
|
`Attempted commands were: ${potentialBaseCommands.join(', ')}.`,
|
||||||
|
'Please ensure ImageMagick is correctly installed and accessible.',
|
||||||
|
].join('\n'));
|
||||||
|
}
|
||||||
|
|
||||||
await main();
|
await main();
|
||||||
|
|||||||
13
src/presentation/electron/build/README.md
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# build
|
||||||
|
|
||||||
|
This folder is the *build resources directory* [1] and contains files used by electron-build to
|
||||||
|
create the desktop version of the application.
|
||||||
|
|
||||||
|
Generate icons from the main logo file.
|
||||||
|
Do not modify these icons manually.
|
||||||
|
For more details, see the [related documentation](./../../../../img/README.md).
|
||||||
|
|
||||||
|
The electron-builder [1] uses these files, as specified in the [configuration file](./../../../../electron-builder.cjs).
|
||||||
|
|
||||||
|
For more information on icons in electron-builder,
|
||||||
|
visit [Icons - electron-builder | www.electron.build](https://web.archive.org/web/20240501103645/https://www.electron.build/icons.html).
|
||||||
|
Before Width: | Height: | Size: 353 KiB After Width: | Height: | Size: 364 KiB |
BIN
src/presentation/electron/build/icon.png
Normal file
|
After Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 353 KiB After Width: | Height: | Size: 364 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 24 KiB |