Additional Icon support (#42)

This commit is contained in:
Simon J
2021-09-03 10:26:49 +10:00
committed by GitHub
parent 9dcac1f324
commit f8f8224213
13 changed files with 653 additions and 257 deletions

View File

@@ -4,6 +4,9 @@ export const isChristmas = (clock: Clock = SystemClock) => clock.now().month() =
export const isHalloween = (clock: Clock = SystemClock) => clock.now().month() == 9 && clock.now().date() == 31
export const isHoli = (clock: Clock = SystemClock) => ["2022/03/18", "2023/03/07", "2024/03/25", "2025/03/14"].map(dayjs).find(it => it.isSame(clock.now())) != undefined
export const isCNY = (clock: Clock = SystemClock) => ["2022/02/01", "2023/01/22", "2024/02/10", "2025/02/29"].map(dayjs).find(it => it.isSame(clock.now())) != undefined
export const isCNY_2022 = (clock: Clock = SystemClock) => clock.now().isSame(dayjs("2022/02/01"))
export const isCNY_2023 = (clock: Clock = SystemClock) => clock.now().isSame(dayjs("2023/01/22"))
export const isCNY_2024 = (clock: Clock = SystemClock) => clock.now().isSame(dayjs("2024/02/10"))
export interface Clock {
now(): Dayjs;

View File

@@ -5,19 +5,15 @@ import fs from "fs";
import {
Clock,
isChristmas,
isCNY,
isCNY_2022,
isCNY_2023,
isCNY_2024,
isHalloween,
isHoli,
SystemClock,
} from "./clock";
import path from "path";
export type Transformation = {
viewPortIncreasePercent: number | undefined;
backgroundColor: string | undefined;
foregroundColor: string | undefined;
};
const SVG_NS = {
svg: "http://www.w3.org/2000/svg",
};
@@ -47,55 +43,77 @@ class ViewBox {
`${this.minX} ${this.minY} ${this.width} ${this.height}`;
}
export type IconFeatures = {
viewPortIncreasePercent: number | undefined;
backgroundColor: string | undefined;
foregroundColor: string | undefined;
};
export type IconSpec = {
svg: string | undefined;
features: Partial<IconFeatures> | undefined;
};
export interface Icon {
with(newTransformation: Partial<Transformation>): Icon;
with(spec: Partial<IconSpec>): Icon;
apply(transformer: Transformer): Icon;
}
export class ColorOverridingIcon implements Icon {
rule: () => Boolean;
newColors: () => Partial<
Pick<Transformation, "backgroundColor" | "foregroundColor">
>;
icon: Icon;
export type Transformer = (icon: Icon) => Icon;
constructor(
icon: Icon,
rule: () => Boolean,
newColors: () => Partial<
Pick<Transformation, "backgroundColor" | "foregroundColor">
>
) {
this.icon = icon;
this.rule = rule;
this.newColors = newColors;
}
export function transform(spec: Partial<IconSpec>): Transformer {
return (icon: Icon) =>
icon.with({
...spec,
features: { ...spec.features },
});
}
public with = (transformation: Partial<Transformation>) =>
this.rule()
? this.icon.with({ ...transformation, ...this.newColors() })
: this.icon.with(transformation);
export function features(features: Partial<IconFeatures>): Transformer {
return (icon: Icon) => icon.with({ features });
}
public toString = () => this.with({}).toString();
export function maybeTransform(rule: () => Boolean, transformer: Transformer) {
return (icon: Icon) => (rule() ? transformer(icon) : icon);
}
export function allOf(...transformers: Transformer[]): Transformer {
return (icon: Icon): Icon =>
_.inject(
transformers,
(current: Icon, transformer: Transformer) => transformer(current),
icon
);
}
export class SvgIcon implements Icon {
private svg: string;
private transformation: Transformation;
svg: string;
features: IconFeatures;
constructor(
svg: string,
transformation: Transformation = {
features: Partial<IconFeatures> = {
viewPortIncreasePercent: undefined,
backgroundColor: undefined,
foregroundColor: undefined,
}
) {
this.svg = svg;
this.transformation = transformation;
this.features = {
viewPortIncreasePercent: undefined,
backgroundColor: undefined,
foregroundColor: undefined,
...features,
};
}
public with = (newTransformation: Partial<Transformation>) =>
new SvgIcon(this.svg, { ...this.transformation, ...newTransformation });
public apply = (transformer: Transformer): Icon => transformer(this);
public with = (spec: Partial<IconSpec>) =>
new SvgIcon(spec.svg || this.svg, {
...this.features,
...spec.features,
});
public toString = () => {
const xml = libxmljs.parseXmlString(this.svg, {
@@ -105,30 +123,28 @@ export class SvgIcon implements Icon {
const viewBoxAttr = xml.get("//svg:svg/@viewBox", SVG_NS) as Attribute;
let viewBox = new ViewBox(viewBoxAttr.value());
if (
this.transformation.viewPortIncreasePercent &&
this.transformation.viewPortIncreasePercent > 0
this.features.viewPortIncreasePercent &&
this.features.viewPortIncreasePercent > 0
) {
viewBox = viewBox.increasePercent(
this.transformation.viewPortIncreasePercent
);
viewBox = viewBox.increasePercent(this.features.viewPortIncreasePercent);
viewBoxAttr.value(viewBox.toString());
}
if (this.transformation.backgroundColor) {
if (this.features.backgroundColor) {
(xml.get("//svg:svg/*[1]", SVG_NS) as Element).addPrevSibling(
new Element(xml, "rect").attr({
x: `${viewBox.minX}`,
y: `${viewBox.minY}`,
width: `${Math.abs(viewBox.minX) + viewBox.width}`,
height: `${Math.abs(viewBox.minY) + viewBox.height}`,
fill: this.transformation.backgroundColor,
fill: this.features.backgroundColor,
})
);
}
if (this.transformation.foregroundColor) {
if (this.features.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! });
path.attr({ stroke: this.features.foregroundColor! });
else path.attr({ fill: this.features.foregroundColor! });
});
}
return xml.toString();
@@ -143,49 +159,6 @@ export const HOLI_COLORS = [
"#fa9705",
];
export const makeFestive = (icon: Icon, clock: Clock = SystemClock): Icon => {
const wrap = (
icon: Icon,
rule: (clock: Clock) => boolean,
colors: Pick<Transformation, "backgroundColor" | "foregroundColor">
) =>
new ColorOverridingIcon(
icon,
() => rule(clock),
() => colors
);
let result = icon;
const apply = (
rule: (clock: Clock) => boolean,
colors: Pick<Transformation, "backgroundColor" | "foregroundColor">
) => (result = wrap(result, rule, colors));
apply(isChristmas, {
backgroundColor: "green",
foregroundColor: "red",
});
const randomHoliColors = _.shuffle([...HOLI_COLORS]);
apply(isHoli, {
backgroundColor: randomHoliColors.pop(),
foregroundColor: randomHoliColors.pop(),
});
apply(isCNY, {
backgroundColor: "red",
foregroundColor: "yellow",
});
apply(isHalloween, {
backgroundColor: "orange",
foregroundColor: "black",
});
return result;
};
export type ICON =
| "artists"
| "albums"
@@ -237,7 +210,14 @@ export type ICON =
| "celtic"
| "children"
| "chillout"
| "progressiveRock";
| "progressiveRock"
| "christmas"
| "halloween"
| "yoDragon"
| "yoRabbit"
| "yoTiger"
| "chapel"
| "audioWave";
const iconFrom = (name: string) =>
new SvgIcon(
@@ -246,7 +226,7 @@ const iconFrom = (name: string) =>
.toString()
);
export const ICONS: Record<ICON, Icon> = {
export const ICONS: Record<ICON, SvgIcon> = {
artists: iconFrom("navidrome-artists.svg"),
albums: iconFrom("navidrome-all.svg"),
blank: iconFrom("blank.svg"),
@@ -298,6 +278,13 @@ export const ICONS: Record<ICON, Icon> = {
celtic: iconFrom("Scottish-Thistle-108212.svg"),
children: iconFrom("Children-78186.svg"),
chillout: iconFrom("Sleeping-in Bed-14385.svg"),
christmas: iconFrom("Christmas-Tree-66793.svg"),
halloween: iconFrom("Jack-o' Lantern-66580.svg"),
yoDragon: iconFrom("Year-of Dragon-4537.svg"),
yoRabbit: iconFrom("Year-of Rabbit-6313.svg"),
yoTiger: iconFrom("Year-of Tiger-22776.svg"),
chapel: iconFrom("Chapel-69791.svg"),
audioWave: iconFrom("Audio-Wave-1892.svg"),
};
export type RULE = (genre: string) => boolean;
@@ -332,6 +319,8 @@ const GENRE_RULES: [RULE, ICON][] = [
[eq("Turntablism"), "vinyl"],
[eq("Celtic"), "celtic"],
[eq("Progressive Rock"), "progressiveRock"],
[containsWord("Christmas"), "christmas"],
[containsWord("Kerst"), "christmas"], // christmas in dutch
[containsWord("Country"), "country"],
[containsWord("Rock"), "rock"],
[containsWord("Folk"), "guitar"],
@@ -347,6 +336,7 @@ const GENRE_RULES: [RULE, ICON][] = [
[eq("Classic"), "classical"],
[containsWord("Classical"), "classical"],
[containsWord("Comedy"), "comedy"],
[containsWord("Komedie"), "comedy"], // dutch for Comedy
[containsWord("Turntable"), "vinyl"],
[containsWord("Dub"), "electronic"],
[eq("Dubstep"), "electronic"],
@@ -374,6 +364,9 @@ const GENRE_RULES: [RULE, ICON][] = [
[contains("Children"), "children"],
[contains("Chill"), "chill"],
[contains("Old"), "old"],
[containsWord("Christian"), "chapel"],
[containsWord("Religious"), "chapel"],
[containsWord("Spoken"), "audioWave"],
];
export function iconForGenre(genre: string): ICON {
@@ -383,3 +376,68 @@ export function iconForGenre(genre: string): ICON {
];
return name! as ICON;
}
export const festivals = (clock: Clock = SystemClock): Transformer => {
const randomHoliColors = _.shuffle([...HOLI_COLORS]);
return allOf(
maybeTransform(
() => isChristmas(clock),
transform({
svg: ICONS.christmas.svg,
features: {
backgroundColor: "green",
foregroundColor: "red",
},
})
),
maybeTransform(
() => isHoli(clock),
transform({
features: {
backgroundColor: randomHoliColors.pop(),
foregroundColor: randomHoliColors.pop(),
},
})
),
maybeTransform(
() => isCNY_2022(clock),
transform({
svg: ICONS.yoTiger.svg,
features: {
backgroundColor: "red",
foregroundColor: "yellow",
},
})
),
maybeTransform(
() => isCNY_2023(clock),
transform({
svg: ICONS.yoRabbit.svg,
features: {
backgroundColor: "red",
foregroundColor: "yellow",
},
})
),
maybeTransform(
() => isCNY_2024(clock),
transform({
svg: ICONS.yoDragon.svg,
features: {
backgroundColor: "red",
foregroundColor: "yellow",
},
})
),
maybeTransform(
() => isHalloween(clock),
transform({
svg: ICONS.halloween.svg,
features: {
backgroundColor: "black",
foregroundColor: "orange",
},
})
)
);
};

View File

@@ -25,7 +25,7 @@ import { Clock, SystemClock } from "./clock";
import { pipe } from "fp-ts/lib/function";
import { URLBuilder } from "./url_builder";
import makeI8N, { asLANGs, KEY, keys as i8nKeys, LANG } from "./i8n";
import { Icon, makeFestive, ICONS } from "./icon";
import { Icon, ICONS, festivals, features } from "./icon";
import _, { shuffle } from "underscore";
import morgan from "morgan";
import { takeWithRepeats } from "./utils";
@@ -79,7 +79,7 @@ export type ServerOpts = {
};
applyContextPath: boolean;
logRequests: boolean;
version: string
version: string;
};
const DEFAULT_SERVER_OPTS: ServerOpts = {
@@ -89,7 +89,7 @@ const DEFAULT_SERVER_OPTS: ServerOpts = {
iconColors: { foregroundColor: undefined, backgroundColor: undefined },
applyContextPath: true,
logRequests: false,
version: "v?"
version: "v?",
};
function server(
@@ -148,7 +148,7 @@ function server(
removeRegistrationRoute: bonobUrl
.append({ pathname: REMOVE_REGISTRATION_ROUTE })
.pathname(),
version: opts.version
version: opts.version,
});
}
);
@@ -413,10 +413,15 @@ function server(
};
return Promise.resolve(
makeFestive(
icon.with({ viewPortIncreasePercent: 80, ...serverOpts.iconColors }),
clock
).toString()
icon
.apply(
features({
viewPortIncreasePercent: 80,
...serverOpts.iconColors,
})
)
.apply(festivals(clock))
.toString()
)
.then(spec.responseFormatter)
.then((data) => res.status(200).type(spec.mimeType).send(data));
@@ -428,7 +433,12 @@ function server(
icons: Object.keys(ICONS).map((k) => [
k,
((ICONS as any)[k] as Icon)
.with({ viewPortIncreasePercent: 80, ...serverOpts.iconColors })
.apply(
features({
viewPortIncreasePercent: 80,
...serverOpts.iconColors,
})
)
.toString()
.replace('<?xml version="1.0" encoding="UTF-8"?>', ""),
]),