mirror of
https://github.com/wkulhanek/bonob.git
synced 2025-12-22 09:53:32 +01:00
Icons for genres with backgrounds, text, and ability to specify text color and font family (#34)
This commit is contained in:
242
src/icon.ts
242
src/icon.ts
@@ -1,11 +1,24 @@
|
||||
import libxmljs, { Element, Attribute } from "libxmljs2";
|
||||
import _ from "underscore";
|
||||
import { Clock, isChristmas, isCNY, isHalloween, isHoli, SystemClock } from "./clock";
|
||||
import fs from "fs";
|
||||
|
||||
import {
|
||||
Clock,
|
||||
isChristmas,
|
||||
isCNY,
|
||||
isHalloween,
|
||||
isHoli,
|
||||
SystemClock,
|
||||
} from "./clock";
|
||||
import path from "path";
|
||||
|
||||
export type Transformation = {
|
||||
viewPortIncreasePercent: number | undefined;
|
||||
backgroundColor: string | undefined;
|
||||
foregroundColor: string | undefined;
|
||||
text: string | undefined;
|
||||
fontColor: string | undefined;
|
||||
fontFamily: string | undefined;
|
||||
};
|
||||
|
||||
const SVG_NS = {
|
||||
@@ -43,13 +56,23 @@ export interface Icon {
|
||||
|
||||
export class ColorOverridingIcon implements Icon {
|
||||
rule: () => Boolean;
|
||||
newColors: () => Pick<Transformation, "backgroundColor" | "foregroundColor">;
|
||||
newColors: () => Partial<
|
||||
Pick<
|
||||
Transformation,
|
||||
"backgroundColor" | "foregroundColor" | "fontColor" | "fontFamily"
|
||||
>
|
||||
>;
|
||||
icon: Icon;
|
||||
|
||||
constructor(
|
||||
icon: Icon,
|
||||
rule: () => Boolean,
|
||||
newColors: () => Pick<Transformation, "backgroundColor" | "foregroundColor">
|
||||
newColors: () => Partial<
|
||||
Pick<
|
||||
Transformation,
|
||||
"backgroundColor" | "foregroundColor" | "fontColor" | "fontFamily"
|
||||
>
|
||||
>
|
||||
) {
|
||||
this.icon = icon;
|
||||
this.rule = rule;
|
||||
@@ -74,6 +97,9 @@ export class SvgIcon implements Icon {
|
||||
viewPortIncreasePercent: undefined,
|
||||
backgroundColor: undefined,
|
||||
foregroundColor: undefined,
|
||||
text: undefined,
|
||||
fontColor: undefined,
|
||||
fontFamily: undefined,
|
||||
}
|
||||
) {
|
||||
this.svg = svg;
|
||||
@@ -106,26 +132,62 @@ export class SvgIcon implements Icon {
|
||||
y: `${viewBox.minY}`,
|
||||
width: `${Math.abs(viewBox.minX) + viewBox.width}`,
|
||||
height: `${Math.abs(viewBox.minY) + viewBox.height}`,
|
||||
style: `fill:${this.transformation.backgroundColor}`,
|
||||
fill: this.transformation.backgroundColor,
|
||||
})
|
||||
);
|
||||
}
|
||||
if (this.transformation.foregroundColor) {
|
||||
(xml.find("//svg:path", SVG_NS) as Element[]).forEach((path) =>
|
||||
path.attr({ style: `fill:${this.transformation.foregroundColor}` })
|
||||
(xml.find("//svg:path", SVG_NS) as Element[]).forEach((path) => {
|
||||
if (path.attr("fill"))
|
||||
path.attr({ stroke: this.transformation.foregroundColor! });
|
||||
else path.attr({ fill: this.transformation.foregroundColor! });
|
||||
});
|
||||
}
|
||||
if (this.transformation.text) {
|
||||
const w = Math.abs(viewBox.minX) + Math.abs(viewBox.width);
|
||||
const h = Math.abs(viewBox.minY) + Math.abs(viewBox.height);
|
||||
const i = Math.floor(0.1 * w);
|
||||
let attr: any = {
|
||||
"font-size": `${Math.floor(h / 4)}`,
|
||||
"font-weight": "bold",
|
||||
};
|
||||
if (this.transformation.fontFamily)
|
||||
attr = { ...attr, "font-family": this.transformation.fontFamily };
|
||||
if (this.transformation.fontColor)
|
||||
attr = { ...attr, style: `fill:${this.transformation.fontColor}` };
|
||||
const g = new Element(xml, "g");
|
||||
g.attr(attr);
|
||||
(xml.get("//svg:svg", SVG_NS) as Element).addChild(
|
||||
g.addChild(
|
||||
new Element(xml, "text")
|
||||
.attr({
|
||||
x: `${viewBox.minX + i}`,
|
||||
y: `${viewBox.minY + Math.floor(0.8 * h)}`,
|
||||
})
|
||||
.text(this.transformation.text)
|
||||
)
|
||||
);
|
||||
}
|
||||
return xml.toString();
|
||||
};
|
||||
}
|
||||
|
||||
export const HOLI_COLORS = ["#06bceb", "#9fc717", "#fbdc10", "#f00b9a", "#fa9705"]
|
||||
export const HOLI_COLORS = [
|
||||
"#06bceb",
|
||||
"#9fc717",
|
||||
"#fbdc10",
|
||||
"#f00b9a",
|
||||
"#fa9705",
|
||||
];
|
||||
|
||||
export const makeFestive = (icon: Icon, clock: Clock = SystemClock): Icon => {
|
||||
const wrap = (
|
||||
icon: Icon,
|
||||
rule: (clock: Clock) => boolean,
|
||||
colors: Pick<Transformation, "backgroundColor" | "foregroundColor">
|
||||
colors: Pick<
|
||||
Transformation,
|
||||
"backgroundColor" | "foregroundColor" | "fontColor"
|
||||
>
|
||||
) =>
|
||||
new ColorOverridingIcon(
|
||||
icon,
|
||||
@@ -133,22 +195,176 @@ export const makeFestive = (icon: Icon, clock: Clock = SystemClock): Icon => {
|
||||
() => colors
|
||||
);
|
||||
|
||||
const xmas = wrap(icon, isChristmas, {
|
||||
let result = icon;
|
||||
|
||||
const apply = (
|
||||
rule: (clock: Clock) => boolean,
|
||||
colors: Pick<
|
||||
Transformation,
|
||||
"backgroundColor" | "foregroundColor" | "fontColor"
|
||||
>
|
||||
) => (result = wrap(result, rule, colors));
|
||||
|
||||
apply(isChristmas, {
|
||||
backgroundColor: "green",
|
||||
foregroundColor: "red",
|
||||
fontColor: "white",
|
||||
});
|
||||
|
||||
const randomHoliColors = _.shuffle([...HOLI_COLORS]);
|
||||
const holi = wrap(xmas, isHoli, {
|
||||
apply(isHoli, {
|
||||
backgroundColor: randomHoliColors.pop(),
|
||||
foregroundColor: randomHoliColors.pop(),
|
||||
fontColor: randomHoliColors.pop(),
|
||||
});
|
||||
const cny = wrap(holi, isCNY, {
|
||||
|
||||
apply(isCNY, {
|
||||
backgroundColor: "red",
|
||||
foregroundColor: "yellow",
|
||||
fontColor: "crimson",
|
||||
});
|
||||
const halloween = wrap(cny, isHalloween, {
|
||||
|
||||
apply(isHalloween, {
|
||||
backgroundColor: "orange",
|
||||
foregroundColor: "black",
|
||||
fontColor: "orangered",
|
||||
});
|
||||
return halloween;
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export type ICON =
|
||||
| "artists"
|
||||
| "albums"
|
||||
| "playlists"
|
||||
| "genres"
|
||||
| "random"
|
||||
| "starred"
|
||||
| "recentlyAdded"
|
||||
| "recentlyPlayed"
|
||||
| "mostPlayed"
|
||||
| "discover"
|
||||
| "blank"
|
||||
| "mushroom"
|
||||
| "african"
|
||||
| "rock"
|
||||
| "metal"
|
||||
| "punk"
|
||||
| "americana"
|
||||
| "guitar"
|
||||
| "book"
|
||||
| "oz"
|
||||
| "rap"
|
||||
| "horror"
|
||||
| "hipHop"
|
||||
| "pop"
|
||||
| "blues"
|
||||
| "classical"
|
||||
| "comedy"
|
||||
| "vinyl"
|
||||
| "electronic"
|
||||
| "pills"
|
||||
| "trumpet"
|
||||
| "conductor"
|
||||
| "reggae"
|
||||
| "music";
|
||||
|
||||
const iconFrom = (name: string) =>
|
||||
new SvgIcon(
|
||||
fs
|
||||
.readFileSync(path.resolve(__dirname, "..", "web", "icons", name))
|
||||
.toString()
|
||||
).with({ viewPortIncreasePercent: 50 });
|
||||
|
||||
export const ICONS: Record<ICON, Icon> = {
|
||||
artists: iconFrom("navidrome-artists.svg"),
|
||||
albums: iconFrom("navidrome-all.svg"),
|
||||
blank: iconFrom("blank.svg"),
|
||||
playlists: iconFrom("navidrome-playlists.svg"),
|
||||
genres: iconFrom("Theatre-Mask-111172.svg"),
|
||||
random: iconFrom("navidrome-random.svg"),
|
||||
starred: iconFrom("navidrome-topRated.svg"),
|
||||
recentlyAdded: iconFrom("navidrome-recentlyAdded.svg"),
|
||||
recentlyPlayed: iconFrom("navidrome-recentlyPlayed.svg"),
|
||||
mostPlayed: iconFrom("navidrome-mostPlayed.svg"),
|
||||
discover: iconFrom("Binoculars-14310.svg"),
|
||||
mushroom: iconFrom("Mushroom-63864.svg"),
|
||||
african: iconFrom("Africa-48087.svg"),
|
||||
rock: iconFrom("Rock-Music-11007.svg"),
|
||||
metal: iconFrom("Metal-Music-17763.svg"),
|
||||
punk: iconFrom("Punk-40450.svg"),
|
||||
americana: iconFrom("US-Capitol-104805.svg"),
|
||||
guitar: iconFrom("Guitar-110433.svg"),
|
||||
book: iconFrom("Book-453.svg"),
|
||||
oz: iconFrom("Kangaroo-16730.svg"),
|
||||
hipHop: iconFrom("Hip-Hop Music-17757.svg"),
|
||||
rap: iconFrom("Rap-24851.svg"),
|
||||
horror: iconFrom("Horror-4387.svg"),
|
||||
pop: iconFrom("Ice-Pop Yellow-94532.svg"),
|
||||
blues: iconFrom("Blues-113548.svg"),
|
||||
classical: iconFrom("Classic-Music-11646.svg"),
|
||||
comedy: iconFrom("Comedy-2-599.svg"),
|
||||
vinyl: iconFrom("Music-Record-102104.svg"),
|
||||
electronic: iconFrom("Electronic-Music-17745.svg"),
|
||||
pills: iconFrom("Pills-112386.svg"),
|
||||
trumpet: iconFrom("Trumpet-17823.svg"),
|
||||
conductor: iconFrom("Music-Conductor-225.svg"),
|
||||
reggae: iconFrom("Reggae-24843.svg"),
|
||||
music: iconFrom("Music-14097.svg")
|
||||
};
|
||||
|
||||
export type RULE = (genre: string) => boolean;
|
||||
|
||||
const eq =
|
||||
(expected: string): RULE =>
|
||||
(value: string) =>
|
||||
expected.toLowerCase() === value.toLowerCase();
|
||||
|
||||
const contains =
|
||||
(expected: string): RULE =>
|
||||
(value: string) =>
|
||||
value.toLowerCase().includes(expected.toLowerCase());
|
||||
|
||||
const containsWithAllTheNonWordCharsRemoved =
|
||||
(expected: string): RULE =>
|
||||
(value: string) =>
|
||||
value.replace(/\W+/, " ").toLowerCase().includes(expected.toLowerCase());
|
||||
|
||||
const GENRE_RULES: [RULE, ICON][] = [
|
||||
[eq("Acid House"), "mushroom"],
|
||||
[contains("Goa"), "mushroom"],
|
||||
[contains("Psy"), "mushroom"],
|
||||
[eq("African"), "african"],
|
||||
[eq("Americana"), "americana"],
|
||||
[contains("Rock"), "rock"],
|
||||
[contains("Folk"), "guitar"],
|
||||
[contains("Book"), "book"],
|
||||
[contains("Australian"), "oz"],
|
||||
[contains("Rap"), "rap"],
|
||||
[containsWithAllTheNonWordCharsRemoved("Hip Hop"), "hipHop"],
|
||||
[contains("Horror"), "horror"],
|
||||
[contains("Metal"), "metal"],
|
||||
[contains("Punk"), "punk"],
|
||||
[contains("Pop"), "pop"],
|
||||
[contains("Blues"), "blues"],
|
||||
[contains("Classical"), "classical"],
|
||||
[contains("Comedy"), "comedy"],
|
||||
[contains("Dub"), "vinyl"],
|
||||
[contains("Turntable"), "vinyl"],
|
||||
[contains("Electro"), "electronic"],
|
||||
[contains("Trance"), "pills"],
|
||||
[contains("Techno"), "pills"],
|
||||
[contains("House"), "pills"],
|
||||
[contains("Rave"), "pills"],
|
||||
[contains("Jazz"), "trumpet"],
|
||||
[contains("Orchestra"), "conductor"],
|
||||
[contains("Reggae"), "reggae"],
|
||||
];
|
||||
|
||||
export function iconForGenre(genre: string): ICON {
|
||||
const [_, name] = GENRE_RULES.find(([rule, _]) => rule(genre)) || [
|
||||
"music",
|
||||
"music",
|
||||
];
|
||||
return name! as ICON;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user