From 29b75041099001095df2a832d6ce1ded74fd203b Mon Sep 17 00:00:00 2001 From: Tw93 Date: Sat, 16 Aug 2025 22:44:31 +0800 Subject: [PATCH] :sparkles: Add prettier as devDependency to improve development workflow --- bin/builders/BaseBuilder.ts | 4 +- bin/helpers/merge.ts | 142 +++------------ bin/options/icon.ts | 262 +++++----------------------- bin/options/index.ts | 10 +- bin/utils/dir.ts | 5 - dist/cli.js | 339 ++++++++---------------------------- package.json | 4 +- rollup.config.js | 2 +- src-tauri/src/app/setup.rs | 41 +---- 9 files changed, 152 insertions(+), 657 deletions(-) diff --git a/bin/builders/BaseBuilder.ts b/bin/builders/BaseBuilder.ts index 6776e50..a22db05 100644 --- a/bin/builders/BaseBuilder.ts +++ b/bin/builders/BaseBuilder.ts @@ -129,7 +129,7 @@ export default abstract class BaseBuilder { '.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 if (IS_MAC && this.options.targets === 'app') { @@ -140,7 +140,7 @@ export default abstract class BaseBuilder { if (IS_MAC) { const macOSVersion = this.getMacOSMajorVersion(); if (macOSVersion >= 23) { - fullCommand += ',macos-proxy'; + fullCommand += ' --features macos-proxy'; } } diff --git a/bin/helpers/merge.ts b/bin/helpers/merge.ts index 2ec947a..906258a 100644 --- a/bin/helpers/merge.ts +++ b/bin/helpers/merge.ts @@ -4,11 +4,7 @@ import fsExtra from 'fs-extra'; import combineFiles from '@/utils/combine'; import logger from '@/options/logger'; import { PakeAppOptions, PlatformMap } from '@/types'; -import { - tauriConfigDirectory, - npmDirectory, - getUserHomeDir, -} from '@/utils/dir'; +import { tauriConfigDirectory, npmDirectory } from '@/utils/dir'; export async function mergeConfig( url: string, @@ -186,10 +182,8 @@ export async function mergeConfig( logger.warn(`✼ ${iconInfo.message}, but you give ${customIconExt}`); tauriConf.bundle.icon = [iconInfo.defaultIcon]; } else { - // Save icon to .pake directory instead of src-tauri - const iconPath = path.join(tauriConfigDirectory, iconInfo.path); - await fsExtra.ensureDir(path.dirname(iconPath)); - tauriConf.bundle.resources = [`.pake/${iconInfo.path}`]; + const iconPath = path.join(npmDirectory, 'src-tauri/', iconInfo.path); + tauriConf.bundle.resources = [iconInfo.path]; await fsExtra.copy(options.icon, iconPath); } @@ -205,20 +199,30 @@ export async function mergeConfig( tauriConf.bundle.icon = [iconInfo.defaultIcon]; } - // Set system tray icon path - let trayIconPath = 'icons/icon.png'; // default fallback - - if (showSystemTray) { - if (systemTrayIcon.length > 0) { - // User provided custom system tray icon - trayIconPath = await handleCustomTrayIcon( - systemTrayIcon, - name, - tauriConfigDirectory, - ); - } else { - // Use original downloaded PNG icon for system tray - trayIconPath = await handleDownloadedTrayIcon(name, tauriConfigDirectory); + // Set tray icon path. + let trayIconPath = + platform === 'darwin' ? 'png/icon_512.png' : tauriConf.bundle.icon[0]; + if (systemTrayIcon.length > 0) { + try { + await fsExtra.pathExists(systemTrayIcon); + // 需要判断图标格式,默认只支持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.`); + } + } 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 }; + console.log('pakeConfig', tauriConf.pake); await fsExtra.outputJSON(configPath, bundleConf, { spaces: 4 }); const pakeConfigPath = path.join(tauriConfigDirectory, 'pake.json'); await fsExtra.outputJSON(pakeConfigPath, tauriConf.pake, { spaces: 4 }); @@ -278,94 +283,3 @@ export async function mergeConfig( const configJsonPath = path.join(tauriConfigDirectory, 'tauri.conf.json'); 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 { - 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 { - 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; - } -} diff --git a/bin/options/icon.ts b/bin/options/icon.ts index 162b162..d8e47f4 100644 --- a/bin/options/icon.ts +++ b/bin/options/icon.ts @@ -1,243 +1,73 @@ import path from 'path'; import axios from 'axios'; import fsExtra from 'fs-extra'; +import chalk from 'chalk'; import { dir } from 'tmp-promise'; -import { fileTypeFromBuffer } from 'file-type'; -import icongen from 'icon-gen'; -import sharp from 'sharp'; import logger from './logger'; -import { npmDirectory, getUserHomeDir } from '@/utils/dir'; +import { npmDirectory } from '@/utils/dir'; import { IS_LINUX, IS_WIN } from '@/utils/platform'; +import { getSpinner } from '@/utils/info'; +import { fileTypeFromBuffer } from 'file-type'; import { PakeAppOptions } from '@/types'; -// Constants -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 { - 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 { - 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) { +export async function handleIcon(options: PakeAppOptions) { if (options.icon) { if (options.icon.startsWith('http')) { return downloadIcon(options.icon); + } else { + return path.resolve(options.icon); } - 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: 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`, + } else { + logger.warn( + '✼ No icon given, default in use. For a custom icon, use --icon option.', ); - - 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: string, - appName: string, -): Promise { - 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; + 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); } } -/** - * Downloads icon from URL - */ -export async function downloadIcon( - iconUrl: string, - showSpinner = true, -): Promise { +export async function downloadIcon(iconUrl: string) { + const spinner = getSpinner('Downloading icon...'); try { - const response = await axios.get(iconUrl, { + const iconResponse = await axios.get(iconUrl, { responseType: 'arraybuffer', - timeout: ICON_CONFIG.downloadTimeout, }); + const iconData = await iconResponse.data; - const iconData = response.data; - if (!iconData || iconData.byteLength < ICON_CONFIG.minFileSize) return null; - - const fileDetails = await fileTypeFromBuffer(iconData); - if ( - !fileDetails || - !ICON_CONFIG.supportedFormats.includes(fileDetails.ext as any) - ) { + if (!iconData) { return null; } - return await saveIconFile(iconData, fileDetails.ext); - } catch (error) { - if (showSpinner && !(error.response?.status === 404)) { - throw error; + const fileDetails = await fileTypeFromBuffer(iconData); + if (!fileDetails) { + return null; } - 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 { - 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; -} diff --git a/bin/options/index.ts b/bin/options/index.ts index 347573d..564ac0c 100644 --- a/bin/options/index.ts +++ b/bin/options/index.ts @@ -43,13 +43,11 @@ export default async function handleOptions( } 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 = - platform === 'linux' - ? `✕ 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.`; - + platform === 'linux' ? LINUX_NAME_ERROR : DEFAULT_NAME_ERROR; logger.error(errorMsg); - if (isActions) { name = resolveAppName(url, platform); logger.warn(`✼ Inside github actions, use the default name: ${name}`); @@ -64,7 +62,7 @@ export default async function handleOptions( identifier: getIdentifier(url), }; - appOptions.icon = await handleIcon(appOptions, url); + appOptions.icon = await handleIcon(appOptions); return appOptions; } diff --git a/bin/utils/dir.ts b/bin/utils/dir.ts index 0936fb2..c261667 100644 --- a/bin/utils/dir.ts +++ b/bin/utils/dir.ts @@ -1,5 +1,4 @@ import path from 'path'; -import os from 'os'; import { fileURLToPath } from 'url'; // Convert the current module URL to a file path @@ -13,7 +12,3 @@ export const tauriConfigDirectory = path.join( 'src-tauri', '.pake', ); - -export function getUserHomeDir(): string { - return os.homedir(); -} diff --git a/dist/cli.js b/dist/cli.js index c762b05..3b4aea0 100644 --- a/dist/cli.js +++ b/dist/cli.js @@ -7,7 +7,6 @@ import prompts from 'prompts'; import { execa, execaSync } from 'execa'; import crypto from 'crypto'; import ora from 'ora'; -import os from 'os'; import { fileURLToPath } from 'url'; import dns from 'dns'; import http from 'http'; @@ -17,12 +16,10 @@ import updateNotifier from 'update-notifier'; import axios from 'axios'; import { dir } from 'tmp-promise'; import { fileTypeFromBuffer } from 'file-type'; -import icongen from 'icon-gen'; -import sharp from 'sharp'; import * as psl from 'psl'; 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 engines = { node: ">=16.0.0" @@ -80,7 +77,6 @@ var dependencies = { execa: "^9.6.0", "file-type": "^18.7.0", "fs-extra": "^11.3.1", - "icon-gen": "^5.0.0", loglevel: "^1.9.2", ora: "^8.2.0", prompts: "^2.4.2", @@ -320,9 +316,6 @@ const currentModulePath = fileURLToPath(import.meta.url); // Resolve the parent directory of the current module const npmDirectory = path.join(path.dirname(currentModulePath), '..'); const tauriConfigDirectory = path.join(npmDirectory, 'src-tauri', '.pake'); -function getUserHomeDir() { - return os.homedir(); -} async function shellExec(command, timeout = 300000) { try { @@ -456,18 +449,11 @@ async function mergeConfig(url, options, tauriConf) { const srcTauriDir = path.join(npmDirectory, 'src-tauri'); await fsExtra.ensureDir(tauriConfigDirectory); // Copy source config files to .pake directory (as templates) - const sourceFiles = [ - 'tauri.conf.json', - 'tauri.macos.conf.json', - 'tauri.windows.conf.json', - 'tauri.linux.conf.json', - 'pake.json', - ]; + const sourceFiles = ['tauri.conf.json', 'tauri.macos.conf.json', 'tauri.windows.conf.json', 'tauri.linux.conf.json', 'pake.json']; await Promise.all(sourceFiles.map(async (file) => { const sourcePath = path.join(srcTauriDir, file); const destPath = path.join(tauriConfigDirectory, file); - if ((await fsExtra.pathExists(sourcePath)) && - !(await fsExtra.pathExists(destPath))) { + if (await fsExtra.pathExists(sourcePath) && !(await fsExtra.pathExists(destPath))) { await fsExtra.copy(sourcePath, destPath); } })); @@ -575,10 +561,8 @@ async function mergeConfig(url, options, tauriConf) { tauriConf.bundle.icon = [iconInfo.defaultIcon]; } else { - // Save icon to .pake directory instead of src-tauri - const iconPath = path.join(tauriConfigDirectory, iconInfo.path); - await fsExtra.ensureDir(path.dirname(iconPath)); - tauriConf.bundle.resources = [`.pake/${iconInfo.path}`]; + const iconPath = path.join(npmDirectory, 'src-tauri/', iconInfo.path); + tauriConf.bundle.resources = [iconInfo.path]; await fsExtra.copy(options.icon, iconPath); } 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.'); tauriConf.bundle.icon = [iconInfo.defaultIcon]; } - // Set system tray icon path - let trayIconPath = 'icons/icon.png'; // default fallback - if (showSystemTray) { - if (systemTrayIcon.length > 0) { - // User provided custom system tray icon - trayIconPath = await handleCustomTrayIcon(systemTrayIcon, name, tauriConfigDirectory); + // Set tray icon path. + let trayIconPath = platform === 'darwin' ? 'png/icon_512.png' : tauriConf.bundle.icon[0]; + if (systemTrayIcon.length > 0) { + try { + await fsExtra.pathExists(systemTrayIcon); + // 需要判断图标格式,默认只支持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 { - // Use original downloaded PNG icon for system tray - trayIconPath = await handleDownloadedTrayIcon(name, tauriConfigDirectory); + catch { + logger.warn(`✼ ${systemTrayIcon} not exists!`); + logger.warn(`✼ Default system tray icon will remain unchanged.`); } } tauriConf.app.trayIcon.iconPath = trayIconPath; @@ -631,6 +625,7 @@ async function mergeConfig(url, options, tauriConf) { }; const configPath = path.join(tauriConfigDirectory, platformConfigPaths[platform]); const bundleConf = { bundle: tauriConf.bundle }; + console.log('pakeConfig', tauriConf.pake); await fsExtra.outputJSON(configPath, bundleConf, { spaces: 4 }); const pakeConfigPath = path.join(tauriConfigDirectory, 'pake.json'); 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'); 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 { constructor(options) { @@ -784,7 +719,7 @@ class BaseBuilder { : 'npm run build'; // Use temporary config directory to avoid modifying source files 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 if (IS_MAC && this.options.targets === 'app') { fullCommand += ' --bundles app'; @@ -793,7 +728,7 @@ class BaseBuilder { if (IS_MAC) { const macOSVersion = this.getMacOSMajorVersion(); if (macOSVersion >= 23) { - fullCommand += ',macos-proxy'; + fullCommand += ' --features macos-proxy'; } } return fullCommand; @@ -954,199 +889,61 @@ async function checkUpdateTips() { }); } -// Constants -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) { +async function handleIcon(options) { if (options.icon) { if (options.icon.startsWith('http')) { return downloadIcon(options.icon); } - 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; - } + else { + return path.resolve(options.icon); } - logger.info(`No favicon found for ${domain}. Using default.`); - return null; } - catch { - return null; + else { + 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); } } -/** - * Downloads icon from URL - */ -async function downloadIcon(iconUrl, showSpinner = true) { +async function downloadIcon(iconUrl) { + const spinner = getSpinner('Downloading icon...'); try { - const response = await axios.get(iconUrl, { + const iconResponse = await axios.get(iconUrl, { responseType: 'arraybuffer', - timeout: ICON_CONFIG.downloadTimeout, }); - const iconData = response.data; - if (!iconData || iconData.byteLength < ICON_CONFIG.minFileSize) - return null; - const fileDetails = await fileTypeFromBuffer(iconData); - if (!fileDetails || !ICON_CONFIG.supportedFormats.includes(fileDetails.ext)) { + const iconData = await iconResponse.data; + if (!iconData) { 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) { - if (showSpinner && !(error.response?.status === 404)) { - throw error; + spinner.fail(chalk.red('Icon download failed!')); + 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. function getDomain(inputUrl) { @@ -1217,9 +1014,9 @@ async function handleOptions(options, url) { name = name.toLowerCase().replace(/\s+/g, '-'); } if (!isValidName(name, platform)) { - const errorMsg = platform === 'linux' - ? `✕ 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.`; + 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 = platform === 'linux' ? LINUX_NAME_ERROR : DEFAULT_NAME_ERROR; logger.error(errorMsg); if (isActions) { name = resolveAppName(url, platform); @@ -1234,7 +1031,7 @@ async function handleOptions(options, url) { name, identifier: getIdentifier(url), }; - appOptions.icon = await handleIcon(appOptions, url); + appOptions.icon = await handleIcon(appOptions); return appOptions; } diff --git a/package.json b/package.json index 7c19c98..57f924e 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ "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", "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", "postinstall": "npm run hooks:setup", "prepublishOnly": "npm run cli:build" @@ -58,7 +58,6 @@ "execa": "^9.6.0", "file-type": "^18.7.0", "fs-extra": "^11.3.1", - "icon-gen": "^5.0.0", "loglevel": "^1.9.2", "ora": "^8.2.0", "prompts": "^2.4.2", @@ -81,6 +80,7 @@ "@types/update-notifier": "^6.0.8", "app-root-path": "^3.1.0", "cross-env": "^7.0.3", + "prettier": "^3.4.2", "rollup": "^4.46.2", "rollup-plugin-typescript2": "^0.36.0", "tslib": "^2.8.1", diff --git a/rollup.config.js b/rollup.config.js index 715698d..d7da4f3 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -7,7 +7,6 @@ import json from "@rollup/plugin-json"; import replace from "@rollup/plugin-replace"; import chalk from "chalk"; import { spawn, exec } from "child_process"; -import fs from "fs"; const isProduction = process.env.NODE_ENV === "production"; const devPlugins = !isProduction ? [pakeCliDevPlugin()] : []; @@ -49,6 +48,7 @@ function pakeCliDevPlugin() { // 智能检测包管理器 const detectPackageManager = () => { + const fs = require("fs"); if (fs.existsSync("pnpm-lock.yaml")) return "pnpm"; if (fs.existsSync("yarn.lock")) return "yarn"; return "npm"; diff --git a/src-tauri/src/app/setup.rs b/src-tauri/src/app/setup.rs index 16dc083..a66a427 100644 --- a/src-tauri/src/app/setup.rs +++ b/src-tauri/src/app/setup.rs @@ -2,7 +2,6 @@ use std::str::FromStr; use std::sync::{Arc, Mutex}; use std::time::{Duration, Instant}; use tauri::{ - image::Image, menu::{MenuBuilder, MenuItemBuilder}, tray::TrayIconBuilder, AppHandle, Manager, @@ -10,8 +9,6 @@ use tauri::{ use tauri_plugin_global_shortcut::{GlobalShortcutExt, Shortcut}; 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<()> { if !show_system_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"); - // 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() .menu(&menu) .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)?; tray.set_icon_as_template(false)?;