Icon resizing of viewPort dynamically, ability to specify custom fore and background colors via env vars (#32)

This commit is contained in:
Simon J
2021-08-26 15:18:15 +10:00
committed by GitHub
parent b900863c78
commit d1f00f549c
26 changed files with 930 additions and 200 deletions

View File

@@ -141,6 +141,8 @@ BONOB_NAVIDROME_URL | http://$(hostname):4533 | URL for navidrome
BONOB_NAVIDROME_CUSTOM_CLIENTS | undefined | Comma delimeted mime types for custom navidrome clients when streaming. ie. "audio/flac,audio/ogg" would use client = 'bonob+audio/flac' for flacs, and 'bonob+audio/ogg' for oggs.
BONOB_SCROBBLE_TRACKS | true | Whether to scrobble the playing of a track if it has been played for >30s
BONOB_REPORT_NOW_PLAYING | true | Whether to report a track as now playing
BONOB_ICON_FOREGROUND_COLOR | undefined | Icon foreground color in sonos app, must be a valid [web color](https://www.december.com/html/spec/colorsvg.html)
BONOB_ICON_BACKGROUND_COLOR | undefined | Icon background color in sonos app, must be a valid [web color](https://www.december.com/html/spec/colorsvg.html)
## Initialising service within sonos app

5
nodemon.json Normal file
View File

@@ -0,0 +1,5 @@
{
"watch": ["src/**/*.ts", "web/**/*.svg"],
"ignore": [],
"exec": "ts-node ./src/app.ts"
}

View File

@@ -18,9 +18,9 @@
"eta": "^1.12.1",
"express": "^4.17.1",
"fp-ts": "^2.9.5",
"libxmljs2": "^0.27.0",
"morgan": "^1.10.0",
"node-html-parser": "^2.1.0",
"scale-that-svg": "^1.0.5",
"sharp": "^0.27.2",
"soap": "^0.37.0",
"ts-md5": "^1.2.7",
@@ -50,8 +50,8 @@
"scripts": {
"clean": "rm -Rf build",
"build": "tsc",
"dev": "BONOB_SONOS_SERVICE_NAME=bonobDev BONOB_SONOS_DEVICE_DISCOVERY=true nodemon ./src/app.ts",
"devr": "BONOB_SONOS_SERVICE_NAME=bonobDev BONOB_SONOS_DEVICE_DISCOVERY=true BONOB_SONOS_AUTO_REGISTER=true nodemon ./src/app.ts",
"dev": "BONOB_SONOS_SERVICE_NAME=bonobDev BONOB_SONOS_DEVICE_DISCOVERY=true nodemon",
"devr": "BONOB_SONOS_SERVICE_NAME=bonobDev BONOB_SONOS_DEVICE_DISCOVERY=true BONOB_SONOS_AUTO_REGISTER=true nodemon",
"register-dev": "ts-node ./src/register.ts http://$(hostname):4534",
"test": "jest --testPathIgnorePatterns=build"
}

View File

@@ -64,6 +64,7 @@ const app = server(
new InMemoryLinkCodes(),
new InMemoryAccessTokens(sha256(config.secret)),
SystemClock,
config.icons,
true,
);

View File

@@ -1,5 +1,10 @@
import dayjs, { Dayjs } from "dayjs";
export const isChristmas = (clock: Clock = SystemClock) => clock.now().month() == 11 && clock.now().date() == 25;
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 interface Clock {
now(): Dayjs;
}

View File

@@ -16,10 +16,24 @@ export default function () {
process.exit(1);
}
const colorFrom = (envVar: string) => {
const value = process.env[envVar];
if (value && value != "") {
if (value.match(/^\w+$/)) return value;
else throw `Invalid color specified for ${envVar}`;
} else {
return undefined;
}
};
return {
port,
bonobUrl: url(bonobUrl),
secret: process.env["BONOB_SECRET"] || "bonob",
icons: {
foregroundColor: colorFrom("BONOB_ICON_FOREGROUND_COLOR"),
backgroundColor: colorFrom("BONOB_ICON_BACKGROUND_COLOR"),
},
sonos: {
serviceName: process.env["BONOB_SONOS_SERVICE_NAME"] || "bonob",
deviceDiscovery:

154
src/icon.ts Normal file
View File

@@ -0,0 +1,154 @@
import libxmljs, { Element, Attribute } from "libxmljs2";
import _ from "underscore";
import { Clock, isChristmas, isCNY, isHalloween, isHoli, SystemClock } from "./clock";
export type Transformation = {
viewPortIncreasePercent: number | undefined;
backgroundColor: string | undefined;
foregroundColor: string | undefined;
};
const SVG_NS = {
svg: "http://www.w3.org/2000/svg",
};
class ViewBox {
minX: number;
minY: number;
width: number;
height: number;
constructor(viewBox: string) {
const parts = viewBox.split(" ").map((it) => Number.parseInt(it));
this.minX = parts[0]!;
this.minY = parts[1]!;
this.width = parts[2]!;
this.height = parts[3]!;
}
public increasePercent = (percent: number) => {
const i = Math.floor(((percent / 100) * this.height) / 3);
return new ViewBox(
`${-i} ${-i} ${this.height + 2 * i} ${this.height + 2 * i}`
);
};
public toString = () =>
`${this.minX} ${this.minY} ${this.width} ${this.height}`;
}
export interface Icon {
with(newTransformation: Partial<Transformation>): Icon;
}
export class ColorOverridingIcon implements Icon {
rule: () => Boolean;
newColors: () => Pick<Transformation, "backgroundColor" | "foregroundColor">;
icon: Icon;
constructor(
icon: Icon,
rule: () => Boolean,
newColors: () => Pick<Transformation, "backgroundColor" | "foregroundColor">
) {
this.icon = icon;
this.rule = rule;
this.newColors = newColors;
}
public with = (transformation: Partial<Transformation>) =>
this.rule()
? this.icon.with({ ...transformation, ...this.newColors() })
: this.icon.with(transformation);
public toString = () => this.with({}).toString();
}
export class SvgIcon implements Icon {
private svg: string;
private transformation: Transformation;
constructor(
svg: string,
transformation: Transformation = {
viewPortIncreasePercent: undefined,
backgroundColor: undefined,
foregroundColor: undefined,
}
) {
this.svg = svg;
this.transformation = transformation;
}
public with = (newTransformation: Partial<Transformation>) =>
new SvgIcon(this.svg, { ...this.transformation, ...newTransformation });
public toString = () => {
const xml = libxmljs.parseXmlString(this.svg, {
noblanks: true,
net: false,
});
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
) {
viewBox = viewBox.increasePercent(
this.transformation.viewPortIncreasePercent
);
viewBoxAttr.value(viewBox.toString());
}
if (this.transformation.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}`,
style: `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}` })
);
}
return xml.toString();
};
}
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">
) =>
new ColorOverridingIcon(
icon,
() => rule(clock),
() => colors
);
const xmas = wrap(icon, isChristmas, {
backgroundColor: "green",
foregroundColor: "red",
});
const randomHoliColors = _.shuffle([...HOLI_COLORS]);
const holi = wrap(xmas, isHoli, {
backgroundColor: randomHoliColors.pop(),
foregroundColor: randomHoliColors.pop(),
});
const cny = wrap(holi, isCNY, {
backgroundColor: "red",
foregroundColor: "yellow",
});
const halloween = wrap(cny, isHalloween, {
backgroundColor: "orange",
foregroundColor: "black",
});
return halloween;
};

View File

@@ -3,7 +3,6 @@ import express, { Express, Request } from "express";
import * as Eta from "eta";
import morgan from "morgan";
import path from "path";
import scale from "scale-that-svg";
import sharp from "sharp";
import fs from "fs";
@@ -29,29 +28,10 @@ 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 { SvgIcon, Icon, makeFestive } from "./icon";
export const BONOB_ACCESS_TOKEN_HEADER = "bonob-access-token";
const icon = (name: string) =>
fs
.readFileSync(path.resolve(__dirname, "..", "web", "icons", name))
.toString();
export type Icon = { svg: string; size: number };
export const ICONS: Record<ICON, Icon> = {
artists: { svg: icon("navidrome-artists.svg"), size: 24 },
albums: { svg: icon("navidrome-all.svg"), size: 24 },
playlists: { svg: icon("navidrome-playlists.svg"), size: 24 },
genres: { svg: icon("Theatre-Mask-111172.svg"), size: 128 },
random: { svg: icon("navidrome-random.svg"), size: 24 },
starred: { svg: icon("navidrome-topRated.svg"), size: 24 },
recentlyAdded: { svg: icon("navidrome-recentlyAdded.svg"), size: 24 },
recentlyPlayed: { svg: icon("navidrome-recentlyPlayed.svg"), size: 24 },
mostPlayed: { svg: icon("navidrome-mostPlayed.svg"), size: 24 },
discover: { svg: icon("Binoculars-14310.svg"), size: 32 },
};
interface RangeFilter extends Transform {
range: (length: number) => string;
}
@@ -97,6 +77,10 @@ function server(
linkCodes: LinkCodes = new InMemoryLinkCodes(),
accessTokens: AccessTokens = new AccessTokenPerAuthToken(),
clock: Clock = SystemClock,
iconColors: {
foregroundColor: string | undefined;
backgroundColor: string | undefined;
} = { foregroundColor: undefined, backgroundColor: undefined },
applyContextPath = true
): Express {
const app = express();
@@ -119,6 +103,27 @@ function server(
return i8n(...asLANGs(req.headers["accept-language"]));
};
const iconFrom = (name: string) =>
makeFestive(
new SvgIcon(
fs.readFileSync(path.resolve(__dirname, "..", "web", "icons", name)).toString()
).with({ viewPortIncreasePercent: 50, ...iconColors }),
clock
);
const ICONS: Record<ICON, Icon> = {
artists: iconFrom("navidrome-artists.svg"),
albums: iconFrom("navidrome-all.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"),
};
app.get("/", (req, res) => {
const lang = langFor(req);
Promise.all([sonos.devices(), sonos.services()]).then(
@@ -391,20 +396,17 @@ function server(
const spec =
size == "legacy"
? {
outputSize: 80,
mimeType: "image/png",
responseFormatter: (svg: string): Promise<Buffer | string> =>
sharp(Buffer.from(svg)).png().toBuffer(),
sharp(Buffer.from(svg)).resize(80).png().toBuffer(),
}
: {
outputSize: Number.parseInt(size),
mimeType: "image/svg+xml",
responseFormatter: (svg: string): Promise<Buffer | string> =>
Promise.resolve(svg),
};
return Promise.resolve(icon.svg)
.then((svg) => scale(svg, { scale: spec.outputSize / icon.size }))
return Promise.resolve(icon.toString())
.then(spec.responseFormatter)
.then((data) => res.status(200).type(spec.mimeType).send(data));
}
@@ -437,9 +439,9 @@ function server(
}
})
.catch((e: Error) => {
logger.error(
`Failed fetching image ${type}/${id}/size/${size}`, { cause: e }
);
logger.error(`Failed fetching image ${type}/${id}/size/${size}`, {
cause: e,
});
return res.status(500).send();
});
}

58
tests/clock.test.ts Normal file
View File

@@ -0,0 +1,58 @@
import dayjs from "dayjs";
import { isChristmas, isCNY, isHalloween, isHoli } from "../src/clock";
describe("isChristmas", () => {
["2000/12/25", "2022/12/25", "2030/12/25"].forEach((date) => {
it(`should return true for ${date} regardless of year`, () => {
expect(isChristmas({ now: () => dayjs(date) })).toEqual(true);
});
});
["2000/12/24", "2000/12/26", "2021/01/01"].forEach((date) => {
it(`should return false for ${date} regardless of year`, () => {
expect(isChristmas({ now: () => dayjs(date) })).toEqual(false);
});
});
});
describe("isHalloween", () => {
["2000/10/31", "2022/10/31", "2030/10/31"].forEach((date) => {
it(`should return true for ${date} regardless of year`, () => {
expect(isHalloween({ now: () => dayjs(date) })).toEqual(true);
});
});
["2000/09/31", "2000/10/30", "2021/01/01"].forEach((date) => {
it(`should return false for ${date} regardless of year`, () => {
expect(isHalloween({ now: () => dayjs(date) })).toEqual(false);
});
});
});
describe("isHoli", () => {
["2022/03/18", "2023/03/07", "2024/03/25", "2025/03/14"].forEach((date) => {
it(`should return true for ${date} regardless of year`, () => {
expect(isHoli({ now: () => dayjs(date) })).toEqual(true);
});
});
["2000/09/31", "2000/10/30", "2021/01/01"].forEach((date) => {
it(`should return false for ${date} regardless of year`, () => {
expect(isHoli({ now: () => dayjs(date) })).toEqual(false);
});
});
});
describe("isCNY", () => {
["2022/02/01", "2023/01/22", "2024/02/10", "2025/02/29"].forEach((date) => {
it(`should return true for ${date} regardless of year`, () => {
expect(isCNY({ now: () => dayjs(date) })).toEqual(true);
});
});
["2000/09/31", "2000/10/30", "2021/01/01"].forEach((date) => {
it(`should return false for ${date} regardless of year`, () => {
expect(isCNY({ now: () => dayjs(date) })).toEqual(false);
});
});
});

View File

@@ -107,6 +107,66 @@ describe("config", () => {
});
});
describe("icons", () => {
describe("foregroundColor", () => {
describe("when BONOB_ICON_FOREGROUND_COLOR is not specified", () => {
it(`should default to undefined`, () => {
expect(config().icons.foregroundColor).toEqual(undefined);
});
});
describe("when BONOB_ICON_FOREGROUND_COLOR is ''", () => {
it(`should default to undefined`, () => {
process.env["BONOB_ICON_FOREGROUND_COLOR"] = "";
expect(config().icons.foregroundColor).toEqual(undefined);
});
});
describe("when BONOB_ICON_FOREGROUND_COLOR is specified", () => {
it(`should use it`, () => {
process.env["BONOB_ICON_FOREGROUND_COLOR"] = "pink";
expect(config().icons.foregroundColor).toEqual("pink");
});
});
describe("when BONOB_ICON_FOREGROUND_COLOR is an invalid string", () => {
it(`should blow up`, () => {
process.env["BONOB_ICON_FOREGROUND_COLOR"] = "#dfasd";
expect(() => config()).toThrow("Invalid color specified for BONOB_ICON_FOREGROUND_COLOR")
});
});
});
describe("backgroundColor", () => {
describe("when BONOB_ICON_BACKGROUND_COLOR is not specified", () => {
it(`should default to undefined`, () => {
expect(config().icons.backgroundColor).toEqual(undefined);
});
});
describe("when BONOB_ICON_BACKGROUND_COLOR is ''", () => {
it(`should default to undefined`, () => {
process.env["BONOB_ICON_BACKGROUND_COLOR"] = "";
expect(config().icons.backgroundColor).toEqual(undefined);
});
});
describe("when BONOB_ICON_BACKGROUND_COLOR is specified", () => {
it(`should use it`, () => {
process.env["BONOB_ICON_BACKGROUND_COLOR"] = "blue";
expect(config().icons.backgroundColor).toEqual("blue");
});
});
describe("when BONOB_ICON_BACKGROUND_COLOR is an invalid string", () => {
it(`should blow up`, () => {
process.env["BONOB_ICON_BACKGROUND_COLOR"] = "#red";
expect(() => config()).toThrow("Invalid color specified for BONOB_ICON_BACKGROUND_COLOR")
});
});
});
});
describe("secret", () => {
it("should default to bonob", () => {
expect(config().secret).toEqual("bonob");

429
tests/icon.test.ts Normal file
View File

@@ -0,0 +1,429 @@
import dayjs from "dayjs";
import libxmljs from "libxmljs2";
import {
ColorOverridingIcon,
HOLI_COLORS,
Icon,
makeFestive,
SvgIcon,
Transformation,
} from "../src/icon";
describe("SvgIcon", () => {
const xmlTidy = (xml: string) =>
libxmljs.parseXmlString(xml, { noblanks: true, net: false }).toString();
const svgIcon24 = `<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="something1"/>
<path d="something2"/>
<path d="something3"/>
</svg>
`;
const svgIcon128 = `<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128">
<path d="something1"/>
<path d="something2"/>
<path d="something3"/>
</svg>
`;
describe("with no transformation", () => {
it("should be the same", () => {
expect(new SvgIcon(svgIcon24).toString()).toEqual(xmlTidy(svgIcon24));
});
});
describe("with a view port increase", () => {
describe("of 50%", () => {
describe("when the viewPort is of size 0 0 24 24", () => {
it("should resize the viewPort", () => {
expect(
new SvgIcon(svgIcon24)
.with({ viewPortIncreasePercent: 50 })
.toString()
).toEqual(
xmlTidy(`<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-4 -4 32 32">
<path d="something1"/>
<path d="something2"/>
<path d="something3"/>
</svg>
`)
);
});
});
describe("when the viewPort is of size 0 0 128 128", () => {
it("should resize the viewPort", () => {
expect(
new SvgIcon(svgIcon128)
.with({ viewPortIncreasePercent: 50 })
.toString()
).toEqual(
xmlTidy(`<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-21 -21 170 170">
<path d="something1"/>
<path d="something2"/>
<path d="something3"/>
</svg>
`)
);
});
});
});
describe("of 0%", () => {
it("should do nothing", () => {
expect(
new SvgIcon(svgIcon24).with({ viewPortIncreasePercent: 0 }).toString()
).toEqual(xmlTidy(svgIcon24));
});
});
});
describe("background color", () => {
describe("with no viewPort increase", () => {
it("should add a rectangle the same size as the original viewPort", () => {
expect(
new SvgIcon(svgIcon24).with({ backgroundColor: "red" }).toString()
).toEqual(
xmlTidy(`<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<rect x="0" y="0" width="24" height="24" style="fill:red"/>
<path d="something1"/>
<path d="something2"/>
<path d="something3"/>
</svg>
`)
);
});
});
describe("with a viewPort increase", () => {
it("should add a rectangle the same size as the original viewPort", () => {
expect(
new SvgIcon(svgIcon24)
.with({ backgroundColor: "pink", viewPortIncreasePercent: 50 })
.toString()
).toEqual(
xmlTidy(`<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-4 -4 32 32">
<rect x="-4" y="-4" width="36" height="36" style="fill:pink"/>
<path d="something1"/>
<path d="something2"/>
<path d="something3"/>
</svg>
`)
);
});
});
describe("of undefined", () => {
it("should not do anything", () => {
expect(
new SvgIcon(svgIcon24)
.with({ backgroundColor: undefined })
.toString()
).toEqual(
xmlTidy(`<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="something1"/>
<path d="something2"/>
<path d="something3"/>
</svg>
`)
);
});
});
describe("multiple times", () => {
it("should use the most recent", () => {
expect(
new SvgIcon(svgIcon24)
.with({ backgroundColor: "green" })
.with({ backgroundColor: "red" })
.toString()
).toEqual(
xmlTidy(`<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<rect x="0" y="0" width="24" height="24" style="fill:red"/>
<path d="something1"/>
<path d="something2"/>
<path d="something3"/>
</svg>
`)
);
});
});
});
describe("foreground color", () => {
describe("with no viewPort increase", () => {
it("should add a rectangle the same size as the original viewPort", () => {
expect(
new SvgIcon(svgIcon24).with({ foregroundColor: "red" }).toString()
).toEqual(
xmlTidy(`<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="something1" style="fill:red"/>
<path d="something2" style="fill:red"/>
<path d="something3" style="fill:red"/>
</svg>
`)
);
});
});
describe("with a viewPort increase", () => {
it("should add a rectangle the same size as the original viewPort", () => {
expect(
new SvgIcon(svgIcon24)
.with({ foregroundColor: "pink", viewPortIncreasePercent: 50 })
.toString()
).toEqual(
xmlTidy(`<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-4 -4 32 32">
<path d="something1" style="fill:pink"/>
<path d="something2" style="fill:pink"/>
<path d="something3" style="fill:pink"/>
</svg>
`)
);
});
});
describe("of undefined", () => {
it("should not do anything", () => {
expect(
new SvgIcon(svgIcon24)
.with({ foregroundColor: undefined })
.toString()
).toEqual(
xmlTidy(`<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="something1"/>
<path d="something2"/>
<path d="something3"/>
</svg>
`)
);
});
});
describe("mutliple times", () => {
it("should use the most recent", () => {
expect(
new SvgIcon(svgIcon24)
.with({ foregroundColor: "blue" })
.with({ foregroundColor: "red" })
.toString()
).toEqual(
xmlTidy(`<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="something1" style="fill:red"/>
<path d="something2" style="fill:red"/>
<path d="something3" style="fill:red"/>
</svg>
`)
);
});
});
});
});
class DummyIcon implements Icon {
transformation: Partial<Transformation>;
constructor(transformation: Partial<Transformation>) {
this.transformation = transformation;
}
public with = (newTransformation: Partial<Transformation>) =>
new DummyIcon({ ...this.transformation, ...newTransformation });
public toString = () => JSON.stringify(this);
}
describe("ColorOverridingIcon", () => {
describe("when the rule matches", () => {
const icon = new DummyIcon({
backgroundColor: "black",
foregroundColor: "black",
});
const overriding = new ColorOverridingIcon(
icon,
() => true,
() => ({ backgroundColor: "blue", foregroundColor: "red" })
);
describe("with", () => {
it("should be the with of the underlieing icon with the overriden colors", () => {
const result = overriding.with({
viewPortIncreasePercent: 99,
backgroundColor: "shouldBeIgnored",
foregroundColor: "shouldBeIgnored",
}) as DummyIcon;
expect(result.transformation).toEqual({
viewPortIncreasePercent: 99,
backgroundColor: "blue",
foregroundColor: "red",
});
});
});
describe("toString", () => {
it("should be the toString of the underlieing icon with the overriden colors", () => {
expect(overriding.toString()).toEqual(
new DummyIcon({
backgroundColor: "blue",
foregroundColor: "red",
}).toString()
);
});
});
});
describe("when the rule doesnt match", () => {
const icon = new DummyIcon({
backgroundColor: "black",
foregroundColor: "black",
});
const overriding = new ColorOverridingIcon(
icon,
() => false,
() => ({ backgroundColor: "blue", foregroundColor: "red" })
);
describe("with", () => {
it("should use the provided transformation", () => {
const result = overriding.with({
viewPortIncreasePercent: 88,
backgroundColor: "shouldBeUsed",
foregroundColor: "shouldBeUsed",
}) as DummyIcon;
expect(result.transformation).toEqual({
viewPortIncreasePercent: 88,
backgroundColor: "shouldBeUsed",
foregroundColor: "shouldBeUsed",
});
});
});
describe("toString", () => {
it("should be the toString of the unchanged icon", () => {
expect(overriding.toString()).toEqual(icon.toString());
});
});
});
});
describe("makeFestive", () => {
const icon = new DummyIcon({
backgroundColor: "black",
foregroundColor: "black",
});
let now = dayjs();
const festiveIcon = makeFestive(icon, { now: () => now })
describe("on a non special day", () => {
beforeEach(() => {
now = dayjs("2022/10/12");
});
it("should use the given colors", () => {
const result = festiveIcon.with({
viewPortIncreasePercent: 88,
backgroundColor: "shouldBeUsed",
foregroundColor: "shouldBeUsed",
}) as DummyIcon;
expect(result.transformation).toEqual({
viewPortIncreasePercent: 88,
backgroundColor: "shouldBeUsed",
foregroundColor: "shouldBeUsed",
});
});
});
describe("on christmas day", () => {
beforeEach(() => {
now = dayjs("2022/12/25");
});
it("should use the given colors", () => {
const result = festiveIcon.with({
viewPortIncreasePercent: 25,
backgroundColor: "shouldNotBeUsed",
foregroundColor: "shouldNotBeUsed",
}) as DummyIcon;
expect(result.transformation).toEqual({
viewPortIncreasePercent: 25,
backgroundColor: "green",
foregroundColor: "red",
});
});
});
describe("on halloween", () => {
beforeEach(() => {
now = dayjs("2022/10/31");
});
it("should use the given colors", () => {
const result = festiveIcon.with({
viewPortIncreasePercent: 12,
backgroundColor: "shouldNotBeUsed",
foregroundColor: "shouldNotBeUsed",
}) as DummyIcon;
expect(result.transformation).toEqual({
viewPortIncreasePercent: 12,
backgroundColor: "orange",
foregroundColor: "black",
});
});
});
describe("on cny", () => {
beforeEach(() => {
now = dayjs("2022/02/01");
});
it("should use the given colors", () => {
const result = festiveIcon.with({
viewPortIncreasePercent: 12,
backgroundColor: "shouldNotBeUsed",
foregroundColor: "shouldNotBeUsed",
}) as DummyIcon;
expect(result.transformation).toEqual({
viewPortIncreasePercent: 12,
backgroundColor: "red",
foregroundColor: "yellow",
});
});
});
describe("on holi", () => {
beforeEach(() => {
now = dayjs("2022/03/18");
});
it("should use the given colors", () => {
const result = festiveIcon.with({
viewPortIncreasePercent: 12,
backgroundColor: "shouldNotBeUsed",
foregroundColor: "shouldNotBeUsed",
}) as DummyIcon;
expect(result.transformation.viewPortIncreasePercent).toEqual(12);
expect(HOLI_COLORS.includes(result.transformation.backgroundColor!)).toEqual(true);
expect(HOLI_COLORS.includes(result.transformation.foregroundColor!)).toEqual(true);
expect(result.transformation.backgroundColor).not.toEqual(result.transformation.foregroundColor);
});
});
});

View File

@@ -6,7 +6,6 @@ import Image from "image-js";
import { MusicService } from "../src/music_service";
import makeServer, {
BONOB_ACCESS_TOKEN_HEADER,
ICONS,
RangeBytesFromFilter,
rangeFilterFor,
} from "../src/server";
@@ -21,9 +20,8 @@ import { Response } from "express";
import { Transform } from "stream";
import url from "../src/url_builder";
import i8n, { randomLang } from "../src/i8n";
import {
SONOS_RECOMMENDED_IMAGE_SIZES,
} from "../src/smapi";
import { SONOS_RECOMMENDED_IMAGE_SIZES } from "../src/smapi";
import { Clock, SystemClock } from "../src/clock";
describe("rangeFilterFor", () => {
describe("invalid range header string", () => {
@@ -1264,14 +1262,23 @@ describe("server", () => {
});
describe("/icon", () => {
const server = makeServer(
jest.fn() as unknown as Sonos,
aService(),
url("http://localhost:1234"),
jest.fn() as unknown as MusicService,
new InMemoryLinkCodes(),
jest.fn() as unknown as AccessTokens
);
const server = (
clock: Clock = SystemClock,
iconColors: {
foregroundColor: string | undefined;
backgroundColor: string | undefined;
} = { foregroundColor: undefined, backgroundColor: undefined }
) =>
makeServer(
jest.fn() as unknown as Sonos,
aService(),
url("http://localhost:1234"),
jest.fn() as unknown as MusicService,
new InMemoryLinkCodes(),
jest.fn() as unknown as AccessTokens,
clock,
iconColors
);
describe("invalid icon names", () => {
[
@@ -1286,7 +1293,7 @@ describe("server", () => {
].forEach((type) => {
describe(`trying to retrieve an icon with name ${type}`, () => {
it(`should fail`, async () => {
const response = await request(server).get(
const response = await request(server()).get(
`/icon/${type}/size/legacy`
);
@@ -1300,7 +1307,7 @@ describe("server", () => {
["-1", "0", "59", "foo"].forEach((size) => {
describe(`trying to retrieve an icon with size ${size}`, () => {
it(`should fail`, async () => {
const response = await request(server).get(
const response = await request(server()).get(
`/icon/artists/size/${size}`
);
@@ -1311,11 +1318,22 @@ describe("server", () => {
});
describe("fetching", () => {
Object.keys(ICONS).forEach((type) => {
[
"artists",
"albums",
"playlists",
"genres",
"random",
"starred",
"recentlyAdded",
"recentlyPlayed",
"mostPlayed",
"discover",
].forEach((type) => {
describe(`type=${type}`, () => {
describe(`legacy icon`, () => {
it("should return the png image", async () => {
const response = await request(server).get(
const response = await request(server()).get(
`/icon/${type}/size/legacy`
);
@@ -1330,7 +1348,7 @@ describe("server", () => {
describe("svg icon", () => {
SONOS_RECOMMENDED_IMAGE_SIZES.forEach((size) => {
it(`should return an svg image for size = ${size}`, async () => {
const response = await request(server).get(
const response = await request(server()).get(
`/icon/${type}/size/${size}`
);
@@ -1339,12 +1357,33 @@ describe("server", () => {
"image/svg+xml; charset=utf-8"
);
const svg = Buffer.from(response.body).toString();
expect(svg).toContain(`viewBox="0 0 ${size} ${size}"`);
expect(svg).toContain(
` xmlns="http://www.w3.org/2000/svg" `
);
});
});
it("should return icon colors as per config if overriden", async () => {
const response = await request(server(SystemClock, { foregroundColor: 'brightblue', backgroundColor: 'brightpink' })).get(
`/icon/${type}/size/180`
);
expect(response.status).toEqual(200);
const svg = Buffer.from(response.body).toString();
expect(svg).toContain(`fill:brightblue`);
expect(svg).toContain(`fill:brightpink`);
});
it("should return a christmas icon on christmas day", async () => {
const response = await request(server({ now: () => dayjs("2022/12/25") })).get(
`/icon/${type}/size/180`
);
expect(response.status).toEqual(200);
const svg = Buffer.from(response.body).toString();
expect(svg).toContain(`fill:red`);
expect(svg).toContain(`fill:green`);
});
});
});
});

View File

@@ -1 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><path fill="none" stroke="#000" stroke-miterlimit="10" stroke-width="2" d="M7 15A6 6 0 1 0 7 27 6 6 0 1 0 7 15zM25 15A6 6 0 1 0 25 27 6 6 0 1 0 25 15zM16 18A2 2 0 1 0 16 22 2 2 0 1 0 16 18z"/><path fill="none" stroke="#000" stroke-miterlimit="10" stroke-width="2" d="M30.5,18.6l-2.3-6.4C27.5,10.5,25.9,9.2,24,9v0c0-1.7-1.3-3-3-3s-3,1.3-3,3h-4c0-1.7-1.3-3-3-3S8,7.3,8,9v0c-1.9,0.2-3.5,1.4-4.2,3.3l-2.3,6.4"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<path stroke-miterlimit="10" stroke-width="2" d="M7 15A6 6 0 1 0 7 27 6 6 0 1 0 7 15zM25 15A6 6 0 1 0 25 27 6 6 0 1 0 25 15zM16 18A2 2 0 1 0 16 22 2 2 0 1 0 16 18z"/>
<path stroke-miterlimit="10" stroke-width="2" d="M30.5,18.6l-2.3-6.4C27.5,10.5,25.9,9.2,24,9v0c0-1.7-1.3-3-3-3s-3,1.3-3,3h-4c0-1.7-1.3-3-3-3S8,7.3,8,9v0c-1.9,0.2-3.5,1.4-4.2,3.3l-2.3,6.4"/>
</svg>

Before

Width:  |  Height:  |  Size: 473 B

After

Width:  |  Height:  |  Size: 428 B

View File

@@ -1 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 128 128"><path d="M64.9,98c-0.2,0.2-0.4,0.3-0.7,0.5l-0.1-0.1c-3.7-3-7-6.4-9.9-10l-12.6,4.6c-2.3-6.3-0.1-13.1,4.9-17l0,0c-3.8-8.2-5.9-17.3-5.9-26.7v-16c0.3-0.1,0.5-0.2,0.8-0.2c1.6-0.4,2.5-2.1,2.1-3.7c-0.4-1.6-2.1-2.5-3.7-2.1C26.2,30.9,14.2,38.6,5.1,49.4C4.2,50.5,4,52,4.5,53.3l6.9,19c5.8,16,18.4,28.4,34.6,33.9c2.6,0.9,5.3,1.3,8,1.3c5.1,0,10.2-1.6,14.5-4.7c1.3-1,1.6-2.8,0.7-4.2C68.1,97.4,66.2,97,64.9,98z M34.5,61.5L26,64.6c-1.6,0.6-3.3-0.2-3.8-1.8c-0.6-1.6,0.2-3.3,1.8-3.8l8.5-3.1c1.6-0.6,3.3,0.2,3.8,1.8S36,60.9,34.5,61.5z"/><path d="M118.8,25.9l-0.5-0.3c-21.2-12.2-47.5-12.2-68.8,0c0,0,0,0,0,0c-1.2,0.7-2,2-2,3.4v20.3c0,17.1,7.6,33,20.9,43.7c4.5,3.6,10,5.4,15.5,5.4s11-1.8,15.5-5.4c13.3-10.7,20.9-26.6,20.9-43.7V28.5C120.3,27.4,119.8,26.4,118.8,25.9z M84,79v5.9c0,3.2,1.8,5.7,4.2,7c-5.5,1.3-11.4,0.1-15.9-3.6c-11.8-9.6-18.6-23.8-18.6-39v-19c9.4-5.2,19.9-7.8,30.3-7.8V64h15C99,72.3,92.3,79,84,79z M103.1,47h-9c-1.7,0-3-1.3-3-3s1.3-3,3-3h9c1.7,0,3,1.3,3,3S104.7,47,103.1,47z"/><path d="M84 79l0-15H69C69 72.3 75.7 79 84 79zM76.1 44c0-1.7-1.3-3-3-3h-9c-1.7 0-3 1.3-3 3s1.3 3 3 3h9C74.7 47 76.1 45.7 76.1 44z"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128">
<path d="M64.9,98c-0.2,0.2-0.4,0.3-0.7,0.5l-0.1-0.1c-3.7-3-7-6.4-9.9-10l-12.6,4.6c-2.3-6.3-0.1-13.1,4.9-17l0,0c-3.8-8.2-5.9-17.3-5.9-26.7v-16c0.3-0.1,0.5-0.2,0.8-0.2c1.6-0.4,2.5-2.1,2.1-3.7c-0.4-1.6-2.1-2.5-3.7-2.1C26.2,30.9,14.2,38.6,5.1,49.4C4.2,50.5,4,52,4.5,53.3l6.9,19c5.8,16,18.4,28.4,34.6,33.9c2.6,0.9,5.3,1.3,8,1.3c5.1,0,10.2-1.6,14.5-4.7c1.3-1,1.6-2.8,0.7-4.2C68.1,97.4,66.2,97,64.9,98z M34.5,61.5L26,64.6c-1.6,0.6-3.3-0.2-3.8-1.8c-0.6-1.6,0.2-3.3,1.8-3.8l8.5-3.1c1.6-0.6,3.3,0.2,3.8,1.8S36,60.9,34.5,61.5z"/>
<path d="M118.8,25.9l-0.5-0.3c-21.2-12.2-47.5-12.2-68.8,0c0,0,0,0,0,0c-1.2,0.7-2,2-2,3.4v20.3c0,17.1,7.6,33,20.9,43.7c4.5,3.6,10,5.4,15.5,5.4s11-1.8,15.5-5.4c13.3-10.7,20.9-26.6,20.9-43.7V28.5C120.3,27.4,119.8,26.4,118.8,25.9z M84,79v5.9c0,3.2,1.8,5.7,4.2,7c-5.5,1.3-11.4,0.1-15.9-3.6c-11.8-9.6-18.6-23.8-18.6-39v-19c9.4-5.2,19.9-7.8,30.3-7.8V64h15C99,72.3,92.3,79,84,79z M103.1,47h-9c-1.7,0-3-1.3-3-3s1.3-3,3-3h9c1.7,0,3,1.3,3,3S104.7,47,103.1,47z"/>
<path d="M84 79l0-15H69C69 72.3 75.7 79 84 79zM76.1 44c0-1.7-1.3-3-3-3h-9c-1.7 0-3 1.3-3 3s1.3 3 3 3h9C74.7 47 76.1 45.7 76.1 44z"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

33
web/icons/index.html Normal file
View File

@@ -0,0 +1,33 @@
<html>
<body>
original<br>
<div style="width:100px; height:100px; border: 1px; border-style: solid;">
<svg xmlns="http://www.w3.org/2000/svg" version="1.0" class="MuiSvgIcon-root" focusable="false" viewBox="0 0 24 24" aria-hidden="true" data-testid="icon">
<rect x="0" y="0" width="100%" height="100%" style="fill:lightgrey" />
<path style="fill:white" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm0-12.5c-2.49 0-4.5 2.01-4.5 4.5s2.01 4.5 4.5 4.5 4.5-2.01 4.5-4.5-2.01-4.5-4.5-4.5zm0 5.5c-.55 0-1-.45-1-1s.45-1 1-1 1 .45 1 1-.45 1-1 1z"></path>
</svg>
</div>
hack 1<br>
<div style="width:100px; height:100px; border: 1px; border-style: solid;">
<svg xmlns="http://www.w3.org/2000/svg" version="1.0" class="MuiSvgIcon-root" focusable="false" viewBox="-3 -3 30 30" aria-hidden="true" data-testid="icon">
<rect x="-3" y="-3" width="100%" height="100%" style="fill:lightgrey" />
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm0-12.5c-2.49 0-4.5 2.01-4.5 4.5s2.01 4.5 4.5 4.5 4.5-2.01 4.5-4.5-2.01-4.5-4.5-4.5zm0 5.5c-.55 0-1-.45-1-1s.45-1 1-1 1 .45 1 1-.45 1-1 1z"></path>
</svg>
</div>
hack 2<br>
<div style="width:100px; height:100px; border: 1px; border-style: solid;">
<svg xmlns="http://www.w3.org/2000/svg" version="1.0" class="MuiSvgIcon-root" focusable="false" viewBox="-2 -2 28 28" aria-hidden="true" data-testid="icon">
<rect x="-2" y="-2" width="100%" height="100%" style="fill:lightgrey" />
<path style="fill:white" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm0-12.5c-2.49 0-4.5 2.01-4.5 4.5s2.01 4.5 4.5 4.5 4.5-2.01 4.5-4.5-2.01-4.5-4.5-4.5zm0 5.5c-.55 0-1-.45-1-1s.45-1 1-1 1 .45 1 1-.45 1-1 1z"></path>
</svg>
</div>
hack 3<br>
<div style="width:100px; height:100px; border: 1px; border-style: solid;">
<svg xmlns="http://www.w3.org/2000/svg" version="1.0" class="MuiSvgIcon-root" focusable="false" viewBox="-4 -4 32 32" aria-hidden="true" data-testid="icon">
<rect x="-4" y="-4" width="100%" height="100%" style="fill:lightgrey" />
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm0-12.5c-2.49 0-4.5 2.01-4.5 4.5s2.01 4.5 4.5 4.5 4.5-2.01 4.5-4.5-2.01-4.5-4.5-4.5zm0 5.5c-.55 0-1-.45-1-1s.45-1 1-1 1 .45 1 1-.45 1-1 1z"></path>
</svg>
</div>
</body>
</html>

View File

@@ -1,3 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" version="1.0" class="MuiSvgIcon-root" focusable="false" viewBox="0 0 24 24" aria-hidden="true" data-testid="icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm0-12.5c-2.49 0-4.5 2.01-4.5 4.5s2.01 4.5 4.5 4.5 4.5-2.01 4.5-4.5-2.01-4.5-4.5-4.5zm0 5.5c-.55 0-1-.45-1-1s.45-1 1-1 1 .45 1 1-.45 1-1 1z"></path>
</svg>

Before

Width:  |  Height:  |  Size: 444 B

After

Width:  |  Height:  |  Size: 350 B

View File

@@ -1,3 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" version="1.0" class="MuiSvgIcon-root" focusable="false" viewBox="0 0 24 24" aria-hidden="true" data-testid="icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M12 14c1.66 0 3-1.34 3-3V5c0-1.66-1.34-3-3-3S9 3.34 9 5v6c0 1.66 1.34 3 3 3zm-1-9c0-.55.45-1 1-1s1 .45 1 1v6c0 .55-.45 1-1 1s-1-.45-1-1V5zm6 6c0 2.76-2.24 5-5 5s-5-2.24-5-5H5c0 3.53 2.61 6.43 6 6.92V21h2v-3.08c3.39-.49 6-3.39 6-6.92h-2z"></path>
</svg>

Before

Width:  |  Height:  |  Size: 418 B

After

Width:  |  Height:  |  Size: 325 B

View File

@@ -1,3 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" version="1.0" class="MuiSvgIcon-root" focusable="false" viewBox="0 0 24 24" aria-hidden="true" data-testid="icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M16.5 3c-1.74 0-3.41.81-4.5 2.09C10.91 3.81 9.24 3 7.5 3 4.42 3 2 5.42 2 8.5c0 3.78 3.4 6.86 8.55 11.54L12 21.35l1.45-1.32C18.6 15.36 22 12.28 22 8.5 22 5.42 19.58 3 16.5 3zm-4.4 15.55l-.1.1-.1-.1C7.14 14.24 4 11.39 4 8.5 4 6.5 5.5 5 7.5 5c1.54 0 3.04.99 3.57 2.36h1.87C13.46 5.99 14.96 5 16.5 5c2 0 3.5 1.5 3.5 3.5 0 2.89-3.14 5.74-7.9 10.05z"></path>
</svg>

Before

Width:  |  Height:  |  Size: 525 B

After

Width:  |  Height:  |  Size: 431 B

View File

@@ -1,4 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" version="1.0" class="MuiSvgIcon-root" focusable="false" viewBox="0 0 24 24" role="img">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M7 7h10v3l4-4-4-4v3H5v6h2V7zm10 10H7v-3l-4 4 4 4v-3h12v-6h-2v4z"></path>
<title>Most Played</title>
</svg>

Before

Width:  |  Height:  |  Size: 247 B

After

Width:  |  Height:  |  Size: 151 B

View File

@@ -1,3 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" version="1.0" class="MuiSvgIcon-root" focusable="false" viewBox="0 0 24 24" aria-hidden="true" data-testid="icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M22 6h-5v8.18c-.31-.11-.65-.18-1-.18-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3V8h3V6zm-7 0H3v2h12V6zm0 4H3v2h12v-2zm-4 4H3v2h8v-2zm4 3c0-.55.45-1 1-1s1 .45 1 1-.45 1-1 1-1-.45-1-1z"></path>
</svg>

Before

Width:  |  Height:  |  Size: 360 B

After

Width:  |  Height:  |  Size: 266 B

View File

@@ -1,4 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" version="1.0" class="MuiSvgIcon-root" focusable="false" viewBox="0 0 24 24" role="img">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M10.59 9.17L5.41 4 4 5.41l5.17 5.17 1.42-1.41zM14.5 4l2.04 2.04L4 18.59 5.41 20 17.96 7.46 20 9.5V4h-5.5zm.33 9.41l-1.41 1.41 3.13 3.13L14.5 20H20v-5.5l-2.04 2.04-3.13-3.13z"></path>
<title>Random</title>
</svg>

Before

Width:  |  Height:  |  Size: 352 B

After

Width:  |  Height:  |  Size: 261 B

View File

@@ -1,3 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" version="1.0" class="MuiSvgIcon-root" focusable="false" viewBox="0 0 24 24" aria-hidden="true" data-testid="icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M4 6H2v14c0 1.1.9 2 2 2h14v-2H4V6zm16-4H8c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm0 14H8V4h12v12zm-7-2h2v-3h3V9h-3V6h-2v3h-3v2h3z"></path>
</svg>

Before

Width:  |  Height:  |  Size: 340 B

After

Width:  |  Height:  |  Size: 246 B

View File

@@ -1,3 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" version="1.0" class="MuiSvgIcon-root" focusable="false" viewBox="0 0 24 24" aria-hidden="true" data-testid="icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M4 6H2v14c0 1.1.9 2 2 2h14v-2H4V6zm16-4H8c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm0 14H8V4h12v12zM12 5.5v9l6-4.5z"></path>
</svg>

Before

Width:  |  Height:  |  Size: 324 B

After

Width:  |  Height:  |  Size: 230 B

View File

@@ -1,3 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" version="1.0" class="MuiSvgIcon-root" focusable="false" viewBox="0 0 24 24" aria-hidden="true" data-testid="icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M12 3l.01 10.55c-.59-.34-1.27-.55-2-.55C7.79 13 6 14.79 6 17s1.79 4 4.01 4S14 19.21 14 17V7h4V3h-6zm-1.99 16c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2z"></path>
</svg>

Before

Width:  |  Height:  |  Size: 334 B

After

Width:  |  Height:  |  Size: 240 B

View File

@@ -1,3 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" version="1.0" class="MuiSvgIcon-root" focusable="false" viewBox="0 0 24 24" aria-hidden="true" data-testid="activeIcon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z"></path>
</svg>

Before

Width:  |  Height:  |  Size: 276 B

After

Width:  |  Height:  |  Size: 176 B

193
yarn.lock
View File

@@ -664,6 +664,25 @@ __metadata:
languageName: node
linkType: hard
"@mapbox/node-pre-gyp@npm:^1.0.1":
version: 1.0.5
resolution: "@mapbox/node-pre-gyp@npm:1.0.5"
dependencies:
detect-libc: ^1.0.3
https-proxy-agent: ^5.0.0
make-dir: ^3.1.0
node-fetch: ^2.6.1
nopt: ^5.0.0
npmlog: ^4.1.2
rimraf: ^3.0.2
semver: ^7.3.4
tar: ^6.1.0
bin:
node-pre-gyp: bin/node-pre-gyp
checksum: c1f182a707f5782e47b77a76e9d6a073fb043999cf9ad965bc86732e88db27ad00926d1602918edb7105f05cde67871b84a178ee9844eb742319ade419636675
languageName: node
linkType: hard
"@npmcli/move-file@npm:^1.0.1":
version: 1.1.2
resolution: "@npmcli/move-file@npm:1.1.2"
@@ -1511,6 +1530,15 @@ __metadata:
languageName: node
linkType: hard
"bindings@npm:~1.5.0":
version: 1.5.0
resolution: "bindings@npm:1.5.0"
dependencies:
file-uri-to-path: 1.0.0
checksum: 65b6b48095717c2e6105a021a7da4ea435aa8d3d3cd085cb9e85bcb6e5773cf318c4745c3f7c504412855940b585bdf9b918236612a1c7a7942491de176f1ae7
languageName: node
linkType: hard
"bl@npm:^4.0.3":
version: 4.1.0
resolution: "bl@npm:4.1.0"
@@ -1571,10 +1599,10 @@ __metadata:
get-port: ^5.1.1
image-js: ^0.32.0
jest: ^26.6.3
libxmljs2: ^0.27.0
morgan: ^1.10.0
node-html-parser: ^2.1.0
nodemon: ^2.0.7
scale-that-svg: ^1.0.5
sharp: ^0.27.2
soap: ^0.37.0
supertest: ^6.1.3
@@ -1945,17 +1973,6 @@ __metadata:
languageName: node
linkType: hard
"clean-deep@npm:3.0.2":
version: 3.0.2
resolution: "clean-deep@npm:3.0.2"
dependencies:
lodash.isempty: ^4.4.0
lodash.isplainobject: ^4.0.6
lodash.transform: ^4.6.0
checksum: e086672a5dd049bdee7d0fba7ba3bfd61cba5c4ec12de672c2d2954c657d0f113d42a46450a6bb0da70e682271bebbd961fd23359efe6a5f5321192822da4655
languageName: node
linkType: hard
"clean-stack@npm:^2.0.0":
version: 2.2.0
resolution: "clean-stack@npm:2.2.0"
@@ -2412,16 +2429,6 @@ __metadata:
languageName: node
linkType: hard
"deep-rename-keys@npm:^0.2.1":
version: 0.2.1
resolution: "deep-rename-keys@npm:0.2.1"
dependencies:
kind-of: ^3.0.2
rename-keys: ^1.1.2
checksum: 34c838a7ee375e9579be2ba1de59a5ec5aef46bee96bb08cffdd8b6b3887d941bbfce0caccf3ecd7c5a74e6f3a70c7df0b469e732a97e5849a5149ddc1e2a062
languageName: node
linkType: hard
"deepmerge@npm:^4.2.2":
version: 4.2.2
resolution: "deepmerge@npm:4.2.2"
@@ -2578,13 +2585,6 @@ __metadata:
languageName: node
linkType: hard
"element-to-path@npm:1.2.0":
version: 1.2.0
resolution: "element-to-path@npm:1.2.0"
checksum: 65c99a2ec3be262323ae94b55c32da643910e25560ca5fc72d5e1b5e0cef241d62827fec29080d7739c66c2395a54e5fb66d8b70eb55aac01fbcbf2606a0b8db
languageName: node
linkType: hard
"emittery@npm:^0.7.1":
version: 0.7.2
resolution: "emittery@npm:0.7.2"
@@ -2753,13 +2753,6 @@ __metadata:
languageName: node
linkType: hard
"eventemitter3@npm:^2.0.0":
version: 2.0.3
resolution: "eventemitter3@npm:2.0.3"
checksum: dfbf4a07144afea0712d8e6a7f30ae91beb7c12c36c3d480818488aafa437d9a331327461f82c12dfd60a4fbad502efc97f684089cda02809988b84a23630752
languageName: node
linkType: hard
"exec-sh@npm:^0.3.2":
version: 0.3.6
resolution: "exec-sh@npm:0.3.6"
@@ -3040,6 +3033,13 @@ __metadata:
languageName: node
linkType: hard
"file-uri-to-path@npm:1.0.0":
version: 1.0.0
resolution: "file-uri-to-path@npm:1.0.0"
checksum: b648580bdd893a008c92c7ecc96c3ee57a5e7b6c4c18a9a09b44fb5d36d79146f8e442578bc0e173dc027adf3987e254ba1dfd6e3ec998b7c282873010502144
languageName: node
linkType: hard
"fill-range@npm:^4.0.0":
version: 4.0.0
resolution: "fill-range@npm:4.0.0"
@@ -4116,7 +4116,7 @@ __metadata:
languageName: node
linkType: hard
"is-plain-object@npm:^2.0.1, is-plain-object@npm:^2.0.3, is-plain-object@npm:^2.0.4":
"is-plain-object@npm:^2.0.3, is-plain-object@npm:^2.0.4":
version: 2.0.4
resolution: "is-plain-object@npm:2.0.4"
dependencies:
@@ -4946,6 +4946,17 @@ __metadata:
languageName: node
linkType: hard
"libxmljs2@npm:^0.27.0":
version: 0.27.0
resolution: "libxmljs2@npm:0.27.0"
dependencies:
"@mapbox/node-pre-gyp": ^1.0.1
bindings: ~1.5.0
nan: ~2.14.0
checksum: 8e575bf6b0a1740c09a455238f12783e37e98d54eeeea5c233777bf02431a7c74df063884a9de1dac689cdcdf48e1c1cf2bad07a869c2ee46b88b8afd5627b6d
languageName: node
linkType: hard
"lines-and-columns@npm:^1.1.6":
version: 1.1.6
resolution: "lines-and-columns@npm:1.1.6"
@@ -4962,27 +4973,6 @@ __metadata:
languageName: node
linkType: hard
"lodash.isempty@npm:^4.4.0":
version: 4.4.0
resolution: "lodash.isempty@npm:4.4.0"
checksum: a8118f23f7ed72a1dbd176bf27f297d1e71aa1926288449cb8f7cef99ba1bc7527eab52fe7899ab080fa1dc150aba6e4a6367bf49fa4e0b78da1ecc095f8d8c5
languageName: node
linkType: hard
"lodash.isplainobject@npm:^4.0.6":
version: 4.0.6
resolution: "lodash.isplainobject@npm:4.0.6"
checksum: 29c6351f281e0d9a1d58f1a4c8f4400924b4c79f18dfc4613624d7d54784df07efaff97c1ff2659f3e085ecf4fff493300adc4837553104cef2634110b0d5337
languageName: node
linkType: hard
"lodash.transform@npm:^4.6.0":
version: 4.6.0
resolution: "lodash.transform@npm:4.6.0"
checksum: f9d0f583409212e4e94c08c0de1c9e71679e26658d2645be16ee6db55ee2572db5a8395c76f471c00c7d18f3a86c781f7ac51238a7cfa29e9cca253aa0b97149
languageName: node
linkType: hard
"lodash@npm:4.x, lodash@npm:^4.17.19, lodash@npm:^4.17.5, lodash@npm:^4.7.0":
version: 4.17.21
resolution: "lodash@npm:4.17.21"
@@ -5026,7 +5016,7 @@ __metadata:
languageName: node
linkType: hard
"make-dir@npm:^3.0.0":
"make-dir@npm:^3.0.0, make-dir@npm:^3.1.0":
version: 3.1.0
resolution: "make-dir@npm:3.1.0"
dependencies:
@@ -5605,6 +5595,15 @@ __metadata:
languageName: node
linkType: hard
"nan@npm:~2.14.0":
version: 2.14.2
resolution: "nan@npm:2.14.2"
dependencies:
node-gyp: latest
checksum: 7a269139b66a7d37470effb7fb36a8de8cc3b5ffba6e40bb8e0545307911fe5ebf94797ec62f655ecde79c237d169899f8bd28256c66a32cbc8284faaf94c3f4
languageName: node
linkType: hard
"nanomatch@npm:^1.2.9":
version: 1.2.13
resolution: "nanomatch@npm:1.2.13"
@@ -5926,16 +5925,6 @@ __metadata:
languageName: node
linkType: hard
"omit-deep@npm:0.3.0":
version: 0.3.0
resolution: "omit-deep@npm:0.3.0"
dependencies:
is-plain-object: ^2.0.1
unset-value: ^0.1.1
checksum: ca603591af98f717ee4e4ae199778d386304f80072164fc1fb9c27abb011845faa27ffb32e7fa4a240698a4d54822526059af74f12f4f73315ecd7f03825d590
languageName: node
linkType: hard
"on-finished@npm:~2.3.0":
version: 2.3.0
resolution: "on-finished@npm:2.3.0"
@@ -6484,13 +6473,6 @@ __metadata:
languageName: node
linkType: hard
"rename-keys@npm:^1.1.2":
version: 1.2.0
resolution: "rename-keys@npm:1.2.0"
checksum: 9d8e5ca3d1ae3fe6c0d7319a3fd80ded6ca34651e85bff27604982dcc750aed28d1a621374224a9c9072083769f5eab1fd86d1d5a53f54f96c7705c18267227b
languageName: node
linkType: hard
"repeat-element@npm:^1.1.2":
version: 1.1.4
resolution: "repeat-element@npm:1.1.4"
@@ -6741,17 +6723,6 @@ __metadata:
languageName: node
linkType: hard
"scale-that-svg@npm:^1.0.5":
version: 1.0.5
resolution: "scale-that-svg@npm:1.0.5"
dependencies:
element-to-path: 1.2.0
svg-path-tools: 1.0.0
svgson: 3.0.0
checksum: ebad60633871e2a2d7a32aa68c0f390bc4c6d1d20149740ebaf8feae7086faa7e607d06a552ba8f34454d2954d081ec3324942c2aaaae066a2eec29c0c1eedbe
languageName: node
linkType: hard
"semver-diff@npm:^3.1.1":
version: 3.1.1
resolution: "semver-diff@npm:3.1.1"
@@ -7428,25 +7399,6 @@ __metadata:
languageName: node
linkType: hard
"svg-path-tools@npm:1.0.0":
version: 1.0.0
resolution: "svg-path-tools@npm:1.0.0"
checksum: 8e44971bc160dbd459256a4ebcd05bbe80e2fed4824d6c58fd1a303da65982d0df62d3eec4198720f46077095ea1436537829b216d995e56ed8d50cb621029ea
languageName: node
linkType: hard
"svgson@npm:3.0.0":
version: 3.0.0
resolution: "svgson@npm:3.0.0"
dependencies:
clean-deep: 3.0.2
deep-rename-keys: ^0.2.1
omit-deep: 0.3.0
xml-reader: 2.4.3
checksum: 81d828a2f8af8b320d857b536bbae8d88e53e4543478e707230310f46dca08a4b2da1c00ac49e65fcbf79d39a673ccfbd9dfb8e412892a3584711cebc3ca2f30
languageName: node
linkType: hard
"symbol-tree@npm:^3.2.4":
version: 3.2.4
resolution: "symbol-tree@npm:3.2.4"
@@ -7912,16 +7864,6 @@ typescript@^4.1.3:
languageName: node
linkType: hard
"unset-value@npm:^0.1.1":
version: 0.1.2
resolution: "unset-value@npm:0.1.2"
dependencies:
has-value: ^0.3.1
isobject: ^3.0.0
checksum: 56c7de1ee6b726002cc67b82954ec31b795836c2312d4d3d114a500eab5f632e1d3d6f5a164aff1ed90d7ffa94a009c452f6357f1f7d23bc444d489f622aeb9d
languageName: node
linkType: hard
"unset-value@npm:^1.0.0":
version: 1.0.0
resolution: "unset-value@npm:1.0.0"
@@ -8290,15 +8232,6 @@ typescript@^4.1.3:
languageName: node
linkType: hard
"xml-lexer@npm:^0.2.2":
version: 0.2.2
resolution: "xml-lexer@npm:0.2.2"
dependencies:
eventemitter3: ^2.0.0
checksum: ec9d3f8cbc61ed93b7fc1052d05b23cfe5bfe0064a1146f89bc3a9cfbb0c80c6c40d795cc253b745cdbba0607271d14fa57d496a7754fe350a06a8fceae23359
languageName: node
linkType: hard
"xml-name-validator@npm:^3.0.0":
version: 3.0.0
resolution: "xml-name-validator@npm:3.0.0"
@@ -8306,16 +8239,6 @@ typescript@^4.1.3:
languageName: node
linkType: hard
"xml-reader@npm:2.4.3":
version: 2.4.3
resolution: "xml-reader@npm:2.4.3"
dependencies:
eventemitter3: ^2.0.0
xml-lexer: ^0.2.2
checksum: d4b4ca6eb2d61c17d2df2be73dd82a393ae88a4cd10c5152f9908bf3e3bafa5562ce4b63df31ef198bc9a7c8447d4c277c98622438da5b32abf55c2f15984bfa
languageName: node
linkType: hard
"xmlchars@npm:^2.2.0":
version: 2.2.0
resolution: "xmlchars@npm:2.2.0"