feat: #420 reimplement icon picker (#421)

This commit is contained in:
Manuel
2024-05-04 23:00:15 +02:00
committed by GitHub
parent 51aaab2f23
commit 60a35e2583
37 changed files with 2974 additions and 10 deletions

View File

@@ -0,0 +1,42 @@
import { GitHubIconRepository } from "./repositories/github.icon-repository";
import { JsdelivrIconRepository } from "./repositories/jsdelivr.icon-repository";
import type { RepositoryIconGroup } from "./types";
const repositories = [
new GitHubIconRepository(
"Walkxcode",
"walkxcode/dashboard-icons",
undefined,
new URL("https://github.com/walkxcode/dashboard-icons"),
new URL(
"https://api.github.com/repos/walkxcode/dashboard-icons/git/trees/main?recursive=true",
),
"https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/{0}",
),
new JsdelivrIconRepository(
"Papirus",
"PapirusDevelopmentTeam/papirus-icon-theme",
"GPL-3.0",
new URL("https://github.com/PapirusDevelopmentTeam/papirus-icon-theme"),
new URL(
"https://data.jsdelivr.com/v1/packages/gh/PapirusDevelopmentTeam/papirus_icons@master?structure=flat",
),
"https://cdn.jsdelivr.net/gh/PapirusDevelopmentTeam/papirus_icons/{0}",
),
new JsdelivrIconRepository(
"Homelab SVG assets",
"loganmarchione/homelab-svg-assets",
"MIT",
new URL("https://github.com/loganmarchione/homelab-svg-assets"),
new URL(
"https://data.jsdelivr.com/v1/packages/gh/loganmarchione/homelab-svg-assets@main?structure=flat",
),
"https://cdn.jsdelivr.net/gh/loganmarchione/homelab-svg-assets/{0}",
),
];
export const fetchIconsAsync = async (): Promise<RepositoryIconGroup[]> => {
return await Promise.all(
repositories.map(async (repository) => await repository.getAllIconsAsync()),
);
};

View File

@@ -0,0 +1,72 @@
import type { IconRepositoryLicense } from "../types/icon-repository-license";
import type { RepositoryIconGroup } from "../types/repository-icon-group";
import { IconRepository } from "./icon-repository";
export class GitHubIconRepository extends IconRepository {
constructor(
public readonly name: string,
public readonly slug: string,
public readonly license: IconRepositoryLicense,
public readonly repositoryUrl?: URL,
public readonly repositoryIndexingUrl?: URL,
public readonly repositoryBlobUrlTemplate?: string,
) {
super(
name,
slug,
license,
repositoryUrl,
repositoryIndexingUrl,
repositoryBlobUrlTemplate,
);
}
protected async getAllIconsInternalAsync(): Promise<RepositoryIconGroup> {
if (!this.repositoryIndexingUrl || !this.repositoryBlobUrlTemplate) {
throw new Error("Repository URLs are required for this repository");
}
const response = await fetch(this.repositoryIndexingUrl);
const listOfFiles = (await response.json()) as GitHubApiResponse;
return {
success: true,
icons: listOfFiles.tree
.filter((treeItem) =>
this.allowedImageFileTypes.some((allowedExtension) =>
treeItem.path.includes(allowedExtension),
),
)
.map((treeItem) => {
const fileNameWithExtension =
this.getFileNameWithoutExtensionFromPath(treeItem.path);
return {
imageUrl: new URL(
this.repositoryBlobUrlTemplate!.replace("{0}", treeItem.path),
),
fileNameWithExtension: fileNameWithExtension,
local: false,
sizeInBytes: treeItem.size,
checksum: treeItem.sha,
};
}),
slug: this.slug,
};
}
}
interface GitHubApiResponse {
sha: string;
url: string;
tree: TreeItem[];
truncated: boolean;
}
export interface TreeItem {
path: string;
mode: string;
sha: string;
url: string;
size?: number;
}

View File

@@ -0,0 +1,38 @@
import { logger } from "@homarr/log";
import type { IconRepositoryLicense } from "../types/icon-repository-license";
import type { RepositoryIconGroup } from "../types/repository-icon-group";
export abstract class IconRepository {
protected readonly allowedImageFileTypes = [".png", ".svg", ".jpeg"];
protected constructor(
public readonly name: string,
public readonly slug: string,
public readonly license: IconRepositoryLicense,
public readonly repositoryUrl?: URL,
public readonly repositoryIndexingUrl?: URL,
public readonly repositoryBlobUrlTemplate?: string,
) {}
public async getAllIconsAsync(): Promise<RepositoryIconGroup> {
try {
return await this.getAllIconsInternalAsync();
} catch (err) {
logger.error(
`Unable to request icons from repository "${this.slug}": ${JSON.stringify(err)}`,
);
return {
success: false,
icons: [],
slug: this.slug,
};
}
}
protected abstract getAllIconsInternalAsync(): Promise<RepositoryIconGroup>;
protected getFileNameWithoutExtensionFromPath(path: string) {
return path.replace(/^.*[\\/]/, "");
}
}

View File

@@ -0,0 +1,63 @@
import type { IconRepositoryLicense } from "../types/icon-repository-license";
import type { RepositoryIconGroup } from "../types/repository-icon-group";
import { IconRepository } from "./icon-repository";
export class JsdelivrIconRepository extends IconRepository {
constructor(
public readonly name: string,
public readonly slug: string,
public readonly license: IconRepositoryLicense,
public readonly repositoryUrl: URL,
public readonly repositoryIndexingUrl: URL,
public readonly repositoryBlobUrlTemplate: string,
) {
super(
name,
slug,
license,
repositoryUrl,
repositoryIndexingUrl,
repositoryBlobUrlTemplate,
);
}
protected async getAllIconsInternalAsync(): Promise<RepositoryIconGroup> {
const response = await fetch(this.repositoryIndexingUrl);
const listOfFiles = (await response.json()) as JsdelivrApiResponse;
return {
success: true,
icons: listOfFiles.files
.filter((file) =>
this.allowedImageFileTypes.some((allowedImageFileType) =>
file.name.includes(allowedImageFileType),
),
)
.map((file) => {
const fileNameWithExtension =
this.getFileNameWithoutExtensionFromPath(file.name);
return {
imageUrl: new URL(
this.repositoryBlobUrlTemplate.replace("{0}", file.name),
),
fileNameWithExtension: fileNameWithExtension,
local: false,
sizeInBytes: file.size,
checksum: file.hash,
};
}),
slug: this.slug,
};
}
}
interface JsdelivrApiResponse {
files: JsdelivrFile[];
}
interface JsdelivrFile {
name: string;
size: number;
hash: string;
}

View File

@@ -0,0 +1 @@
export type IconRepositoryLicense = "MIT" | "GPL-3.0" | undefined;

View File

@@ -0,0 +1,3 @@
export * from "./icon-repository-license";
export * from "./repository-icon-group";
export * from "./repository-icon";

View File

@@ -0,0 +1,7 @@
import type { RepositoryIcon } from "./repository-icon";
export interface RepositoryIconGroup {
icons: RepositoryIcon[];
success: boolean;
slug: string;
}

View File

@@ -0,0 +1,7 @@
export interface RepositoryIcon {
fileNameWithExtension: string;
sizeInBytes?: number;
imageUrl: URL;
local: boolean;
checksum: string;
}