Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 53 additions & 2 deletions bin/options/icon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,26 @@ function generateIconServiceUrls(domain: string): string[] {
];
}

/**
* Generates dashboard-icons URLs for an app name.
* Uses walkxcode/dashboard-icons as a final fallback for selfhosted apps.
* Keeps matching conservative to avoid overriding valid site-specific icons.
*/
function generateDashboardIconUrls(appName: string): string[] {
const baseUrl = 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png';
const name = appName.toLowerCase().trim();
const slugs = new Set<string>();

// Exact name
slugs.add(name);
// Replace spaces with hyphens
slugs.add(name.replace(/\s+/g, '-'));

return [...slugs]
.filter((s) => s.length > 0)
.map((slug) => `${baseUrl}/${slug}.png`);
}

/**
* Attempts to fetch favicon from website
*/
Expand All @@ -409,14 +429,14 @@ async function tryGetFavicon(
const domain = new URL(url).hostname;
const spinner = getSpinner(`Fetching icon from ${domain}...`);

const serviceUrls = generateIconServiceUrls(domain);

const isCI =
process.env.CI === 'true' || process.env.GITHUB_ACTIONS === 'true';
const downloadTimeout = isCI
? ICON_CONFIG.downloadTimeout.ci
: ICON_CONFIG.downloadTimeout.default;

const serviceUrls = generateIconServiceUrls(domain);

for (const serviceUrl of serviceUrls) {
try {
const faviconPath = await downloadIcon(
Expand Down Expand Up @@ -445,6 +465,37 @@ async function tryGetFavicon(
}
}

// Final fallback for selfhosted apps behind auth where domain-based
// services cannot access the site favicon.
if (appName) {
const dashboardIconUrls = generateDashboardIconUrls(appName);
for (const iconUrl of dashboardIconUrls) {
try {
const iconPath = await downloadIcon(iconUrl, false, downloadTimeout);
if (!iconPath) continue;

const convertedPath = await convertIconFormat(iconPath, appName);
if (convertedPath) {
const finalPath = await copyWindowsIconIfNeeded(
convertedPath,
appName,
);
spinner.succeed(
chalk.green(
`Icon found via dashboard-icons fallback for "${appName}"!`,
),
);
return finalPath;
}
} catch (error: unknown) {
if (error instanceof Error) {
logger.debug(`Dashboard icon ${iconUrl} failed: ${error.message}`);
}
continue;
}
}
}

spinner.warn(`No favicon found for ${domain}. Using default.`);
return null;
} catch (error) {
Expand Down