mirror of
https://github.com/bookorbit/bookorbit.git
synced 2026-11-03 00:11:37 -06:00
feat: add custom icons support
Add full-stack support for uploading, managing, and using custom SVG icons for libraries and smart scopes. Includes database schema, server module for storage and sanitization, and client UI under Appearance settings.
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
export const CUSTOM_ICON_PREFIX = "custom:";
|
||||
export const CUSTOM_ICON_SLUG_MAX_LENGTH = 80;
|
||||
export const CUSTOM_ICON_NAME_MAX_LENGTH = 120;
|
||||
export const ICON_VALUE_MAX_LENGTH = 100;
|
||||
export const CUSTOM_ICON_MAX_FILE_SIZE = 128 * 1024;
|
||||
export const CUSTOM_ICON_MAX_UPLOAD_FILES = 50;
|
||||
export const CUSTOM_ICON_DEFAULT_PAGE_SIZE = 48;
|
||||
export const CUSTOM_ICON_MAX_PAGE_SIZE = 100;
|
||||
export const CUSTOM_ICON_CATALOG_LIMIT = 500;
|
||||
|
||||
export const CUSTOM_ICON_SORTS = ["newest", "name"] as const;
|
||||
export type CustomIconSort = (typeof CUSTOM_ICON_SORTS)[number];
|
||||
|
||||
export interface CustomIcon {
|
||||
slug: string;
|
||||
name: string;
|
||||
svgUrl: string;
|
||||
fileHash: string;
|
||||
fileSize: number;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface CustomIconPage {
|
||||
items: CustomIcon[];
|
||||
total: number;
|
||||
page: number;
|
||||
size: number;
|
||||
}
|
||||
|
||||
export interface CustomIconCatalog {
|
||||
items: CustomIcon[];
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface CustomIconUsage {
|
||||
total: number;
|
||||
libraries: number;
|
||||
collections: number;
|
||||
smartScopes: number;
|
||||
}
|
||||
|
||||
export interface CustomIconStageItem {
|
||||
filename: string;
|
||||
ok: boolean;
|
||||
error?: string;
|
||||
suggestedName?: string;
|
||||
sanitizedSvg?: string;
|
||||
fileHash?: string;
|
||||
duplicateOfSlug?: string;
|
||||
duplicateOfName?: string;
|
||||
}
|
||||
|
||||
export interface CustomIconStageResponse {
|
||||
items: CustomIconStageItem[];
|
||||
}
|
||||
|
||||
export interface CustomIconUploadMetaItem {
|
||||
filename: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface CustomIconUploadItem {
|
||||
filename: string;
|
||||
status: "created" | "failed";
|
||||
icon?: CustomIcon;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface CustomIconUploadResponse {
|
||||
items: CustomIconUploadItem[];
|
||||
}
|
||||
|
||||
export interface BulkDeleteCustomIconsRequest {
|
||||
slugs: string[];
|
||||
}
|
||||
|
||||
export interface BulkDeleteCustomIconsResponse {
|
||||
deleted: string[];
|
||||
failed: string[];
|
||||
}
|
||||
|
||||
export function isCustomIconValue(value: string | null | undefined): value is `custom:${string}` {
|
||||
return typeof value === "string" && value.startsWith(CUSTOM_ICON_PREFIX) && value.length > CUSTOM_ICON_PREFIX.length;
|
||||
}
|
||||
|
||||
export function customIconSlugFromValue(value: string | null | undefined): string | null {
|
||||
return isCustomIconValue(value) ? value.slice(CUSTOM_ICON_PREFIX.length) : null;
|
||||
}
|
||||
|
||||
export function customIconValue(slug: string): `custom:${string}` {
|
||||
return `${CUSTOM_ICON_PREFIX}${slug}`;
|
||||
}
|
||||
|
||||
export function customIconSvgUrl(slug: string): string {
|
||||
return `/api/v1/custom-icons/${slug}.svg`;
|
||||
}
|
||||
|
||||
export function slugifyIconName(value: string): string {
|
||||
return value
|
||||
.normalize("NFKD")
|
||||
.replace(/[\u0300-\u036f]/g, "")
|
||||
.toLowerCase()
|
||||
.trim()
|
||||
.replace(/[^a-z0-9]+/g, "-")
|
||||
.replace(/^-+|-+$/g, "")
|
||||
.slice(0, CUSTOM_ICON_SLUG_MAX_LENGTH)
|
||||
.replace(/-+$/g, "");
|
||||
}
|
||||
|
||||
export function isValidIconSlug(slug: string): boolean {
|
||||
return /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(slug) && slug.length <= CUSTOM_ICON_SLUG_MAX_LENGTH;
|
||||
}
|
||||
@@ -6,6 +6,7 @@ export * from "./permissions";
|
||||
export * from "./content-filter";
|
||||
export * from "./metadata-preferences";
|
||||
export * from "./custom-metadata";
|
||||
export * from "./custom-icon";
|
||||
export * from "./metadata-lock";
|
||||
export * from "./dashboard";
|
||||
export * from "./book";
|
||||
|
||||
@@ -20,6 +20,7 @@ export enum Permission {
|
||||
// Administration
|
||||
ManageLibraries = "manage_libraries",
|
||||
ManageMetadataConfig = "manage_metadata_config",
|
||||
ManageIcons = "manage_icons",
|
||||
ManageAppSettings = "manage_app_settings",
|
||||
ManageUsers = "manage_users",
|
||||
ViewAuditLog = "view_audit_log",
|
||||
@@ -43,6 +44,7 @@ export const PERMISSION_LABELS: Record<Permission, string> = {
|
||||
[Permission.ManageEmail]: "Manage email",
|
||||
[Permission.ManageLibraries]: "Manage libraries",
|
||||
[Permission.ManageMetadataConfig]: "Metadata config",
|
||||
[Permission.ManageIcons]: "Manage icons",
|
||||
[Permission.ManageAppSettings]: "App settings",
|
||||
[Permission.ManageUsers]: "Manage users",
|
||||
[Permission.ViewAuditLog]: "View audit log",
|
||||
|
||||
Reference in New Issue
Block a user