✨ Add prettier as devDependency to improve development workflow
This commit is contained in:
4
bin/builders/BaseBuilder.ts
vendored
4
bin/builders/BaseBuilder.ts
vendored
@@ -129,7 +129,7 @@ export default abstract class BaseBuilder {
|
|||||||
'.pake',
|
'.pake',
|
||||||
'tauri.conf.json',
|
'tauri.conf.json',
|
||||||
);
|
);
|
||||||
let fullCommand = `${baseCommand} -- -c "${configPath}" --features cli-build`;
|
let fullCommand = `${baseCommand} -- -c "${configPath}"`;
|
||||||
|
|
||||||
// For macOS, use app bundles by default unless DMG is explicitly requested
|
// For macOS, use app bundles by default unless DMG is explicitly requested
|
||||||
if (IS_MAC && this.options.targets === 'app') {
|
if (IS_MAC && this.options.targets === 'app') {
|
||||||
@@ -140,7 +140,7 @@ export default abstract class BaseBuilder {
|
|||||||
if (IS_MAC) {
|
if (IS_MAC) {
|
||||||
const macOSVersion = this.getMacOSMajorVersion();
|
const macOSVersion = this.getMacOSMajorVersion();
|
||||||
if (macOSVersion >= 23) {
|
if (macOSVersion >= 23) {
|
||||||
fullCommand += ',macos-proxy';
|
fullCommand += ' --features macos-proxy';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
142
bin/helpers/merge.ts
vendored
142
bin/helpers/merge.ts
vendored
@@ -4,11 +4,7 @@ import fsExtra from 'fs-extra';
|
|||||||
import combineFiles from '@/utils/combine';
|
import combineFiles from '@/utils/combine';
|
||||||
import logger from '@/options/logger';
|
import logger from '@/options/logger';
|
||||||
import { PakeAppOptions, PlatformMap } from '@/types';
|
import { PakeAppOptions, PlatformMap } from '@/types';
|
||||||
import {
|
import { tauriConfigDirectory, npmDirectory } from '@/utils/dir';
|
||||||
tauriConfigDirectory,
|
|
||||||
npmDirectory,
|
|
||||||
getUserHomeDir,
|
|
||||||
} from '@/utils/dir';
|
|
||||||
|
|
||||||
export async function mergeConfig(
|
export async function mergeConfig(
|
||||||
url: string,
|
url: string,
|
||||||
@@ -186,10 +182,8 @@ export async function mergeConfig(
|
|||||||
logger.warn(`✼ ${iconInfo.message}, but you give ${customIconExt}`);
|
logger.warn(`✼ ${iconInfo.message}, but you give ${customIconExt}`);
|
||||||
tauriConf.bundle.icon = [iconInfo.defaultIcon];
|
tauriConf.bundle.icon = [iconInfo.defaultIcon];
|
||||||
} else {
|
} else {
|
||||||
// Save icon to .pake directory instead of src-tauri
|
const iconPath = path.join(npmDirectory, 'src-tauri/', iconInfo.path);
|
||||||
const iconPath = path.join(tauriConfigDirectory, iconInfo.path);
|
tauriConf.bundle.resources = [iconInfo.path];
|
||||||
await fsExtra.ensureDir(path.dirname(iconPath));
|
|
||||||
tauriConf.bundle.resources = [`.pake/${iconInfo.path}`];
|
|
||||||
await fsExtra.copy(options.icon, iconPath);
|
await fsExtra.copy(options.icon, iconPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -205,20 +199,30 @@ export async function mergeConfig(
|
|||||||
tauriConf.bundle.icon = [iconInfo.defaultIcon];
|
tauriConf.bundle.icon = [iconInfo.defaultIcon];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set system tray icon path
|
// Set tray icon path.
|
||||||
let trayIconPath = 'icons/icon.png'; // default fallback
|
let trayIconPath =
|
||||||
|
platform === 'darwin' ? 'png/icon_512.png' : tauriConf.bundle.icon[0];
|
||||||
if (showSystemTray) {
|
if (systemTrayIcon.length > 0) {
|
||||||
if (systemTrayIcon.length > 0) {
|
try {
|
||||||
// User provided custom system tray icon
|
await fsExtra.pathExists(systemTrayIcon);
|
||||||
trayIconPath = await handleCustomTrayIcon(
|
// 需要判断图标格式,默认只支持ico和png两种
|
||||||
systemTrayIcon,
|
let iconExt = path.extname(systemTrayIcon).toLowerCase();
|
||||||
name,
|
if (iconExt == '.png' || iconExt == '.ico') {
|
||||||
tauriConfigDirectory,
|
const trayIcoPath = path.join(
|
||||||
);
|
npmDirectory,
|
||||||
} else {
|
`src-tauri/png/${name.toLowerCase()}${iconExt}`,
|
||||||
// Use original downloaded PNG icon for system tray
|
);
|
||||||
trayIconPath = await handleDownloadedTrayIcon(name, tauriConfigDirectory);
|
trayIconPath = `png/${name.toLowerCase()}${iconExt}`;
|
||||||
|
await fsExtra.copy(systemTrayIcon, trayIcoPath);
|
||||||
|
} else {
|
||||||
|
logger.warn(
|
||||||
|
`✼ System tray icon must be .ico or .png, but you provided ${iconExt}.`,
|
||||||
|
);
|
||||||
|
logger.warn(`✼ Default system tray icon will be used.`);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
logger.warn(`✼ ${systemTrayIcon} not exists!`);
|
||||||
|
logger.warn(`✼ Default system tray icon will remain unchanged.`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -264,6 +268,7 @@ export async function mergeConfig(
|
|||||||
);
|
);
|
||||||
|
|
||||||
const bundleConf = { bundle: tauriConf.bundle };
|
const bundleConf = { bundle: tauriConf.bundle };
|
||||||
|
console.log('pakeConfig', tauriConf.pake);
|
||||||
await fsExtra.outputJSON(configPath, bundleConf, { spaces: 4 });
|
await fsExtra.outputJSON(configPath, bundleConf, { spaces: 4 });
|
||||||
const pakeConfigPath = path.join(tauriConfigDirectory, 'pake.json');
|
const pakeConfigPath = path.join(tauriConfigDirectory, 'pake.json');
|
||||||
await fsExtra.outputJSON(pakeConfigPath, tauriConf.pake, { spaces: 4 });
|
await fsExtra.outputJSON(pakeConfigPath, tauriConf.pake, { spaces: 4 });
|
||||||
@@ -278,94 +283,3 @@ export async function mergeConfig(
|
|||||||
const configJsonPath = path.join(tauriConfigDirectory, 'tauri.conf.json');
|
const configJsonPath = path.join(tauriConfigDirectory, 'tauri.conf.json');
|
||||||
await fsExtra.outputJSON(configJsonPath, tauriConf2, { spaces: 4 });
|
await fsExtra.outputJSON(configJsonPath, tauriConf2, { spaces: 4 });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle custom system tray icon provided by user
|
|
||||||
*/
|
|
||||||
async function handleCustomTrayIcon(
|
|
||||||
systemTrayIcon: string,
|
|
||||||
appName: string,
|
|
||||||
configDir: string,
|
|
||||||
): Promise<string> {
|
|
||||||
const defaultPath = 'icons/icon.png';
|
|
||||||
|
|
||||||
if (!(await fsExtra.pathExists(systemTrayIcon))) {
|
|
||||||
logger.warn(`✼ Custom tray icon ${systemTrayIcon} not found!`);
|
|
||||||
logger.warn(`✼ Using default icon for system tray.`);
|
|
||||||
return defaultPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
const iconExt = path.extname(systemTrayIcon).toLowerCase();
|
|
||||||
if (iconExt !== '.png' && iconExt !== '.ico') {
|
|
||||||
logger.warn(
|
|
||||||
`✼ System tray icon must be .png or .ico, but you provided ${iconExt}.`,
|
|
||||||
);
|
|
||||||
logger.warn(`✼ Using default icon for system tray.`);
|
|
||||||
return defaultPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const trayIconPath = path.join(
|
|
||||||
configDir,
|
|
||||||
`png/${appName.toLowerCase()}${iconExt}`,
|
|
||||||
);
|
|
||||||
await fsExtra.ensureDir(path.dirname(trayIconPath));
|
|
||||||
await fsExtra.copy(systemTrayIcon, trayIconPath);
|
|
||||||
|
|
||||||
const relativePath = `.pake/png/${appName.toLowerCase()}${iconExt}`;
|
|
||||||
logger.info(`✓ Using custom system tray icon: ${systemTrayIcon}`);
|
|
||||||
return relativePath;
|
|
||||||
} catch (error) {
|
|
||||||
logger.warn(`✼ Failed to copy custom tray icon: ${error}`);
|
|
||||||
logger.warn(`✼ Using default icon for system tray.`);
|
|
||||||
return defaultPath;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle system tray icon from downloaded app icon
|
|
||||||
*/
|
|
||||||
async function handleDownloadedTrayIcon(
|
|
||||||
appName: string,
|
|
||||||
configDir: string,
|
|
||||||
): Promise<string> {
|
|
||||||
const defaultPath = 'icons/icon.png';
|
|
||||||
const homeDir = getUserHomeDir();
|
|
||||||
const downloadedIconPath = path.join(
|
|
||||||
homeDir,
|
|
||||||
'.pake',
|
|
||||||
'icons',
|
|
||||||
'downloaded-icon.png',
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!(await fsExtra.pathExists(downloadedIconPath))) {
|
|
||||||
logger.warn(
|
|
||||||
`✼ No downloaded icon found, using default icon for system tray.`,
|
|
||||||
);
|
|
||||||
return defaultPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const trayPngPath = path.join(
|
|
||||||
configDir,
|
|
||||||
`png/${appName.toLowerCase()}_tray.png`,
|
|
||||||
);
|
|
||||||
await fsExtra.ensureDir(path.dirname(trayPngPath));
|
|
||||||
|
|
||||||
// Resize the original PNG to appropriate tray size (32x32 for optimal display)
|
|
||||||
const sharp = await import('sharp');
|
|
||||||
await sharp
|
|
||||||
.default(downloadedIconPath)
|
|
||||||
.resize(32, 32)
|
|
||||||
.png()
|
|
||||||
.toFile(trayPngPath);
|
|
||||||
|
|
||||||
const relativePath = `.pake/png/${appName.toLowerCase()}_tray.png`;
|
|
||||||
logger.info(`✓ Using downloaded app icon for system tray: ${relativePath}`);
|
|
||||||
return relativePath;
|
|
||||||
} catch (error) {
|
|
||||||
logger.warn(`✼ Failed to process downloaded icon for tray: ${error}`);
|
|
||||||
logger.warn(`✼ Using default icon for system tray.`);
|
|
||||||
return defaultPath;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
262
bin/options/icon.ts
vendored
262
bin/options/icon.ts
vendored
@@ -1,243 +1,73 @@
|
|||||||
import path from 'path';
|
import path from 'path';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import fsExtra from 'fs-extra';
|
import fsExtra from 'fs-extra';
|
||||||
|
import chalk from 'chalk';
|
||||||
import { dir } from 'tmp-promise';
|
import { dir } from 'tmp-promise';
|
||||||
import { fileTypeFromBuffer } from 'file-type';
|
|
||||||
import icongen from 'icon-gen';
|
|
||||||
import sharp from 'sharp';
|
|
||||||
|
|
||||||
import logger from './logger';
|
import logger from './logger';
|
||||||
import { npmDirectory, getUserHomeDir } from '@/utils/dir';
|
import { npmDirectory } from '@/utils/dir';
|
||||||
import { IS_LINUX, IS_WIN } from '@/utils/platform';
|
import { IS_LINUX, IS_WIN } from '@/utils/platform';
|
||||||
|
import { getSpinner } from '@/utils/info';
|
||||||
|
import { fileTypeFromBuffer } from 'file-type';
|
||||||
import { PakeAppOptions } from '@/types';
|
import { PakeAppOptions } from '@/types';
|
||||||
|
|
||||||
// Constants
|
export async function handleIcon(options: PakeAppOptions) {
|
||||||
const ICON_CONFIG = {
|
|
||||||
minFileSize: 100,
|
|
||||||
downloadTimeout: 10000,
|
|
||||||
supportedFormats: ['png', 'ico', 'jpeg', 'jpg', 'webp'] as const,
|
|
||||||
whiteBackground: { r: 255, g: 255, b: 255 },
|
|
||||||
};
|
|
||||||
|
|
||||||
// API Configuration
|
|
||||||
const API_TOKENS = {
|
|
||||||
// cspell:disable-next-line
|
|
||||||
logoDev: ['pk_JLLMUKGZRpaG5YclhXaTkg', 'pk_Ph745P8mQSeYFfW2Wk039A'],
|
|
||||||
// cspell:disable-next-line
|
|
||||||
brandfetch: ['1idqvJC0CeFSeyp3Yf7', '1idej-yhU_ThggIHFyG'],
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds white background to transparent icons only
|
|
||||||
*/
|
|
||||||
async function preprocessIcon(inputPath: string): Promise<string> {
|
|
||||||
try {
|
|
||||||
const metadata = await sharp(inputPath).metadata();
|
|
||||||
if (metadata.channels !== 4) return inputPath; // No transparency
|
|
||||||
|
|
||||||
const { path: tempDir } = await dir();
|
|
||||||
const outputPath = path.join(tempDir, 'icon-with-background.png');
|
|
||||||
|
|
||||||
await sharp({
|
|
||||||
create: {
|
|
||||||
width: metadata.width || 512,
|
|
||||||
height: metadata.height || 512,
|
|
||||||
channels: 3,
|
|
||||||
background: ICON_CONFIG.whiteBackground,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.composite([{ input: inputPath }])
|
|
||||||
.png()
|
|
||||||
.toFile(outputPath);
|
|
||||||
|
|
||||||
return outputPath;
|
|
||||||
} catch {
|
|
||||||
return inputPath;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts icon to platform-specific format
|
|
||||||
*/
|
|
||||||
async function convertIconFormat(
|
|
||||||
inputPath: string,
|
|
||||||
appName: string,
|
|
||||||
): Promise<string | null> {
|
|
||||||
try {
|
|
||||||
if (!(await fsExtra.pathExists(inputPath))) return null;
|
|
||||||
|
|
||||||
const { path: outputDir } = await dir();
|
|
||||||
const platformOutputDir = path.join(outputDir, 'converted-icons');
|
|
||||||
await fsExtra.ensureDir(platformOutputDir);
|
|
||||||
|
|
||||||
const processedInputPath = await preprocessIcon(inputPath);
|
|
||||||
const iconName = appName.toLowerCase();
|
|
||||||
|
|
||||||
// Generate platform-specific format
|
|
||||||
if (IS_WIN) {
|
|
||||||
await icongen(processedInputPath, platformOutputDir, {
|
|
||||||
report: false,
|
|
||||||
ico: { name: `${iconName}_256`, sizes: [256] },
|
|
||||||
});
|
|
||||||
return path.join(platformOutputDir, `${iconName}_256.ico`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (IS_LINUX) {
|
|
||||||
const outputPath = path.join(platformOutputDir, `${iconName}_512.png`);
|
|
||||||
await fsExtra.copy(processedInputPath, outputPath);
|
|
||||||
return outputPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
// macOS
|
|
||||||
await icongen(processedInputPath, platformOutputDir, {
|
|
||||||
report: false,
|
|
||||||
icns: { name: iconName, sizes: [16, 32, 64, 128, 256, 512, 1024] },
|
|
||||||
});
|
|
||||||
const outputPath = path.join(platformOutputDir, `${iconName}.icns`);
|
|
||||||
return (await fsExtra.pathExists(outputPath)) ? outputPath : null;
|
|
||||||
} catch (error) {
|
|
||||||
logger.warn(`Icon format conversion failed: ${error.message}`);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function handleIcon(options: PakeAppOptions, url?: string) {
|
|
||||||
if (options.icon) {
|
if (options.icon) {
|
||||||
if (options.icon.startsWith('http')) {
|
if (options.icon.startsWith('http')) {
|
||||||
return downloadIcon(options.icon);
|
return downloadIcon(options.icon);
|
||||||
|
} else {
|
||||||
|
return path.resolve(options.icon);
|
||||||
}
|
}
|
||||||
return path.resolve(options.icon);
|
} else {
|
||||||
}
|
logger.warn(
|
||||||
|
'✼ No icon given, default in use. For a custom icon, use --icon option.',
|
||||||
// Try to get favicon from website if URL is provided
|
|
||||||
if (url && url.startsWith('http') && options.name) {
|
|
||||||
const faviconPath = await tryGetFavicon(url, options.name);
|
|
||||||
if (faviconPath) return faviconPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info('✼ No icon provided, using default icon.');
|
|
||||||
const iconPath = IS_WIN
|
|
||||||
? 'src-tauri/png/icon_256.ico'
|
|
||||||
: IS_LINUX
|
|
||||||
? 'src-tauri/png/icon_512.png'
|
|
||||||
: 'src-tauri/icons/icon.icns';
|
|
||||||
return path.join(npmDirectory, iconPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generates icon service URLs for a domain
|
|
||||||
*/
|
|
||||||
function generateIconServiceUrls(domain: string): string[] {
|
|
||||||
const logoDevUrls = API_TOKENS.logoDev
|
|
||||||
.sort(() => Math.random() - 0.5)
|
|
||||||
.map(
|
|
||||||
(token) =>
|
|
||||||
`https://img.logo.dev/${domain}?token=${token}&format=png&size=256`,
|
|
||||||
);
|
);
|
||||||
|
const iconPath = IS_WIN
|
||||||
const brandfetchUrls = API_TOKENS.brandfetch
|
? 'src-tauri/png/icon_256.ico'
|
||||||
.sort(() => Math.random() - 0.5)
|
: IS_LINUX
|
||||||
.map((key) => `https://cdn.brandfetch.io/${domain}/w/400/h/400?c=${key}`);
|
? 'src-tauri/png/icon_512.png'
|
||||||
|
: 'src-tauri/icons/icon.icns';
|
||||||
return [
|
return path.join(npmDirectory, iconPath);
|
||||||
...logoDevUrls,
|
|
||||||
...brandfetchUrls,
|
|
||||||
`https://logo.clearbit.com/${domain}?size=256`,
|
|
||||||
`https://logo.uplead.com/${domain}`,
|
|
||||||
`https://www.google.com/s2/favicons?domain=${domain}&sz=256`,
|
|
||||||
`https://favicon.is/${domain}`,
|
|
||||||
`https://icons.duckduckgo.com/ip3/${domain}.ico`,
|
|
||||||
`https://icon.horse/icon/${domain}`,
|
|
||||||
`https://${domain}/favicon.ico`,
|
|
||||||
`https://www.${domain}/favicon.ico`,
|
|
||||||
`https://${domain}/apple-touch-icon.png`,
|
|
||||||
`https://${domain}/apple-touch-icon-precomposed.png`,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Attempts to fetch favicon from website
|
|
||||||
*/
|
|
||||||
async function tryGetFavicon(
|
|
||||||
url: string,
|
|
||||||
appName: string,
|
|
||||||
): Promise<string | null> {
|
|
||||||
try {
|
|
||||||
const domain = new URL(url).hostname;
|
|
||||||
logger.info(`Auto-fetching favicon for ${domain}...`);
|
|
||||||
|
|
||||||
const serviceUrls = generateIconServiceUrls(domain);
|
|
||||||
|
|
||||||
for (const serviceUrl of serviceUrls) {
|
|
||||||
try {
|
|
||||||
const faviconPath = await downloadIcon(serviceUrl, false);
|
|
||||||
if (!faviconPath) continue;
|
|
||||||
|
|
||||||
const convertedPath = await convertIconFormat(faviconPath, appName);
|
|
||||||
if (convertedPath) {
|
|
||||||
logger.info(`Favicon ready for ${domain}`);
|
|
||||||
return convertedPath;
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info(`No favicon found for ${domain}. Using default.`);
|
|
||||||
return null;
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
export async function downloadIcon(iconUrl: string) {
|
||||||
* Downloads icon from URL
|
const spinner = getSpinner('Downloading icon...');
|
||||||
*/
|
|
||||||
export async function downloadIcon(
|
|
||||||
iconUrl: string,
|
|
||||||
showSpinner = true,
|
|
||||||
): Promise<string | null> {
|
|
||||||
try {
|
try {
|
||||||
const response = await axios.get(iconUrl, {
|
const iconResponse = await axios.get(iconUrl, {
|
||||||
responseType: 'arraybuffer',
|
responseType: 'arraybuffer',
|
||||||
timeout: ICON_CONFIG.downloadTimeout,
|
|
||||||
});
|
});
|
||||||
|
const iconData = await iconResponse.data;
|
||||||
|
|
||||||
const iconData = response.data;
|
if (!iconData) {
|
||||||
if (!iconData || iconData.byteLength < ICON_CONFIG.minFileSize) return null;
|
|
||||||
|
|
||||||
const fileDetails = await fileTypeFromBuffer(iconData);
|
|
||||||
if (
|
|
||||||
!fileDetails ||
|
|
||||||
!ICON_CONFIG.supportedFormats.includes(fileDetails.ext as any)
|
|
||||||
) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return await saveIconFile(iconData, fileDetails.ext);
|
const fileDetails = await fileTypeFromBuffer(iconData);
|
||||||
} catch (error) {
|
if (!fileDetails) {
|
||||||
if (showSpinner && !(error.response?.status === 404)) {
|
return null;
|
||||||
throw error;
|
|
||||||
}
|
}
|
||||||
return null;
|
|
||||||
|
const { path: tempPath } = await dir();
|
||||||
|
let iconPath = `${tempPath}/icon.${fileDetails.ext}`;
|
||||||
|
// Fix this for linux
|
||||||
|
if (IS_LINUX) {
|
||||||
|
iconPath = 'png/linux_temp.png';
|
||||||
|
await fsExtra.outputFile(
|
||||||
|
`${npmDirectory}/src-tauri/${iconPath}`,
|
||||||
|
iconData,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
await fsExtra.outputFile(iconPath, iconData);
|
||||||
|
}
|
||||||
|
await fsExtra.outputFile(iconPath, iconData);
|
||||||
|
spinner.succeed(chalk.green('Icon downloaded successfully!'));
|
||||||
|
return iconPath;
|
||||||
|
} catch (error) {
|
||||||
|
spinner.fail(chalk.red('Icon download failed!'));
|
||||||
|
if (error.response && error.response.status === 404) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Saves icon file to .pake directory
|
|
||||||
*/
|
|
||||||
async function saveIconFile(
|
|
||||||
iconData: ArrayBuffer,
|
|
||||||
extension: string,
|
|
||||||
): Promise<string> {
|
|
||||||
const buffer = Buffer.from(iconData);
|
|
||||||
const homeDir = getUserHomeDir();
|
|
||||||
const pakeDir = path.join(homeDir, '.pake', 'icons');
|
|
||||||
|
|
||||||
// Ensure .pake/icons directory exists
|
|
||||||
await fsExtra.ensureDir(pakeDir);
|
|
||||||
|
|
||||||
const iconPath = path.join(pakeDir, `downloaded-icon.${extension}`);
|
|
||||||
await fsExtra.outputFile(iconPath, buffer);
|
|
||||||
return iconPath;
|
|
||||||
}
|
|
||||||
|
|||||||
10
bin/options/index.ts
vendored
10
bin/options/index.ts
vendored
@@ -43,13 +43,11 @@ export default async function handleOptions(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!isValidName(name, platform)) {
|
if (!isValidName(name, platform)) {
|
||||||
|
const LINUX_NAME_ERROR = `✕ Name should only include lowercase letters, numbers, and dashes (not leading dashes). Examples: com-123-xxx, 123pan, pan123, weread, we-read, 123.`;
|
||||||
|
const DEFAULT_NAME_ERROR = `✕ Name should only include letters, numbers, dashes, and spaces (not leading dashes and spaces). Examples: 123pan, 123Pan, Pan123, weread, WeRead, WERead, we-read, We Read, 123.`;
|
||||||
const errorMsg =
|
const errorMsg =
|
||||||
platform === 'linux'
|
platform === 'linux' ? LINUX_NAME_ERROR : DEFAULT_NAME_ERROR;
|
||||||
? `✕ Name should only include letters, numbers, dashes, and spaces. Spaces will be converted to dashes. Examples: Google Translate → google-translate, 123pan, weread.`
|
|
||||||
: `✕ Name should only include letters, numbers, dashes, and spaces (not leading dashes and spaces). Examples: Google Translate, 123pan, WeRead, we-read.`;
|
|
||||||
|
|
||||||
logger.error(errorMsg);
|
logger.error(errorMsg);
|
||||||
|
|
||||||
if (isActions) {
|
if (isActions) {
|
||||||
name = resolveAppName(url, platform);
|
name = resolveAppName(url, platform);
|
||||||
logger.warn(`✼ Inside github actions, use the default name: ${name}`);
|
logger.warn(`✼ Inside github actions, use the default name: ${name}`);
|
||||||
@@ -64,7 +62,7 @@ export default async function handleOptions(
|
|||||||
identifier: getIdentifier(url),
|
identifier: getIdentifier(url),
|
||||||
};
|
};
|
||||||
|
|
||||||
appOptions.icon = await handleIcon(appOptions, url);
|
appOptions.icon = await handleIcon(appOptions);
|
||||||
|
|
||||||
return appOptions;
|
return appOptions;
|
||||||
}
|
}
|
||||||
|
|||||||
5
bin/utils/dir.ts
vendored
5
bin/utils/dir.ts
vendored
@@ -1,5 +1,4 @@
|
|||||||
import path from 'path';
|
import path from 'path';
|
||||||
import os from 'os';
|
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
|
|
||||||
// Convert the current module URL to a file path
|
// Convert the current module URL to a file path
|
||||||
@@ -13,7 +12,3 @@ export const tauriConfigDirectory = path.join(
|
|||||||
'src-tauri',
|
'src-tauri',
|
||||||
'.pake',
|
'.pake',
|
||||||
);
|
);
|
||||||
|
|
||||||
export function getUserHomeDir(): string {
|
|
||||||
return os.homedir();
|
|
||||||
}
|
|
||||||
|
|||||||
339
dist/cli.js
vendored
339
dist/cli.js
vendored
@@ -7,7 +7,6 @@ import prompts from 'prompts';
|
|||||||
import { execa, execaSync } from 'execa';
|
import { execa, execaSync } from 'execa';
|
||||||
import crypto from 'crypto';
|
import crypto from 'crypto';
|
||||||
import ora from 'ora';
|
import ora from 'ora';
|
||||||
import os from 'os';
|
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
import dns from 'dns';
|
import dns from 'dns';
|
||||||
import http from 'http';
|
import http from 'http';
|
||||||
@@ -17,12 +16,10 @@ import updateNotifier from 'update-notifier';
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { dir } from 'tmp-promise';
|
import { dir } from 'tmp-promise';
|
||||||
import { fileTypeFromBuffer } from 'file-type';
|
import { fileTypeFromBuffer } from 'file-type';
|
||||||
import icongen from 'icon-gen';
|
|
||||||
import sharp from 'sharp';
|
|
||||||
import * as psl from 'psl';
|
import * as psl from 'psl';
|
||||||
|
|
||||||
var name = "pake-cli";
|
var name = "pake-cli";
|
||||||
var version$1 = "3.2.0-beta2";
|
var version$1 = "3.2.0-beta1";
|
||||||
var description = "🤱🏻 Turn any webpage into a desktop app with Rust. 🤱🏻 利用 Rust 轻松构建轻量级多端桌面应用。";
|
var description = "🤱🏻 Turn any webpage into a desktop app with Rust. 🤱🏻 利用 Rust 轻松构建轻量级多端桌面应用。";
|
||||||
var engines = {
|
var engines = {
|
||||||
node: ">=16.0.0"
|
node: ">=16.0.0"
|
||||||
@@ -80,7 +77,6 @@ var dependencies = {
|
|||||||
execa: "^9.6.0",
|
execa: "^9.6.0",
|
||||||
"file-type": "^18.7.0",
|
"file-type": "^18.7.0",
|
||||||
"fs-extra": "^11.3.1",
|
"fs-extra": "^11.3.1",
|
||||||
"icon-gen": "^5.0.0",
|
|
||||||
loglevel: "^1.9.2",
|
loglevel: "^1.9.2",
|
||||||
ora: "^8.2.0",
|
ora: "^8.2.0",
|
||||||
prompts: "^2.4.2",
|
prompts: "^2.4.2",
|
||||||
@@ -320,9 +316,6 @@ const currentModulePath = fileURLToPath(import.meta.url);
|
|||||||
// Resolve the parent directory of the current module
|
// Resolve the parent directory of the current module
|
||||||
const npmDirectory = path.join(path.dirname(currentModulePath), '..');
|
const npmDirectory = path.join(path.dirname(currentModulePath), '..');
|
||||||
const tauriConfigDirectory = path.join(npmDirectory, 'src-tauri', '.pake');
|
const tauriConfigDirectory = path.join(npmDirectory, 'src-tauri', '.pake');
|
||||||
function getUserHomeDir() {
|
|
||||||
return os.homedir();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function shellExec(command, timeout = 300000) {
|
async function shellExec(command, timeout = 300000) {
|
||||||
try {
|
try {
|
||||||
@@ -456,18 +449,11 @@ async function mergeConfig(url, options, tauriConf) {
|
|||||||
const srcTauriDir = path.join(npmDirectory, 'src-tauri');
|
const srcTauriDir = path.join(npmDirectory, 'src-tauri');
|
||||||
await fsExtra.ensureDir(tauriConfigDirectory);
|
await fsExtra.ensureDir(tauriConfigDirectory);
|
||||||
// Copy source config files to .pake directory (as templates)
|
// Copy source config files to .pake directory (as templates)
|
||||||
const sourceFiles = [
|
const sourceFiles = ['tauri.conf.json', 'tauri.macos.conf.json', 'tauri.windows.conf.json', 'tauri.linux.conf.json', 'pake.json'];
|
||||||
'tauri.conf.json',
|
|
||||||
'tauri.macos.conf.json',
|
|
||||||
'tauri.windows.conf.json',
|
|
||||||
'tauri.linux.conf.json',
|
|
||||||
'pake.json',
|
|
||||||
];
|
|
||||||
await Promise.all(sourceFiles.map(async (file) => {
|
await Promise.all(sourceFiles.map(async (file) => {
|
||||||
const sourcePath = path.join(srcTauriDir, file);
|
const sourcePath = path.join(srcTauriDir, file);
|
||||||
const destPath = path.join(tauriConfigDirectory, file);
|
const destPath = path.join(tauriConfigDirectory, file);
|
||||||
if ((await fsExtra.pathExists(sourcePath)) &&
|
if (await fsExtra.pathExists(sourcePath) && !(await fsExtra.pathExists(destPath))) {
|
||||||
!(await fsExtra.pathExists(destPath))) {
|
|
||||||
await fsExtra.copy(sourcePath, destPath);
|
await fsExtra.copy(sourcePath, destPath);
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
@@ -575,10 +561,8 @@ async function mergeConfig(url, options, tauriConf) {
|
|||||||
tauriConf.bundle.icon = [iconInfo.defaultIcon];
|
tauriConf.bundle.icon = [iconInfo.defaultIcon];
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// Save icon to .pake directory instead of src-tauri
|
const iconPath = path.join(npmDirectory, 'src-tauri/', iconInfo.path);
|
||||||
const iconPath = path.join(tauriConfigDirectory, iconInfo.path);
|
tauriConf.bundle.resources = [iconInfo.path];
|
||||||
await fsExtra.ensureDir(path.dirname(iconPath));
|
|
||||||
tauriConf.bundle.resources = [`.pake/${iconInfo.path}`];
|
|
||||||
await fsExtra.copy(options.icon, iconPath);
|
await fsExtra.copy(options.icon, iconPath);
|
||||||
}
|
}
|
||||||
if (updateIconPath) {
|
if (updateIconPath) {
|
||||||
@@ -592,16 +576,26 @@ async function mergeConfig(url, options, tauriConf) {
|
|||||||
logger.warn('✼ Custom icon path may be invalid, default icon will be used instead.');
|
logger.warn('✼ Custom icon path may be invalid, default icon will be used instead.');
|
||||||
tauriConf.bundle.icon = [iconInfo.defaultIcon];
|
tauriConf.bundle.icon = [iconInfo.defaultIcon];
|
||||||
}
|
}
|
||||||
// Set system tray icon path
|
// Set tray icon path.
|
||||||
let trayIconPath = 'icons/icon.png'; // default fallback
|
let trayIconPath = platform === 'darwin' ? 'png/icon_512.png' : tauriConf.bundle.icon[0];
|
||||||
if (showSystemTray) {
|
if (systemTrayIcon.length > 0) {
|
||||||
if (systemTrayIcon.length > 0) {
|
try {
|
||||||
// User provided custom system tray icon
|
await fsExtra.pathExists(systemTrayIcon);
|
||||||
trayIconPath = await handleCustomTrayIcon(systemTrayIcon, name, tauriConfigDirectory);
|
// 需要判断图标格式,默认只支持ico和png两种
|
||||||
|
let iconExt = path.extname(systemTrayIcon).toLowerCase();
|
||||||
|
if (iconExt == '.png' || iconExt == '.ico') {
|
||||||
|
const trayIcoPath = path.join(npmDirectory, `src-tauri/png/${name.toLowerCase()}${iconExt}`);
|
||||||
|
trayIconPath = `png/${name.toLowerCase()}${iconExt}`;
|
||||||
|
await fsExtra.copy(systemTrayIcon, trayIcoPath);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
logger.warn(`✼ System tray icon must be .ico or .png, but you provided ${iconExt}.`);
|
||||||
|
logger.warn(`✼ Default system tray icon will be used.`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else {
|
catch {
|
||||||
// Use original downloaded PNG icon for system tray
|
logger.warn(`✼ ${systemTrayIcon} not exists!`);
|
||||||
trayIconPath = await handleDownloadedTrayIcon(name, tauriConfigDirectory);
|
logger.warn(`✼ Default system tray icon will remain unchanged.`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tauriConf.app.trayIcon.iconPath = trayIconPath;
|
tauriConf.app.trayIcon.iconPath = trayIconPath;
|
||||||
@@ -631,6 +625,7 @@ async function mergeConfig(url, options, tauriConf) {
|
|||||||
};
|
};
|
||||||
const configPath = path.join(tauriConfigDirectory, platformConfigPaths[platform]);
|
const configPath = path.join(tauriConfigDirectory, platformConfigPaths[platform]);
|
||||||
const bundleConf = { bundle: tauriConf.bundle };
|
const bundleConf = { bundle: tauriConf.bundle };
|
||||||
|
console.log('pakeConfig', tauriConf.pake);
|
||||||
await fsExtra.outputJSON(configPath, bundleConf, { spaces: 4 });
|
await fsExtra.outputJSON(configPath, bundleConf, { spaces: 4 });
|
||||||
const pakeConfigPath = path.join(tauriConfigDirectory, 'pake.json');
|
const pakeConfigPath = path.join(tauriConfigDirectory, 'pake.json');
|
||||||
await fsExtra.outputJSON(pakeConfigPath, tauriConf.pake, { spaces: 4 });
|
await fsExtra.outputJSON(pakeConfigPath, tauriConf.pake, { spaces: 4 });
|
||||||
@@ -639,66 +634,6 @@ async function mergeConfig(url, options, tauriConf) {
|
|||||||
const configJsonPath = path.join(tauriConfigDirectory, 'tauri.conf.json');
|
const configJsonPath = path.join(tauriConfigDirectory, 'tauri.conf.json');
|
||||||
await fsExtra.outputJSON(configJsonPath, tauriConf2, { spaces: 4 });
|
await fsExtra.outputJSON(configJsonPath, tauriConf2, { spaces: 4 });
|
||||||
}
|
}
|
||||||
/**
|
|
||||||
* Handle custom system tray icon provided by user
|
|
||||||
*/
|
|
||||||
async function handleCustomTrayIcon(systemTrayIcon, appName, configDir) {
|
|
||||||
const defaultPath = 'icons/icon.png';
|
|
||||||
if (!(await fsExtra.pathExists(systemTrayIcon))) {
|
|
||||||
logger.warn(`✼ Custom tray icon ${systemTrayIcon} not found!`);
|
|
||||||
logger.warn(`✼ Using default icon for system tray.`);
|
|
||||||
return defaultPath;
|
|
||||||
}
|
|
||||||
const iconExt = path.extname(systemTrayIcon).toLowerCase();
|
|
||||||
if (iconExt !== '.png' && iconExt !== '.ico') {
|
|
||||||
logger.warn(`✼ System tray icon must be .png or .ico, but you provided ${iconExt}.`);
|
|
||||||
logger.warn(`✼ Using default icon for system tray.`);
|
|
||||||
return defaultPath;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const trayIconPath = path.join(configDir, `png/${appName.toLowerCase()}${iconExt}`);
|
|
||||||
await fsExtra.ensureDir(path.dirname(trayIconPath));
|
|
||||||
await fsExtra.copy(systemTrayIcon, trayIconPath);
|
|
||||||
const relativePath = `.pake/png/${appName.toLowerCase()}${iconExt}`;
|
|
||||||
logger.info(`✓ Using custom system tray icon: ${systemTrayIcon}`);
|
|
||||||
return relativePath;
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
logger.warn(`✼ Failed to copy custom tray icon: ${error}`);
|
|
||||||
logger.warn(`✼ Using default icon for system tray.`);
|
|
||||||
return defaultPath;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Handle system tray icon from downloaded app icon
|
|
||||||
*/
|
|
||||||
async function handleDownloadedTrayIcon(appName, configDir) {
|
|
||||||
const defaultPath = 'icons/icon.png';
|
|
||||||
const homeDir = getUserHomeDir();
|
|
||||||
const downloadedIconPath = path.join(homeDir, '.pake', 'icons', 'downloaded-icon.png');
|
|
||||||
if (!(await fsExtra.pathExists(downloadedIconPath))) {
|
|
||||||
logger.warn(`✼ No downloaded icon found, using default icon for system tray.`);
|
|
||||||
return defaultPath;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const trayPngPath = path.join(configDir, `png/${appName.toLowerCase()}_tray.png`);
|
|
||||||
await fsExtra.ensureDir(path.dirname(trayPngPath));
|
|
||||||
// Resize the original PNG to appropriate tray size (32x32 for optimal display)
|
|
||||||
const sharp = await import('sharp');
|
|
||||||
await sharp.default(downloadedIconPath)
|
|
||||||
.resize(32, 32)
|
|
||||||
.png()
|
|
||||||
.toFile(trayPngPath);
|
|
||||||
const relativePath = `.pake/png/${appName.toLowerCase()}_tray.png`;
|
|
||||||
logger.info(`✓ Using downloaded app icon for system tray: ${relativePath}`);
|
|
||||||
return relativePath;
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
logger.warn(`✼ Failed to process downloaded icon for tray: ${error}`);
|
|
||||||
logger.warn(`✼ Using default icon for system tray.`);
|
|
||||||
return defaultPath;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class BaseBuilder {
|
class BaseBuilder {
|
||||||
constructor(options) {
|
constructor(options) {
|
||||||
@@ -784,7 +719,7 @@ class BaseBuilder {
|
|||||||
: 'npm run build';
|
: 'npm run build';
|
||||||
// Use temporary config directory to avoid modifying source files
|
// Use temporary config directory to avoid modifying source files
|
||||||
const configPath = path.join(npmDirectory, 'src-tauri', '.pake', 'tauri.conf.json');
|
const configPath = path.join(npmDirectory, 'src-tauri', '.pake', 'tauri.conf.json');
|
||||||
let fullCommand = `${baseCommand} -- -c "${configPath}" --features cli-build`;
|
let fullCommand = `${baseCommand} -- -c "${configPath}"`;
|
||||||
// For macOS, use app bundles by default unless DMG is explicitly requested
|
// For macOS, use app bundles by default unless DMG is explicitly requested
|
||||||
if (IS_MAC && this.options.targets === 'app') {
|
if (IS_MAC && this.options.targets === 'app') {
|
||||||
fullCommand += ' --bundles app';
|
fullCommand += ' --bundles app';
|
||||||
@@ -793,7 +728,7 @@ class BaseBuilder {
|
|||||||
if (IS_MAC) {
|
if (IS_MAC) {
|
||||||
const macOSVersion = this.getMacOSMajorVersion();
|
const macOSVersion = this.getMacOSMajorVersion();
|
||||||
if (macOSVersion >= 23) {
|
if (macOSVersion >= 23) {
|
||||||
fullCommand += ',macos-proxy';
|
fullCommand += ' --features macos-proxy';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return fullCommand;
|
return fullCommand;
|
||||||
@@ -954,199 +889,61 @@ async function checkUpdateTips() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Constants
|
async function handleIcon(options) {
|
||||||
const ICON_CONFIG = {
|
|
||||||
minFileSize: 100,
|
|
||||||
downloadTimeout: 10000,
|
|
||||||
supportedFormats: ['png', 'ico', 'jpeg', 'jpg', 'webp'],
|
|
||||||
whiteBackground: { r: 255, g: 255, b: 255 },
|
|
||||||
};
|
|
||||||
// API Configuration
|
|
||||||
const API_TOKENS = {
|
|
||||||
// cspell:disable-next-line
|
|
||||||
logoDev: ['pk_JLLMUKGZRpaG5YclhXaTkg', 'pk_Ph745P8mQSeYFfW2Wk039A'],
|
|
||||||
// cspell:disable-next-line
|
|
||||||
brandfetch: ['1idqvJC0CeFSeyp3Yf7', '1idej-yhU_ThggIHFyG'],
|
|
||||||
};
|
|
||||||
/**
|
|
||||||
* Adds white background to transparent icons only
|
|
||||||
*/
|
|
||||||
async function preprocessIcon(inputPath) {
|
|
||||||
try {
|
|
||||||
const metadata = await sharp(inputPath).metadata();
|
|
||||||
if (metadata.channels !== 4)
|
|
||||||
return inputPath; // No transparency
|
|
||||||
const { path: tempDir } = await dir();
|
|
||||||
const outputPath = path.join(tempDir, 'icon-with-background.png');
|
|
||||||
await sharp({
|
|
||||||
create: {
|
|
||||||
width: metadata.width || 512,
|
|
||||||
height: metadata.height || 512,
|
|
||||||
channels: 3,
|
|
||||||
background: ICON_CONFIG.whiteBackground,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.composite([{ input: inputPath }])
|
|
||||||
.png()
|
|
||||||
.toFile(outputPath);
|
|
||||||
return outputPath;
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
return inputPath;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Converts icon to platform-specific format
|
|
||||||
*/
|
|
||||||
async function convertIconFormat(inputPath, appName) {
|
|
||||||
try {
|
|
||||||
if (!(await fsExtra.pathExists(inputPath)))
|
|
||||||
return null;
|
|
||||||
const { path: outputDir } = await dir();
|
|
||||||
const platformOutputDir = path.join(outputDir, 'converted-icons');
|
|
||||||
await fsExtra.ensureDir(platformOutputDir);
|
|
||||||
const processedInputPath = await preprocessIcon(inputPath);
|
|
||||||
const iconName = appName.toLowerCase();
|
|
||||||
// Generate platform-specific format
|
|
||||||
if (IS_WIN) {
|
|
||||||
await icongen(processedInputPath, platformOutputDir, {
|
|
||||||
report: false,
|
|
||||||
ico: { name: `${iconName}_256`, sizes: [256] },
|
|
||||||
});
|
|
||||||
return path.join(platformOutputDir, `${iconName}_256.ico`);
|
|
||||||
}
|
|
||||||
if (IS_LINUX) {
|
|
||||||
const outputPath = path.join(platformOutputDir, `${iconName}_512.png`);
|
|
||||||
await fsExtra.copy(processedInputPath, outputPath);
|
|
||||||
return outputPath;
|
|
||||||
}
|
|
||||||
// macOS
|
|
||||||
await icongen(processedInputPath, platformOutputDir, {
|
|
||||||
report: false,
|
|
||||||
icns: { name: iconName, sizes: [16, 32, 64, 128, 256, 512, 1024] },
|
|
||||||
});
|
|
||||||
const outputPath = path.join(platformOutputDir, `${iconName}.icns`);
|
|
||||||
return (await fsExtra.pathExists(outputPath)) ? outputPath : null;
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
logger.warn(`Icon format conversion failed: ${error.message}`);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
async function handleIcon(options, url) {
|
|
||||||
if (options.icon) {
|
if (options.icon) {
|
||||||
if (options.icon.startsWith('http')) {
|
if (options.icon.startsWith('http')) {
|
||||||
return downloadIcon(options.icon);
|
return downloadIcon(options.icon);
|
||||||
}
|
}
|
||||||
return path.resolve(options.icon);
|
else {
|
||||||
}
|
return path.resolve(options.icon);
|
||||||
// Try to get favicon from website if URL is provided
|
|
||||||
if (url && url.startsWith('http') && options.name) {
|
|
||||||
const faviconPath = await tryGetFavicon(url, options.name);
|
|
||||||
if (faviconPath)
|
|
||||||
return faviconPath;
|
|
||||||
}
|
|
||||||
logger.info('✼ No icon provided, using default icon.');
|
|
||||||
const iconPath = IS_WIN
|
|
||||||
? 'src-tauri/png/icon_256.ico'
|
|
||||||
: IS_LINUX
|
|
||||||
? 'src-tauri/png/icon_512.png'
|
|
||||||
: 'src-tauri/icons/icon.icns';
|
|
||||||
return path.join(npmDirectory, iconPath);
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Generates icon service URLs for a domain
|
|
||||||
*/
|
|
||||||
function generateIconServiceUrls(domain) {
|
|
||||||
const logoDevUrls = API_TOKENS.logoDev
|
|
||||||
.sort(() => Math.random() - 0.5)
|
|
||||||
.map(token => `https://img.logo.dev/${domain}?token=${token}&format=png&size=256`);
|
|
||||||
const brandfetchUrls = API_TOKENS.brandfetch
|
|
||||||
.sort(() => Math.random() - 0.5)
|
|
||||||
.map(key => `https://cdn.brandfetch.io/${domain}/w/400/h/400?c=${key}`);
|
|
||||||
return [
|
|
||||||
...logoDevUrls,
|
|
||||||
...brandfetchUrls,
|
|
||||||
`https://logo.clearbit.com/${domain}?size=256`,
|
|
||||||
`https://logo.uplead.com/${domain}`,
|
|
||||||
`https://www.google.com/s2/favicons?domain=${domain}&sz=256`,
|
|
||||||
`https://favicon.is/${domain}`,
|
|
||||||
`https://icons.duckduckgo.com/ip3/${domain}.ico`,
|
|
||||||
`https://icon.horse/icon/${domain}`,
|
|
||||||
`https://${domain}/favicon.ico`,
|
|
||||||
`https://www.${domain}/favicon.ico`,
|
|
||||||
`https://${domain}/apple-touch-icon.png`,
|
|
||||||
`https://${domain}/apple-touch-icon-precomposed.png`,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Attempts to fetch favicon from website
|
|
||||||
*/
|
|
||||||
async function tryGetFavicon(url, appName) {
|
|
||||||
try {
|
|
||||||
const domain = new URL(url).hostname;
|
|
||||||
logger.info(`Auto-fetching favicon for ${domain}...`);
|
|
||||||
const serviceUrls = generateIconServiceUrls(domain);
|
|
||||||
for (const serviceUrl of serviceUrls) {
|
|
||||||
try {
|
|
||||||
const faviconPath = await downloadIcon(serviceUrl, false);
|
|
||||||
if (!faviconPath)
|
|
||||||
continue;
|
|
||||||
const convertedPath = await convertIconFormat(faviconPath, appName);
|
|
||||||
if (convertedPath) {
|
|
||||||
logger.info(`Favicon ready for ${domain}`);
|
|
||||||
return convertedPath;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
logger.info(`No favicon found for ${domain}. Using default.`);
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
catch {
|
else {
|
||||||
return null;
|
logger.warn('✼ No icon given, default in use. For a custom icon, use --icon option.');
|
||||||
|
const iconPath = IS_WIN
|
||||||
|
? 'src-tauri/png/icon_256.ico'
|
||||||
|
: IS_LINUX
|
||||||
|
? 'src-tauri/png/icon_512.png'
|
||||||
|
: 'src-tauri/icons/icon.icns';
|
||||||
|
return path.join(npmDirectory, iconPath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/**
|
async function downloadIcon(iconUrl) {
|
||||||
* Downloads icon from URL
|
const spinner = getSpinner('Downloading icon...');
|
||||||
*/
|
|
||||||
async function downloadIcon(iconUrl, showSpinner = true) {
|
|
||||||
try {
|
try {
|
||||||
const response = await axios.get(iconUrl, {
|
const iconResponse = await axios.get(iconUrl, {
|
||||||
responseType: 'arraybuffer',
|
responseType: 'arraybuffer',
|
||||||
timeout: ICON_CONFIG.downloadTimeout,
|
|
||||||
});
|
});
|
||||||
const iconData = response.data;
|
const iconData = await iconResponse.data;
|
||||||
if (!iconData || iconData.byteLength < ICON_CONFIG.minFileSize)
|
if (!iconData) {
|
||||||
return null;
|
|
||||||
const fileDetails = await fileTypeFromBuffer(iconData);
|
|
||||||
if (!fileDetails || !ICON_CONFIG.supportedFormats.includes(fileDetails.ext)) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return await saveIconFile(iconData, fileDetails.ext);
|
const fileDetails = await fileTypeFromBuffer(iconData);
|
||||||
|
if (!fileDetails) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const { path: tempPath } = await dir();
|
||||||
|
let iconPath = `${tempPath}/icon.${fileDetails.ext}`;
|
||||||
|
// Fix this for linux
|
||||||
|
if (IS_LINUX) {
|
||||||
|
iconPath = 'png/linux_temp.png';
|
||||||
|
await fsExtra.outputFile(`${npmDirectory}/src-tauri/${iconPath}`, iconData);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
await fsExtra.outputFile(iconPath, iconData);
|
||||||
|
}
|
||||||
|
await fsExtra.outputFile(iconPath, iconData);
|
||||||
|
spinner.succeed(chalk.green('Icon downloaded successfully!'));
|
||||||
|
return iconPath;
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
if (showSpinner && !(error.response?.status === 404)) {
|
spinner.fail(chalk.red('Icon download failed!'));
|
||||||
throw error;
|
if (error.response && error.response.status === 404) {
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
return null;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/**
|
|
||||||
* Saves icon file to .pake directory
|
|
||||||
*/
|
|
||||||
async function saveIconFile(iconData, extension) {
|
|
||||||
const buffer = Buffer.from(iconData);
|
|
||||||
const homeDir = getUserHomeDir();
|
|
||||||
const pakeDir = path.join(homeDir, '.pake', 'icons');
|
|
||||||
// Ensure .pake/icons directory exists
|
|
||||||
await fsExtra.ensureDir(pakeDir);
|
|
||||||
const iconPath = path.join(pakeDir, `downloaded-icon.${extension}`);
|
|
||||||
await fsExtra.outputFile(iconPath, buffer);
|
|
||||||
return iconPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extracts the domain from a given URL.
|
// Extracts the domain from a given URL.
|
||||||
function getDomain(inputUrl) {
|
function getDomain(inputUrl) {
|
||||||
@@ -1217,9 +1014,9 @@ async function handleOptions(options, url) {
|
|||||||
name = name.toLowerCase().replace(/\s+/g, '-');
|
name = name.toLowerCase().replace(/\s+/g, '-');
|
||||||
}
|
}
|
||||||
if (!isValidName(name, platform)) {
|
if (!isValidName(name, platform)) {
|
||||||
const errorMsg = platform === 'linux'
|
const LINUX_NAME_ERROR = `✕ Name should only include lowercase letters, numbers, and dashes (not leading dashes). Examples: com-123-xxx, 123pan, pan123, weread, we-read, 123.`;
|
||||||
? `✕ Name should only include letters, numbers, dashes, and spaces. Spaces will be converted to dashes. Examples: Google Translate → google-translate, 123pan, weread.`
|
const DEFAULT_NAME_ERROR = `✕ Name should only include letters, numbers, dashes, and spaces (not leading dashes and spaces). Examples: 123pan, 123Pan, Pan123, weread, WeRead, WERead, we-read, We Read, 123.`;
|
||||||
: `✕ Name should only include letters, numbers, dashes, and spaces (not leading dashes and spaces). Examples: Google Translate, 123pan, WeRead, we-read.`;
|
const errorMsg = platform === 'linux' ? LINUX_NAME_ERROR : DEFAULT_NAME_ERROR;
|
||||||
logger.error(errorMsg);
|
logger.error(errorMsg);
|
||||||
if (isActions) {
|
if (isActions) {
|
||||||
name = resolveAppName(url, platform);
|
name = resolveAppName(url, platform);
|
||||||
@@ -1234,7 +1031,7 @@ async function handleOptions(options, url) {
|
|||||||
name,
|
name,
|
||||||
identifier: getIdentifier(url),
|
identifier: getIdentifier(url),
|
||||||
};
|
};
|
||||||
appOptions.icon = await handleIcon(appOptions, url);
|
appOptions.icon = await handleIcon(appOptions);
|
||||||
return appOptions;
|
return appOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -41,7 +41,7 @@
|
|||||||
"cli:dev": "cross-env NODE_ENV=development rollup -c rollup.config.js -w",
|
"cli:dev": "cross-env NODE_ENV=development rollup -c rollup.config.js -w",
|
||||||
"cli:build": "cross-env NODE_ENV=production rollup -c rollup.config.js",
|
"cli:build": "cross-env NODE_ENV=production rollup -c rollup.config.js",
|
||||||
"test": "npm run cli:build && PAKE_CREATE_APP=1 node tests/index.js",
|
"test": "npm run cli:build && PAKE_CREATE_APP=1 node tests/index.js",
|
||||||
"format": "npx prettier --write . --ignore-unknown && cd src-tauri && cargo fmt --verbose",
|
"format": "prettier --write . --ignore-unknown && cd src-tauri && cargo fmt --verbose",
|
||||||
"hooks:setup": "bash .githooks/setup.sh",
|
"hooks:setup": "bash .githooks/setup.sh",
|
||||||
"postinstall": "npm run hooks:setup",
|
"postinstall": "npm run hooks:setup",
|
||||||
"prepublishOnly": "npm run cli:build"
|
"prepublishOnly": "npm run cli:build"
|
||||||
@@ -58,7 +58,6 @@
|
|||||||
"execa": "^9.6.0",
|
"execa": "^9.6.0",
|
||||||
"file-type": "^18.7.0",
|
"file-type": "^18.7.0",
|
||||||
"fs-extra": "^11.3.1",
|
"fs-extra": "^11.3.1",
|
||||||
"icon-gen": "^5.0.0",
|
|
||||||
"loglevel": "^1.9.2",
|
"loglevel": "^1.9.2",
|
||||||
"ora": "^8.2.0",
|
"ora": "^8.2.0",
|
||||||
"prompts": "^2.4.2",
|
"prompts": "^2.4.2",
|
||||||
@@ -81,6 +80,7 @@
|
|||||||
"@types/update-notifier": "^6.0.8",
|
"@types/update-notifier": "^6.0.8",
|
||||||
"app-root-path": "^3.1.0",
|
"app-root-path": "^3.1.0",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
|
"prettier": "^3.4.2",
|
||||||
"rollup": "^4.46.2",
|
"rollup": "^4.46.2",
|
||||||
"rollup-plugin-typescript2": "^0.36.0",
|
"rollup-plugin-typescript2": "^0.36.0",
|
||||||
"tslib": "^2.8.1",
|
"tslib": "^2.8.1",
|
||||||
|
|||||||
2
rollup.config.js
vendored
2
rollup.config.js
vendored
@@ -7,7 +7,6 @@ import json from "@rollup/plugin-json";
|
|||||||
import replace from "@rollup/plugin-replace";
|
import replace from "@rollup/plugin-replace";
|
||||||
import chalk from "chalk";
|
import chalk from "chalk";
|
||||||
import { spawn, exec } from "child_process";
|
import { spawn, exec } from "child_process";
|
||||||
import fs from "fs";
|
|
||||||
|
|
||||||
const isProduction = process.env.NODE_ENV === "production";
|
const isProduction = process.env.NODE_ENV === "production";
|
||||||
const devPlugins = !isProduction ? [pakeCliDevPlugin()] : [];
|
const devPlugins = !isProduction ? [pakeCliDevPlugin()] : [];
|
||||||
@@ -49,6 +48,7 @@ function pakeCliDevPlugin() {
|
|||||||
|
|
||||||
// 智能检测包管理器
|
// 智能检测包管理器
|
||||||
const detectPackageManager = () => {
|
const detectPackageManager = () => {
|
||||||
|
const fs = require("fs");
|
||||||
if (fs.existsSync("pnpm-lock.yaml")) return "pnpm";
|
if (fs.existsSync("pnpm-lock.yaml")) return "pnpm";
|
||||||
if (fs.existsSync("yarn.lock")) return "yarn";
|
if (fs.existsSync("yarn.lock")) return "yarn";
|
||||||
return "npm";
|
return "npm";
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ use std::str::FromStr;
|
|||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
use tauri::{
|
use tauri::{
|
||||||
image::Image,
|
|
||||||
menu::{MenuBuilder, MenuItemBuilder},
|
menu::{MenuBuilder, MenuItemBuilder},
|
||||||
tray::TrayIconBuilder,
|
tray::TrayIconBuilder,
|
||||||
AppHandle, Manager,
|
AppHandle, Manager,
|
||||||
@@ -10,8 +9,6 @@ use tauri::{
|
|||||||
use tauri_plugin_global_shortcut::{GlobalShortcutExt, Shortcut};
|
use tauri_plugin_global_shortcut::{GlobalShortcutExt, Shortcut};
|
||||||
use tauri_plugin_window_state::{AppHandleExt, StateFlags};
|
use tauri_plugin_window_state::{AppHandleExt, StateFlags};
|
||||||
|
|
||||||
use crate::util::get_pake_config;
|
|
||||||
|
|
||||||
pub fn set_system_tray(app: &AppHandle, show_system_tray: bool) -> tauri::Result<()> {
|
pub fn set_system_tray(app: &AppHandle, show_system_tray: bool) -> tauri::Result<()> {
|
||||||
if !show_system_tray {
|
if !show_system_tray {
|
||||||
app.remove_tray_by_id("pake-tray");
|
app.remove_tray_by_id("pake-tray");
|
||||||
@@ -28,42 +25,6 @@ pub fn set_system_tray(app: &AppHandle, show_system_tray: bool) -> tauri::Result
|
|||||||
|
|
||||||
app.app_handle().remove_tray_by_id("pake-tray");
|
app.app_handle().remove_tray_by_id("pake-tray");
|
||||||
|
|
||||||
// Get tray icon - use custom tray icon if provided, otherwise use app icon
|
|
||||||
let tray_icon = {
|
|
||||||
let (config, _) = get_pake_config();
|
|
||||||
let tray_path = &config.system_tray_path;
|
|
||||||
|
|
||||||
// Check if this is a custom tray icon or app icon
|
|
||||||
if !tray_path.is_empty() && tray_path != "icons/icon.png" {
|
|
||||||
// Try to load the tray icon - could be relative or absolute path
|
|
||||||
let icon_path = if tray_path.starts_with("/") {
|
|
||||||
// Absolute path - use as is
|
|
||||||
tray_path.to_string()
|
|
||||||
} else if tray_path.starts_with(".pake/") || tray_path.starts_with("png/") {
|
|
||||||
// Relative path - prepend manifest dir
|
|
||||||
format!("{}/{}", env!("CARGO_MANIFEST_DIR"), tray_path)
|
|
||||||
} else {
|
|
||||||
// Default fallback path
|
|
||||||
format!("{}/{}", env!("CARGO_MANIFEST_DIR"), tray_path)
|
|
||||||
};
|
|
||||||
|
|
||||||
match Image::from_path(&icon_path) {
|
|
||||||
Ok(icon) => icon,
|
|
||||||
Err(e) => {
|
|
||||||
println!(
|
|
||||||
"Failed to load tray icon from {}: {}, using app icon",
|
|
||||||
icon_path, e
|
|
||||||
);
|
|
||||||
app.default_window_icon().unwrap().clone()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// No custom tray icon, use app icon (which could be downloaded custom icon)
|
|
||||||
println!("Using app icon for system tray (path: {})", tray_path);
|
|
||||||
app.default_window_icon().unwrap().clone()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let tray = TrayIconBuilder::new()
|
let tray = TrayIconBuilder::new()
|
||||||
.menu(&menu)
|
.menu(&menu)
|
||||||
.on_menu_event(move |app, event| match event.id().as_ref() {
|
.on_menu_event(move |app, event| match event.id().as_ref() {
|
||||||
@@ -83,7 +44,7 @@ pub fn set_system_tray(app: &AppHandle, show_system_tray: bool) -> tauri::Result
|
|||||||
}
|
}
|
||||||
_ => (),
|
_ => (),
|
||||||
})
|
})
|
||||||
.icon(tray_icon)
|
.icon(app.default_window_icon().unwrap().clone())
|
||||||
.build(app)?;
|
.build(app)?;
|
||||||
|
|
||||||
tray.set_icon_as_template(false)?;
|
tray.set_icon_as_template(false)?;
|
||||||
|
|||||||
Reference in New Issue
Block a user