Icons for years (#220)

This commit is contained in:
Simon J
2025-02-07 11:52:59 +11:00
committed by GitHub
parent d8d532e35f
commit 2961b651d9
11 changed files with 238 additions and 64 deletions

View File

@@ -48,8 +48,16 @@ export type IconFeatures = {
viewPortIncreasePercent: number | undefined;
backgroundColor: string | undefined;
foregroundColor: string | undefined;
text: string | undefined;
};
export const NO_FEATURES: IconFeatures = {
viewPortIncreasePercent: undefined,
backgroundColor: undefined,
foregroundColor: undefined,
text: undefined
}
export type IconSpec = {
svg: string | undefined;
features: Partial<IconFeatures> | undefined;
@@ -93,17 +101,11 @@ export class SvgIcon implements Icon {
constructor(
svg: string,
features: Partial<IconFeatures> = {
viewPortIncreasePercent: undefined,
backgroundColor: undefined,
foregroundColor: undefined,
}
features: Partial<IconFeatures> = {}
) {
this.svg = svg;
this.features = {
viewPortIncreasePercent: undefined,
backgroundColor: undefined,
foregroundColor: undefined,
...NO_FEATURES,
...features,
};
}
@@ -131,6 +133,17 @@ export class SvgIcon implements Icon {
viewBox = viewBox.increasePercent(this.features.viewPortIncreasePercent);
element("//svg:svg").setAttribute("viewBox", viewBox.toString());
}
if(this.features.text) {
elements("//svg:text").forEach((text) => {
text.textContent = this.features.text!
});
}
if (this.features.foregroundColor) {
elements("//svg:path|//svg:text").forEach((path) => {
if (path.getAttribute("fill")) path.setAttribute("stroke", this.features.foregroundColor!);
else path.setAttribute("fill", this.features.foregroundColor!);
});
}
if (this.features.backgroundColor) {
const rect = doc.createElementNS(SVG_NS, "rect");
rect.setAttribute("x", `${viewBox.minX}`);
@@ -142,12 +155,6 @@ export class SvgIcon implements Icon {
const svg = element("//svg:svg")
svg.insertBefore(rect, svg.childNodes[0]!);
}
if (this.features.foregroundColor) {
elements("//svg:path").forEach((path) => {
if (path.getAttribute("fill")) path.setAttribute("stroke", this.features.foregroundColor!);
else path.setAttribute("fill", this.features.foregroundColor!);
});
}
return xmlTidy(doc as unknown as Node);
};
@@ -230,20 +237,24 @@ export type ICON =
| "yoda"
| "heart"
| "star"
| "solidStar";
| "solidStar"
| "yy"
| "yyyy";
const iconFrom = (name: string) =>
const svgFrom = (name: string) =>
new SvgIcon(
fs
.readFileSync(path.resolve(__dirname, "..", "web", "icons", name))
.toString()
);
const iconFrom = (name: string) => svgFrom(name).with({ features: { viewPortIncreasePercent: 80 } });
export const ICONS: Record<ICON, SvgIcon> = {
artists: iconFrom("navidrome-artists.svg"),
albums: iconFrom("navidrome-all.svg"),
radio: iconFrom("navidrome-radio.svg"),
blank: iconFrom("blank.svg"),
blank: svgFrom("blank.svg"),
playlists: iconFrom("navidrome-playlists.svg"),
genres: iconFrom("Theatre-Mask-111172.svg"),
random: iconFrom("navidrome-random.svg"),
@@ -308,7 +319,9 @@ export const ICONS: Record<ICON, SvgIcon> = {
yoda: iconFrom("Yoda-68107.svg"),
heart: iconFrom("Heart-85038.svg"),
star: iconFrom("Star-16101.svg"),
solidStar: iconFrom("Star-43879.svg")
solidStar: iconFrom("Star-43879.svg"),
yy: svgFrom("yy.svg"),
yyyy: svgFrom("yyyy.svg"),
};
export const STAR_WARS = [ICONS.c3po, ICONS.chewy, ICONS.darth, ICONS.skywalker, ICONS.leia, ICONS.r2d2, ICONS.yoda];

View File

@@ -498,16 +498,18 @@ function server(
}
});
app.get("/icon/:type/size/:size", (req, res) => {
const type = req.params["type"]!;
app.get("/icon/:type_text/size/:size", (req, res) => {
const match = (req.params["type_text"] || "")!.match("^([A-Za-z0-9]+)(?:\:([A-Za-z0-9]+))?$")
if (!match)
return res.status(400).send();
const type = match[1]!
const text = match[2]
const size = req.params["size"]!;
if (!Object.keys(ICONS).includes(type)) {
return res.status(404).send();
} else if (
size != "legacy" &&
!SONOS_RECOMMENDED_IMAGE_SIZES.includes(size)
) {
} else if (size != "legacy" && !SONOS_RECOMMENDED_IMAGE_SIZES.includes(size)) {
return res.status(400).send();
} else {
let icon = (ICONS as any)[type]! as Icon;
@@ -528,8 +530,8 @@ function server(
icon
.apply(
features({
viewPortIncreasePercent: 80,
...serverOpts.iconColors,
text: text
})
)
.apply(festivals(clock))

View File

@@ -251,11 +251,12 @@ const genre = (bonobUrl: URLBuilder, genre: Genre) => ({
albumArtURI: iconArtURI(bonobUrl, iconForGenre(genre.name)).href(),
});
const year = (bonobUrl: URLBuilder, year: Year) => ({
const yyyy = (bonobUrl: URLBuilder, year: Year) => ({
itemType: "albumList",
id: `year:${year.year}`,
title: year.year,
albumArtURI: iconArtURI(bonobUrl, "music").href(),
// todo: maybe year.year should be nullable?
albumArtURI: year.year !== "?" ? iconArtURI(bonobUrl, "yyyy", year.year).href() : iconArtURI(bonobUrl, "music").href(),
});
const playlist = (bonobUrl: URLBuilder, playlist: Playlist) => ({
@@ -286,9 +287,9 @@ export const coverArtURI = (
O.getOrElseW(() => iconArtURI(bonobUrl, "vinyl"))
);
export const iconArtURI = (bonobUrl: URLBuilder, icon: ICON) =>
export const iconArtURI = (bonobUrl: URLBuilder, icon: ICON, text: string | undefined = undefined) =>
bonobUrl.append({
pathname: `/icon/${icon}/size/legacy`,
pathname: `/icon/${text == undefined ? icon : `${icon}:${text}`}/size/legacy`,
});
export const sonosifyMimeType = (mimeType: string) =>
@@ -888,7 +889,7 @@ function bindSmapiSoapServiceToExpress(
.then(([page, total]) =>
getMetadataResult({
mediaCollection: page.map((it) =>
year(bonobUrl, it)
yyyy(bonobUrl, it)
),
index: paging._index,
total,

View File

@@ -805,7 +805,7 @@ export class SubsonicMusicLibrary implements MusicLibrary {
years = async () => {
const q: AlbumQuery = {
_index: 0,
_count: 100000, // FIXME: better than this ?
_count: 100000, // FIXME: better than this, probably doesnt work anyway as max _count is 500 or something
type: "alphabeticalByArtist",
};
const years = this.subsonic.getAlbumList2(this.credentials, q)