Compare commits

...

41 Commits

Author SHA1 Message Date
simojenki
287e203449 Setting node version to 16.6 in ci.yaml actions pipeline 2021-09-07 21:15:24 +10:00
Simon J
92be208a35 Fix bug navigating from album to artist (#47) 2021-09-07 10:24:46 +10:00
simojenki
9d728040e1 Update README 2021-09-05 12:01:07 +10:00
simojenki
8600b9ec85 Update icons 2021-09-05 08:54:30 +10:00
Simon J
588141e569 Migrate to v2 subsonic APIs (#45) 2021-09-04 09:13:57 +10:00
Simon J
b99ff0e5dc Fix album scolling so goes past 100 (#44) 2021-09-03 21:19:40 +10:00
Simon J
9092050c37 Additional Icons (#43) 2021-09-03 13:03:50 +10:00
Simon J
f8f8224213 Additional Icon support (#42) 2021-09-03 10:26:49 +10:00
Simon J
9dcac1f324 Rename access token param to be shorter (#40) 2021-08-31 22:16:20 +10:00
Simon J
f045867554 Remove register button when there are no sonos devices (#39) 2021-08-31 22:03:44 +10:00
Simon J
0f8c45cd03 Fix build (#38) 2021-08-31 09:40:25 +10:00
simojenki
00f6a9ff8f Add version to index page 2021-08-31 09:12:53 +10:00
simojenki
c73f79532d Add icon 2021-08-30 21:17:55 +10:00
simojenki
727affe572 Fix icon 2021-08-30 21:14:01 +10:00
simojenki
59bb702679 Filter out genres with no albums so do not appear in list 2021-08-30 20:57:43 +10:00
simojenki
ee0a0747ee Add new icons, repair icon sizing 2021-08-30 15:56:43 +10:00
Simon J
ca9bf2fc04 Remove labels from genre icons (#37) 2021-08-30 13:01:59 +10:00
simojenki
89accef1e6 Fix build 2021-08-30 12:11:36 +10:00
Simon J
ae29bc14eb Rendering playlist icon collage of 3x3 (#35) 2021-08-30 11:51:22 +10:00
simojenki
e2e73209a2 Fix bug where no genres caused error 2021-08-29 08:14:23 +10:00
simojenki
543d352204 Update README 2021-08-28 07:49:03 +10:00
simojenki
678c8390cc Sample color images 2021-08-28 07:24:44 +10:00
Simon J
29493e090a Icons for genres with backgrounds, text, and ability to specify text color and font family (#34) 2021-08-27 18:14:09 +10:00
Simon J
d1f00f549c Icon resizing of viewPort dynamically, ability to specify custom fore and background colors via env vars (#32) 2021-08-26 15:18:15 +10:00
Simon J
b900863c78 Restructure docker image so relative paths in code work (#30) 2021-08-25 11:09:54 +10:00
Simon J
3bb6776880 Distinguish between supported lang and lang in type system (#29) 2021-08-25 09:41:04 +10:00
Simon J
81d7ea3fe9 Make push_to_registry require build_and_test before running (#27) 2021-08-24 13:19:37 +10:00
Simon J
3970ab5cd3 Fix links on README (#26) 2021-08-24 13:14:45 +10:00
simojenki
75d8c576c3 Replace master workflow with ci workflow, build and docker for v tags 2021-08-24 08:36:18 +10:00
simojenki
77fab65d82 Updating master workflow 2021-08-23 22:01:56 +10:00
simojenki
8dc98ee1a5 Improve error handling when Navidrome fails 2021-08-22 13:12:59 +10:00
simojenki
06db0c2088 Fix bug that causes container to not start due to path issues location icons 2021-08-18 14:17:13 +10:00
simojenki
c8509a23d4 Add docker build step to PR actions workflow 2021-08-18 13:12:42 +10:00
simojenki
2a54eadb3e Add types to Dockerfile 2021-08-18 13:10:47 +10:00
simojenki
0ad1cd5c40 Icons for root menu 2021-08-18 12:54:06 +10:00
simojenki
3545d9c653 Fix bug where langs incorrectly determined in smapi 2021-08-16 23:46:47 +10:00
simojenki
432248fb47 Update nl-NL register to Registreren 2021-08-16 23:43:12 +10:00
simojenki
2cfd52415c Case-insensitive lang search for i8n, along with support for match just lang, without region, ie. 'en' == 'en-US' 2021-08-16 10:50:56 +10:00
simojenki
c67f74bf08 Album view sort by album name not artist name 2021-08-16 10:13:16 +10:00
simojenki
db0351da39 auto register default false 2021-08-15 13:17:17 +10:00
simojenki
5674cd1aa6 Default BONOB_SONOS_AUTO_REGISTER to true, update README for localisation 2021-08-15 10:45:16 +10:00
112 changed files with 5231 additions and 931 deletions

61
.github/workflows/ci.yml vendored Normal file
View File

@@ -0,0 +1,61 @@
name: ci
on:
push:
branches:
- 'master'
tags:
- 'v*'
pull_request:
branches:
- 'master'
jobs:
build_and_test:
runs-on: ubuntu-latest
steps:
-
name: Check out the repo
uses: actions/checkout@v2
-
uses: actions/setup-node@v1
with:
node-version: 16.6.x
-
run: yarn install
-
run: yarn test
push_to_registry:
name: Push Docker image to Docker Hub
needs: build_and_test
runs-on: ubuntu-latest
steps:
-
name: Check out the repo
uses: actions/checkout@v2
with:
fetch-depth: 0
-
name: Docker meta
id: meta
uses: docker/metadata-action@v3
with:
images: simojenki/bonob
-
name: Login to DockerHub
if: github.event_name != 'pull_request'
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
-
name: Push to Docker Hub
uses: docker/build-push-action@v2
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

View File

@@ -1,34 +0,0 @@
name: Build
on:
push:
branches: [ master ]
# pull_request:
# branches: [ master ]
jobs:
build_and_test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: 14.x
- run: yarn install
- run: yarn test
push_to_registry:
needs: build_and_test
name: Push Docker image to Docker Hub
runs-on: ubuntu-latest
steps:
- name: Check out the repo
uses: actions/checkout@v2
- name: Push to Docker Hub
uses: docker/build-push-action@v1
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
repository: simojenki/bonob
tag_with_ref: true

View File

@@ -1,15 +0,0 @@
name: Test PR
on: pull_request
jobs:
build_and_test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: 14.x
- run: yarn install
- run: yarn test

View File

@@ -2,7 +2,10 @@ FROM node:16.6-alpine as build
WORKDIR /bonob
COPY .git ./.git
COPY src ./src
COPY docs ./docs
COPY typings ./typings
COPY web ./web
COPY tests ./tests
COPY jest.config.js .
@@ -17,8 +20,10 @@ RUN apk add --no-cache --update --virtual .gyp \
vips-dev \
python3 \
make \
git \
g++ && \
yarn install --immutable && \
yarn gitinfo && \
yarn test --no-cache && \
yarn build
@@ -34,13 +39,15 @@ WORKDIR /bonob
COPY package.json .
COPY yarn.lock .
COPY --from=build /bonob/build/src/* ./
COPY --from=build /bonob/build/src ./src
COPY --from=build /bonob/node_modules ./node_modules
COPY web web
COPY src/Sonoswsdl-1.19.4-20190411.142401-3.wsdl /bonob/Sonoswsdl-1.19.4-20190411.142401-3.wsdl
COPY --from=build /bonob/.gitinfo ./
COPY web ./web
COPY src/Sonoswsdl-1.19.4-20190411.142401-3.wsdl ./src/Sonoswsdl-1.19.4-20190411.142401-3.wsdl
RUN apk add --no-cache --update vips
USER nobody
CMD ["node", "./app.js"]
CMD ["node", "/bonob/src/app.js"]

View File

@@ -23,29 +23,33 @@ Currently only a single integration allowing Navidrome to be registered with son
- Ability to play a playlist
- Ability to add/remove playlists
- Ability to add/remove tracks from a playlist
- Localization (only en-US & nl-NL supported currently, require translations for other languages). [Sonos localization and supported languages](https://developer.sonos.com/build/content-service-add-features/strings-and-localization/)
## Running
bonob is ditributed via docker and can be run in a number of ways
### Full sonos device auto-discovery by using docker --network host
### Full sonos device auto-discovery and auto-registration using docker --network host
```bash
docker run \
-e BONOB_SONOS_AUTO_REGISTER=true \
-e BONOB_SONOS_DEVICE_DISCOVERY=true \
-p 4534:4534 \
--network host \
simojenki/bonob
```
Now open http://localhost:4534 in your browser, you should see sonos devices, and service configuration. By pressing 'Re-register' bonob will register itself in your sonos system, and should then show up in the "Services" list.
Now open http://localhost:4534 in your browser, you should see sonos devices, and service configuration. Bonob will auto-register itself with your sonos system on startup.
### Full sonos device auto-discovery and auto-registration on custom port by using a sonos seed device, without requiring docker host networking
```bash
docker run \
-e BONOB_PORT=3000 \
-e BONOB_SONOS_AUTO_REGISTER=true \
-e BONOB_SONOS_SEED_HOST=192.168.1.123 \
-e BONOB_SONOS_AUTO_REGISTER=true \
-e BONOB_SONOS_DEVICE_DISCOVERY=true \
-p 3000:3000 \
simojenki/bonob
```
@@ -113,9 +117,9 @@ services:
# ip address of your machine running bonob
BONOB_URL: http://192.168.1.111:4534
BONOB_SECRET: changeme
BONOB_SONOS_SERVICE_ID: 246
BONOB_SONOS_AUTO_REGISTER: "true"
BONOB_SONOS_DEVICE_DISCOVERY: "true"
BONOB_SONOS_AUTO_REGISTER: true
BONOB_SONOS_DEVICE_DISCOVERY: true
BONOB_SONOS_SERVICE_ID: 246
# ip address of one of your sonos devices
BONOB_SONOS_SEED_HOST: 192.168.1.121
BONOB_NAVIDROME_URL: http://navidrome:4533
@@ -137,9 +141,13 @@ 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 [svg color](https://www.december.com/html/spec/colorsvg.html)
BONOB_ICON_BACKGROUND_COLOR | undefined | Icon background color in sonos app, must be a valid [svg color](https://www.december.com/html/spec/colorsvg.html)
## Initialising service within sonos app
- Configure bonob, make sure to set BONOB_URL. **bonob must be accessible from your sonos devices on BONOB_URL, otherwise it will fail to initialise within the sonos app, so make sure you test this in your browser by putting BONOB_URL in the address bar and seeing the bonob information page**
- Start bonob,
- Open sonos app on your device
- Settings -> Services & Voice -> + Add a Service
- Select your Music Service, default name is 'bonob', can be overriden with configuration BONOB_SONOS_SERVICE_NAME
@@ -155,6 +163,26 @@ BONOB_REPORT_NOW_PLAYING | true | Whether to report a track as now playing
- Implement the MusicService/MusicLibrary interface
- Startup bonob with your new implementation.
## Sample Icon colors
```
-e BONOB_ICON_FOREGROUND_COLOR=white \
-e BONOB_ICON_BACKGROUND_COLOR=darkgrey
```
![White & Dark Grey](https://github.com/simojenki/bonob/blob/master/docs/images/whiteDarkGrey.png?raw=true)
```
-e BONOB_ICON_FOREGROUND_COLOR=chartreuse \
-e BONOB_ICON_BACKGROUND_COLOR=fuchsia
```
![Chartreuse & Fuchsia](https://github.com/simojenki/bonob/blob/master/docs/images/chartreuseFuchsia.png?raw=true)
## Credits
- Icons courtesy of: [Navidrome](https://www.navidrome.org/), [Vectornator](https://www.vectornator.io/icons), and @jicho
## TODO
- Artist Radio

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View File

@@ -2,4 +2,8 @@ module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
setupFilesAfterEnv: ["<rootDir>/tests/setup.js"],
modulePathIgnorePatterns: [
'<rootDir>/node_modules',
'<rootDir>/build',
],
};

View File

@@ -18,6 +18,7 @@
"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",
"sharp": "^0.27.2",
@@ -36,6 +37,7 @@
"@types/supertest": "^2.0.10",
"chai": "^4.2.0",
"get-port": "^5.1.1",
"image-js": "^0.32.0",
"jest": "^26.6.3",
"nodemon": "^2.0.7",
"supertest": "^6.1.3",
@@ -48,8 +50,10 @@
"scripts": {
"clean": "rm -Rf build",
"build": "tsc",
"dev": "BONOB_SONOS_SERVICE_NAME=bonobDev BONOB_SONOS_DEVICE_DISCOVERY=true BONOB_SONOS_AUTO_REGISTER=false nodemon ./src/app.ts",
"dev": "BONOB_ICON_FOREGROUND_COLOR=white BONOB_ICON_BACKGROUND_COLOR=darkgrey BONOB_SONOS_SERVICE_NAME=bonobDev BONOB_SONOS_DEVICE_DISCOVERY=true nodemon -V ./src/app.ts",
"devr": "BONOB_ICON_FOREGROUND_COLOR=white BONOB_ICON_BACKGROUND_COLOR=darkgrey BONOB_SONOS_SERVICE_NAME=bonobDev BONOB_SONOS_DEVICE_DISCOVERY=true BONOB_SONOS_AUTO_REGISTER=true nodemon -V ./src/app.ts",
"register-dev": "ts-node ./src/register.ts http://$(hostname):4534",
"test": "jest --testPathIgnorePatterns=build"
"test": "jest",
"gitinfo": "git describe --tags > .gitinfo"
}
}

View File

@@ -1,3 +1,5 @@
import path from "path";
import fs from "fs";
import server from "./server";
import logger from "./logger";
import { appendMimeTypeToClientFor, DEFAULT, Navidrome } from "./navidrome";
@@ -56,15 +58,24 @@ const featureFlagAwareMusicService: MusicService = {
}),
};
export const GIT_INFO = path.join(__dirname, "..", ".gitinfo");
const version = fs.existsSync(GIT_INFO) ? fs.readFileSync(GIT_INFO).toString().trim() : "v??"
const app = server(
sonosSystem,
bonob,
config.bonobUrl,
featureFlagAwareMusicService,
new InMemoryLinkCodes(),
new InMemoryAccessTokens(sha256(config.secret)),
SystemClock,
true,
{
linkCodes: () => new InMemoryLinkCodes(),
accessTokens: () => new InMemoryAccessTokens(sha256(config.secret)),
clock: SystemClock,
iconColors: config.icons,
applyContextPath: true,
logRequests: true,
version
}
);
app.listen(config.port, () => {

View File

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

@@ -16,10 +16,24 @@ export default function () {
process.exit(1);
}
const wordFrom = (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: wordFrom("BONOB_ICON_FOREGROUND_COLOR"),
backgroundColor: wordFrom("BONOB_ICON_BACKGROUND_COLOR"),
},
sonos: {
serviceName: process.env["BONOB_SONOS_SERVICE_NAME"] || "bonob",
deviceDiscovery:

View File

@@ -3,7 +3,8 @@ import { pipe } from "fp-ts/lib/function";
import { option as O } from "fp-ts";
import _ from "underscore";
export type LANG = "en-US" | "nl-NL";
export type LANG = "en-US" | "da-DK" | "de-DE" | "es-ES" | "fr-FR" | "it-IT" | "ja-JP" | "nb-NO" | "nl-NL" | "pt-BR" | "sv-SE" | "zh-CN"
export type SUPPORTED_LANG = "en-US" | "nl-NL";
export type KEY =
| "AppLinkMessage"
| "artists"
@@ -35,9 +36,10 @@ export type KEY =
| "failedToRemoveRegistration"
| "invalidLinkCode"
| "loginSuccessful"
| "loginFailed";
| "loginFailed"
| "noSonosDevices";
const translations: Record<LANG, Record<KEY, string>> = {
const translations: Record<SUPPORTED_LANG, Record<KEY, string>> = {
"en-US": {
AppLinkMessage: "Linking sonos with $BONOB_SONOS_SERVICE_NAME",
artists: "Artists",
@@ -70,6 +72,7 @@ const translations: Record<LANG, Record<KEY, string>> = {
invalidLinkCode: "Invalid linkCode!",
loginSuccessful: "Login successful!",
loginFailed: "Login failed!",
noSonosDevices: "No sonos devices",
},
"nl-NL": {
AppLinkMessage: "Sonos koppelen aan $BONOB_SONOS_SERVICE_NAME",
@@ -88,7 +91,7 @@ const translations: Record<LANG, Record<KEY, string>> = {
expectedConfig: "Verwachte configuratie",
existingServiceConfig: "Bestaande serviceconfiguratie",
noExistingServiceRegistration: "Geen bestaande serviceregistratie",
register: "Register",
register: "Registreren",
removeRegistration: "Verwijder registratie",
devices: "Apparaten",
services: "Services",
@@ -103,12 +106,20 @@ const translations: Record<LANG, Record<KEY, string>> = {
invalidLinkCode: "Ongeldige linkcode!",
loginSuccessful: "Inloggen gelukt!",
loginFailed: "Inloggen mislukt!",
noSonosDevices: "Geen Sonos-apparaten",
},
};
const translationsLookup = Object.keys(translations).reduce((lookups, lang) => {
lookups.set(lang, translations[lang as SUPPORTED_LANG]);
lookups.set(lang.toLocaleLowerCase(), translations[lang as SUPPORTED_LANG]);
lookups.set(lang.toLocaleLowerCase().split("-")[0]!, translations[lang as SUPPORTED_LANG]);
return lookups;
}, new Map<string, Record<KEY, string>>())
export const randomLang = () => _.shuffle(["en-US", "nl-NL"])[0]!;
export const asLANGs = (acceptLanguageHeader: string | undefined) =>
export const asLANGs = (acceptLanguageHeader: string | undefined): LANG[] =>
pipe(
acceptLanguageHeader,
O.fromNullable,
@@ -118,7 +129,8 @@ export const asLANGs = (acceptLanguageHeader: string | undefined) =>
pipe(
it.split(","),
A.map((it) => it.trim()),
A.filter((it) => it != "")
A.filter((it) => it != ""),
A.map(it => it as LANG)
)
),
O.getOrElseW(() => [])
@@ -130,12 +142,12 @@ export type Lang = (key: KEY) => string;
export const langs = () => Object.keys(translations);
export const keys = (lang: LANG = "en-US") => Object.keys(translations[lang]);
export const keys = (lang: SUPPORTED_LANG = "en-US") => Object.keys(translations[lang]);
export default (serviceName: string): I8N =>
(...langs: string[]): Lang => {
const langToUse =
langs.map((l) => translations[l as LANG]).find((it) => it) ||
langs.map((l) => translationsLookup.get(l as SUPPORTED_LANG)).find((it) => it) ||
translations["en-US"];
return (key: KEY) => {
const value = langToUse[key]?.replace(

470
src/icon.ts Normal file
View File

@@ -0,0 +1,470 @@
import libxmljs, { Element, Attribute } from "libxmljs2";
import _ from "underscore";
import fs from "fs";
import {
Clock,
isChristmas,
isCNY_2022,
isCNY_2023,
isCNY_2024,
isHalloween,
isHoli,
isMay4,
SystemClock,
} from "./clock";
import path from "path";
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 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(spec: Partial<IconSpec>): Icon;
apply(transformer: Transformer): Icon;
}
export type Transformer = (icon: Icon) => Icon;
export function transform(spec: Partial<IconSpec>): Transformer {
return (icon: Icon) =>
icon.with({
...spec,
features: { ...spec.features },
});
}
export function features(features: Partial<IconFeatures>): Transformer {
return (icon: Icon) => icon.with({ features });
}
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 {
svg: string;
features: IconFeatures;
constructor(
svg: string,
features: Partial<IconFeatures> = {
viewPortIncreasePercent: undefined,
backgroundColor: undefined,
foregroundColor: undefined,
}
) {
this.svg = svg;
this.features = {
viewPortIncreasePercent: undefined,
backgroundColor: undefined,
foregroundColor: undefined,
...features,
};
}
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, {
noblanks: true,
net: false,
});
const viewBoxAttr = xml.get("//svg:svg/@viewBox", SVG_NS) as Attribute;
let viewBox = new ViewBox(viewBoxAttr.value());
if (
this.features.viewPortIncreasePercent &&
this.features.viewPortIncreasePercent > 0
) {
viewBox = viewBox.increasePercent(this.features.viewPortIncreasePercent);
viewBoxAttr.value(viewBox.toString());
}
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.features.backgroundColor,
})
);
}
if (this.features.foregroundColor) {
(xml.find("//svg:path", SVG_NS) as Element[]).forEach((path) => {
if (path.attr("fill"))
path.attr({ stroke: this.features.foregroundColor! });
else path.attr({ fill: this.features.foregroundColor! });
});
}
return xml.toString();
};
}
export const HOLI_COLORS = [
"#06bceb",
"#9fc717",
"#fbdc10",
"#f00b9a",
"#fa9705",
];
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"
| "error"
| "chill"
| "country"
| "dance"
| "disco"
| "film"
| "new"
| "old"
| "cannabis"
| "trip"
| "opera"
| "world"
| "violin"
| "celtic"
| "children"
| "chillout"
| "progressiveRock"
| "christmas"
| "halloween"
| "yoDragon"
| "yoRabbit"
| "yoTiger"
| "chapel"
| "audioWave"
| "c3po"
| "chewy"
| "darth"
| "skywalker"
| "leia"
| "r2d2"
| "yoda";
const iconFrom = (name: string) =>
new SvgIcon(
fs
.readFileSync(path.resolve(__dirname, "..", "web", "icons", name))
.toString()
);
export const ICONS: Record<ICON, SvgIcon> = {
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("Opera-Glasses-102740.svg"),
mushroom: iconFrom("Mushroom-63864.svg"),
african: iconFrom("Africa-48087.svg"),
rock: iconFrom("Rock-Music-11076.svg"),
progressiveRock: iconFrom("Progressive-Rock-24862.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-22940.svg"),
oz: iconFrom("Kangaroo-16730.svg"),
hipHop: iconFrom("Hip-Hop Music-17757.svg"),
rap: iconFrom("Rap-24851.svg"),
horror: iconFrom("Horror-88855.svg"),
pop: iconFrom("Ice-Pop Yellow-94532.svg"),
blues: iconFrom("Blues-113548.svg"),
classical: iconFrom("Classic-Music-17728.svg"),
comedy: iconFrom("Comedy-5937.svg"),
vinyl: iconFrom("Music-Record-102104.svg"),
electronic: iconFrom("Electronic-Music-17745.svg"),
pills: iconFrom("Pills-92954.svg"),
trumpet: iconFrom("Trumpet-17823.svg"),
conductor: iconFrom("Music-Conductor-225.svg"),
reggae: iconFrom("Reggae-24843.svg"),
music: iconFrom("Music-14097.svg"),
error: iconFrom("Error-82783.svg"),
chill: iconFrom("Fridge-282.svg"),
country: iconFrom("Country-Music-113286.svg"),
dance: iconFrom("Tango-25015.svg"),
disco: iconFrom("Disco-Ball-25777.svg"),
film: iconFrom("Film-Reel-3230.svg"),
new: iconFrom("New-47652.svg"),
old: iconFrom("Old-Woman-77881.svg"),
cannabis: iconFrom("Cannabis-33270.svg"),
trip: iconFrom("TripAdvisor-44407.svg"),
opera: iconFrom("Sydney-Opera House-59090.svg"),
world: iconFrom("Globe-1301.svg"),
violin: iconFrom("Violin-3421.svg"),
celtic: iconFrom("Scottish-Thistle-108212.svg"),
children: iconFrom("Children-78186.svg"),
chillout: iconFrom("Sleeping-in Bed-14385.svg"),
christmas: iconFrom("Christmas-Tree-63332.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"),
c3po: iconFrom("C-3PO-31823.svg"),
chewy: iconFrom("Chewbacca-89771.svg"),
darth: iconFrom("Darth-Vader-35734.svg"),
skywalker: iconFrom("Luke-Skywalker-39424.svg"),
leia: iconFrom("Princess-Leia-68568.svg"),
r2d2: iconFrom("R2-D2-39423.svg"),
yoda: iconFrom("Yoda-68107.svg"),
};
export const STAR_WARS = [ICONS.c3po, ICONS.chewy, ICONS.darth, ICONS.skywalker, ICONS.leia, ICONS.r2d2, ICONS.yoda];
export type RULE = (genre: string) => boolean;
export const eq =
(expected: string): RULE =>
(value: string) =>
expected.toLowerCase() === value.toLowerCase();
export const contains =
(expected: string): RULE =>
(value: string) =>
value.toLowerCase().includes(expected.toLowerCase());
export const containsWord =
(expected: string): RULE =>
(value: string) =>
value.toLowerCase().split(/\W/).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"],
[eq("African"), "african"],
[eq("Americana"), "americana"],
[eq("Film Score"), "film"],
[eq("Soundtrack"), "film"],
[eq("Stoner Rock"), "cannabis"],
[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"],
[containsWord("Book"), "book"],
[containsWord("Australian"), "oz"],
[containsWord("Baroque"), "violin"],
[containsWord("Rap"), "rap"],
[containsWithAllTheNonWordCharsRemoved("Hip Hop"), "hipHop"],
[containsWithAllTheNonWordCharsRemoved("Trip Hop"), "trip"],
[containsWord("Metal"), "metal"],
[containsWord("Punk"), "punk"],
[containsWord("Blues"), "blues"],
[eq("Classic"), "classical"],
[containsWord("Classical"), "classical"],
[containsWord("Comedy"), "comedy"],
[containsWord("Komedie"), "comedy"], // dutch for Comedy
[containsWord("Turntable"), "vinyl"],
[containsWord("Dub"), "electronic"],
[eq("Dubstep"), "electronic"],
[eq("Drum And Bass"), "electronic"],
[contains("Goa"), "mushroom"],
[contains("Psy"), "mushroom"],
[containsWord("Trance"), "pills"],
[containsWord("Techno"), "pills"],
[containsWord("House"), "pills"],
[containsWord("Rave"), "pills"],
[containsWord("Jazz"), "trumpet"],
[containsWord("Orchestra"), "conductor"],
[containsWord("Reggae"), "reggae"],
[containsWord("Disco"), "disco"],
[containsWord("New"), "new"],
[containsWord("Opera"), "opera"],
[containsWord("Vocal"), "opera"],
[containsWord("Ballad"), "opera"],
[containsWord("Western"), "country"],
[containsWord("World"), "world"],
[contains("Electro"), "electronic"],
[contains("Dance"), "dance"],
[contains("Pop"), "pop"],
[contains("Horror"), "horror"],
[contains("Children"), "children"],
[contains("Chill"), "chill"],
[contains("Old"), "old"],
[containsWord("Christian"), "chapel"],
[containsWord("Religious"), "chapel"],
[containsWord("Spoken"), "audioWave"],
];
export function iconForGenre(genre: string): ICON {
const [_, name] = GENRE_RULES.find(([rule, _]) => rule(genre)) || [
"music",
"music",
];
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",
},
})
),
maybeTransform(
() => isMay4(clock),
transform({
svg: STAR_WARS[_.random(STAR_WARS.length - 1)]!.svg,
features: {
backgroundColor: undefined,
foregroundColor: undefined,
},
})
)
);
};

View File

@@ -99,7 +99,7 @@ export const asResult = <T>([results, total]: [T[], number]) => ({
export type ArtistQuery = Paging;
export type AlbumQueryType = 'alphabeticalByArtist' | 'byGenre' | 'random' | 'recent' | 'frequent' | 'newest' | 'starred';
export type AlbumQueryType = 'alphabeticalByArtist' | 'alphabeticalByName' | 'byGenre' | 'random' | 'recent' | 'frequent' | 'newest' | 'starred';
export type AlbumQuery = Paging & {
type: AlbumQueryType;
@@ -120,6 +120,11 @@ export const albumToAlbumSummary = (it: Album): AlbumSummary => ({
artistId: it.artistId,
});
export const playlistToPlaylistSummary = (it: Playlist): PlaylistSummary => ({
id: it.id,
name: it.name
})
export type StreamingHeader = "content-type" | "content-length" | "content-range" | "accept-ranges";
export type TrackStream = {

View File

@@ -90,7 +90,7 @@ export type GetArtistsResponse = SubsonicResponse & {
};
export type GetAlbumListResponse = SubsonicResponse & {
albumList: {
albumList2: {
album: album[];
};
};
@@ -101,7 +101,7 @@ export type genre = {
__text: string;
};
export type GenGenresResponse = SubsonicResponse & {
export type GetGenresResponse = SubsonicResponse & {
genres: {
genre: genre[];
};
@@ -130,7 +130,7 @@ export type ArtistInfo = {
};
export type GetArtistInfoResponse = SubsonicResponse & {
artistInfo: artistInfo;
artistInfo2: artistInfo;
};
export type GetArtistResponse = SubsonicResponse & {
@@ -197,12 +197,12 @@ export type GetPlaylistsResponse = {
};
export type GetSimilarSongsResponse = {
similarSongs: { song: song[] }
}
similarSongs2: { song: song[] };
};
export type GetTopSongsResponse = {
topSongs: { song: song[] }
}
topSongs: { song: song[] };
};
export type GetSongResponse = {
song: song;
@@ -236,7 +236,7 @@ export type getAlbumListParams = {
genre?: string;
};
const MAX_ALBUM_LIST = 500;
export const MAX_ALBUM_LIST = 500;
const asTrack = (album: Album, song: song) => ({
id: song._id,
@@ -248,7 +248,7 @@ const asTrack = (album: Album, song: song) => ({
album,
artist: {
id: song._artistId,
name: song._artist
name: song._artist,
},
});
@@ -349,25 +349,25 @@ export class Navidrome implements MusicService {
new X2JS({
arrayAccessFormPaths: [
"subsonic-response.album.song",
"subsonic-response.albumList.album",
"subsonic-response.albumList2.album",
"subsonic-response.artist.album",
"subsonic-response.artists.index",
"subsonic-response.artists.index.artist",
"subsonic-response.artistInfo.similarArtist",
"subsonic-response.artistInfo2.similarArtist",
"subsonic-response.genres.genre",
"subsonic-response.playlist.entry",
"subsonic-response.playlists.playlist",
"subsonic-response.searchResult3.album",
"subsonic-response.searchResult3.artist",
"subsonic-response.searchResult3.song",
"subsonic-response.similarSongs.song",
"subsonic-response.similarSongs2.song",
"subsonic-response.topSongs.song",
],
}).xml2js(response.data) as SubconicEnvelope
)
.then((json) => json["subsonic-response"])
.then((json) => {
if (isError(json)) throw json.error._message;
if (isError(json)) throw `Navidrome error:${json.error._message}`;
else return json as unknown as T;
});
@@ -389,28 +389,31 @@ export class Navidrome implements MusicService {
)
);
getArtists = (credentials: Credentials): Promise<IdName[]> =>
getArtists = (
credentials: Credentials
): Promise<(IdName & { albumCount: number })[]> =>
this.getJSON<GetArtistsResponse>(credentials, "/rest/getArtists")
.then((it) => (it.artists.index || []).flatMap((it) => it.artist || []))
.then((artists) =>
artists.map((artist) => ({
id: artist._id,
name: artist._name,
albumCount: Number.parseInt(artist._albumCount),
}))
);
getArtistInfo = (credentials: Credentials, id: string): Promise<ArtistInfo> =>
this.getJSON<GetArtistInfoResponse>(credentials, "/rest/getArtistInfo", {
this.getJSON<GetArtistInfoResponse>(credentials, "/rest/getArtistInfo2", {
id,
count: 50,
includeNotPresent: true
includeNotPresent: true,
}).then((it) => ({
image: {
small: validate(it.artistInfo.smallImageUrl),
medium: validate(it.artistInfo.mediumImageUrl),
large: validate(it.artistInfo.largeImageUrl),
small: validate(it.artistInfo2.smallImageUrl),
medium: validate(it.artistInfo2.mediumImageUrl),
large: validate(it.artistInfo2.largeImageUrl),
},
similarArtist: (it.artistInfo.similarArtist || []).map((artist) => ({
similarArtist: (it.artistInfo2.similarArtist || []).map((artist) => ({
id: artist._id,
name: artist._name,
inLibrary: artist._id != "-1",
@@ -516,28 +519,38 @@ export class Navidrome implements MusicService {
})),
artist: async (id: string): Promise<Artist> =>
navidrome.getArtistWithInfo(credentials, id),
albums: (q: AlbumQuery): Promise<Result<AlbumSummary>> =>
navidrome
.getJSON<GetAlbumListResponse>(credentials, "/rest/getAlbumList", {
...pick(q, "type", "genre"),
size: Math.min(MAX_ALBUM_LIST, q._count),
offset: q._index,
})
.then((response) => response.albumList.album || [])
.then(navidrome.toAlbumSummary)
.then(slice2(q))
.then(([page, total]) => ({
results: page,
total: Math.min(MAX_ALBUM_LIST, total),
})),
albums: async (q: AlbumQuery): Promise<Result<AlbumSummary>> => {
return Promise.all([
navidrome
.getArtists(credentials)
.then((it) =>
_.inject(it, (total, artist) => total + artist.albumCount, 0)
),
navidrome
.getJSON<GetAlbumListResponse>(credentials, "/rest/getAlbumList2", {
...pick(q, "type", "genre"),
size: 500,
offset: q._index,
})
.then((response) => response.albumList2.album || [])
.then(navidrome.toAlbumSummary),
]).then(([total, albums]) => ({
results: albums.slice(0, q._count),
total:
albums.length == 500
? total
: q._index + albums.length,
}));
},
album: (id: string): Promise<Album> =>
navidrome.getAlbum(credentials, id),
genres: () =>
navidrome
.getJSON<GenGenresResponse>(credentials, "/rest/getGenres")
.getJSON<GetGenresResponse>(credentials, "/rest/getGenres")
.then((it) =>
pipe(
it.genres.genre,
it.genres.genre || [],
A.filter((it) => Number.parseInt(it._albumCount) > 0),
A.map((it) => it.__text),
A.sort(ordString),
A.map((it) => ({ id: it, name: it }))
@@ -711,7 +724,7 @@ export class Navidrome implements MusicService {
},
artist: {
id: entry._artistId,
name: entry._artist
name: entry._artist,
},
})),
};
@@ -743,24 +756,41 @@ export class Navidrome implements MusicService {
songIndexToRemove: indicies,
})
.then((_) => true),
similarSongs: async (id: string) => navidrome
.getJSON<GetSimilarSongsResponse>(credentials, "/rest/getSimilarSongs", { id, count: 50 })
.then((it) => (it.similarSongs.song || []))
.then(songs =>
Promise.all(
songs.map((song) => navidrome.getAlbum(credentials, song._albumId).then(album => asTrack(album, song)))
similarSongs: async (id: string) =>
navidrome
.getJSON<GetSimilarSongsResponse>(
credentials,
"/rest/getSimilarSongs2",
{ id, count: 50 }
)
),
topSongs: async (artistId: string) => navidrome
.getArtist(credentials, artistId)
.then(({ name }) => navidrome
.getJSON<GetTopSongsResponse>(credentials, "/rest/getTopSongs", { artist: name, count: 50 })
.then((it) => (it.topSongs.song || []))
.then(songs =>
.then((it) => it.similarSongs2.song || [])
.then((songs) =>
Promise.all(
songs.map((song) => navidrome.getAlbum(credentials, song._albumId).then(album => asTrack(album, song)))
songs.map((song) =>
navidrome
.getAlbum(credentials, song._albumId)
.then((album) => asTrack(album, song))
)
)
))
),
topSongs: async (artistId: string) =>
navidrome.getArtist(credentials, artistId).then(({ name }) =>
navidrome
.getJSON<GetTopSongsResponse>(credentials, "/rest/getTopSongs", {
artist: name,
count: 50,
})
.then((it) => it.topSongs.song || [])
.then((songs) =>
Promise.all(
songs.map((song) =>
navidrome
.getAlbum(credentials, song._albumId)
.then((album) => asTrack(album, song))
)
)
)
),
};
return Promise.resolve(musicLibrary);

View File

@@ -1,7 +1,8 @@
import { option as O } from "fp-ts";
import express, { Express, Request } from "express";
import * as Eta from "eta";
import morgan from "morgan";
import path from "path";
import sharp from "sharp";
import { PassThrough, Transform, TransformCallback } from "stream";
@@ -13,7 +14,7 @@ import {
SONOS_RECOMMENDED_IMAGE_SIZES,
LOGIN_ROUTE,
CREATE_REGISTRATION_ROUTE,
REMOVE_REGISTRATION_ROUTE
REMOVE_REGISTRATION_ROUTE,
} from "./smapi";
import { LinkCodes, InMemoryLinkCodes } from "./link_codes";
import { MusicService, isSuccess } from "./music_service";
@@ -24,8 +25,12 @@ 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, ICONS, festivals, features } from "./icon";
import _, { shuffle } from "underscore";
import morgan from "morgan";
import { takeWithRepeats } from "./utils";
export const BONOB_ACCESS_TOKEN_HEADER = "bonob-access-token";
export const BONOB_ACCESS_TOKEN_HEADER = "bat";
interface RangeFilter extends Transform {
range: (length: number) => string;
@@ -64,20 +69,48 @@ export class RangeBytesFromFilter extends Transform {
range = (number: number) => `${this.from}-${number - 1}/${number}`;
}
export type ServerOpts = {
linkCodes: () => LinkCodes;
accessTokens: () => AccessTokens;
clock: Clock;
iconColors: {
foregroundColor: string | undefined;
backgroundColor: string | undefined;
};
applyContextPath: boolean;
logRequests: boolean;
version: string;
};
const DEFAULT_SERVER_OPTS: ServerOpts = {
linkCodes: () => new InMemoryLinkCodes(),
accessTokens: () => new AccessTokenPerAuthToken(),
clock: SystemClock,
iconColors: { foregroundColor: undefined, backgroundColor: undefined },
applyContextPath: true,
logRequests: false,
version: "v?",
};
function server(
sonos: Sonos,
service: Service,
bonobUrl: URLBuilder,
musicService: MusicService,
linkCodes: LinkCodes = new InMemoryLinkCodes(),
accessTokens: AccessTokens = new AccessTokenPerAuthToken(),
clock: Clock = SystemClock,
applyContextPath = true
opts: Partial<ServerOpts> = {}
): Express {
const serverOpts = { ...DEFAULT_SERVER_OPTS, ...opts };
const linkCodes = serverOpts.linkCodes();
const accessTokens = serverOpts.accessTokens();
const clock = serverOpts.clock;
const app = express();
const i8n = makeI8N(service.name);
app.use(morgan("combined"));
if (serverOpts.logRequests) {
app.use(morgan("combined"));
}
app.use(express.urlencoded({ extended: false }));
// todo: pass options in here?
@@ -85,9 +118,16 @@ function server(
app.engine("eta", Eta.renderFile);
app.set("view engine", "eta");
app.set("views", "./web/views");
app.set("views", path.resolve(__dirname, "..", "web", "views"));
const langFor = (req: Request) => i8n(...asLANGs(req.headers["accept-language"]))
app.set("query parser", "simple");
const langFor = (req: Request) => {
logger.debug(
`${req.path} (req[accept-language]=${req.headers["accept-language"]})`
);
return i8n(...asLANGs(req.headers["accept-language"]));
};
app.get("/", (req, res) => {
const lang = langFor(req);
@@ -102,8 +142,13 @@ function server(
services,
bonobService: service,
registeredBonobService,
createRegistrationRoute: bonobUrl.append({ pathname: CREATE_REGISTRATION_ROUTE }).pathname(),
removeRegistrationRoute: bonobUrl.append({ pathname: REMOVE_REGISTRATION_ROUTE }).pathname(),
createRegistrationRoute: bonobUrl
.append({ pathname: CREATE_REGISTRATION_ROUTE })
.pathname(),
removeRegistrationRoute: bonobUrl
.append({ pathname: REMOVE_REGISTRATION_ROUTE })
.pathname(),
version: opts.version,
});
}
);
@@ -113,8 +158,8 @@ function server(
return res.send({
service: {
name: service.name,
sid: service.sid
}
sid: service.sid,
},
});
});
@@ -184,15 +229,19 @@ function server(
res.status(403).render("failure", {
lang,
message: lang("loginFailed"),
cause: authResult.message
cause: authResult.message,
});
}
}
});
app.get(STRINGS_ROUTE, (_, res) => {
const stringNode = (id: string, value: string) => `<string stringId="${id}"><![CDATA[${value}]]></string>`
const stringtableNode = (langName: string) => `<stringtable rev="1" xml:lang="${langName}">${i8nKeys().map(key => stringNode(key, i8n(langName as LANG)(key as KEY))).join("")}</stringtable>`
const stringNode = (id: string, value: string) =>
`<string stringId="${id}"><![CDATA[${value}]]></string>`;
const stringtableNode = (langName: string) =>
`<stringtable rev="1" xml:lang="${langName}">${i8nKeys()
.map((key) => stringNode(key, i8n(langName as LANG)(key as KEY)))
.join("")}</stringtable>`;
res.type("application/xml").send(`<?xml version="1.0" encoding="utf-8" ?>
<stringtables xmlns="http://sonos.com/sonosapi">
@@ -208,12 +257,23 @@ function server(
<Match>
<imageSizeMap>
${SONOS_RECOMMENDED_IMAGE_SIZES.map(
(size) =>
`<sizeEntry size="${size}" substitution="/art/size/${size}"/>`
).join("")}
(size) =>
`<sizeEntry size="${size}" substitution="/size/${size}"/>`
).join("")}
</imageSizeMap>
</Match>
</PresentationMap>
<PresentationMap type="BrowseIconSizeMap">
<Match>
<browseIconSizeMap>
<sizeEntry size="0" substitution="/size/legacy"/>
${SONOS_RECOMMENDED_IMAGE_SIZES.map(
(size) =>
`<sizeEntry size="${size}" substitution="/size/${size}"/>`
).join("")}
</browseIconSizeMap>
</Match>
</PresentationMap>
<PresentationMap type="Search">
<Match>
<SearchCategories>
@@ -252,7 +312,8 @@ function server(
)
.then(({ musicLibrary, stream }) => {
logger.info(
`stream response from music service for ${id}, status=${stream.status
`stream response from music service for ${id}, status=${
stream.status
}, headers=(${JSON.stringify(stream.headers)})`
);
@@ -325,38 +386,141 @@ function server(
}
});
app.get("/:type/:id/art/size/:size", (req, res) => {
app.get("/icon/:type/size/:size", (req, res) => {
const type = req.params["type"]!;
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)
) {
return res.status(400).send();
} else {
let icon = (ICONS as any)[type]! as Icon;
const spec =
size == "legacy"
? {
mimeType: "image/png",
responseFormatter: (svg: string): Promise<Buffer | string> =>
sharp(Buffer.from(svg)).resize(80).png().toBuffer(),
}
: {
mimeType: "image/svg+xml",
responseFormatter: (svg: string): Promise<Buffer | string> =>
Promise.resolve(svg),
};
return Promise.resolve(
icon
.apply(
features({
viewPortIncreasePercent: 80,
...serverOpts.iconColors,
})
)
.apply(festivals(clock))
.toString()
)
.then(spec.responseFormatter)
.then((data) => res.status(200).type(spec.mimeType).send(data));
}
});
app.get("/icons", (_, res) => {
res.render("icons", {
icons: Object.keys(ICONS).map((k) => [
k,
((ICONS as any)[k] as Icon)
.apply(
features({
viewPortIncreasePercent: 80,
...serverOpts.iconColors,
})
)
.toString()
.replace('<?xml version="1.0" encoding="UTF-8"?>', ""),
]),
});
});
const GRAVITY_9 = [
"north",
"northeast",
"east",
"southeast",
"south",
"southwest",
"west",
"northwest",
"centre",
];
app.get("/art/:type/:ids/size/:size", (req, res) => {
const authToken = accessTokens.authTokenFor(
req.query[BONOB_ACCESS_TOKEN_HEADER] as string
);
const type = req.params["type"]!;
const id = req.params["id"]!;
const ids = req.params["ids"]!.split("&");
const size = Number.parseInt(req.params["size"]!);
if (!authToken) {
return res.status(401).send();
} else if (type != "artist" && type != "album") {
return res.status(400).send();
} else {
return musicService
.login(authToken)
.then((it) => it.coverArt(id, type, size))
.then((coverArt) => {
if (coverArt) {
res.status(200);
res.setHeader("content-type", coverArt.contentType);
res.send(coverArt.data);
} else {
res.status(404).send();
}
})
.catch((e: Error) => {
logger.error(
`Failed fetching image ${type}/${id}/size/${size}: ${e.message}`,
e
);
res.status(500).send();
});
} else if (!(size > 0)) {
return res.status(400).send();
}
return musicService
.login(authToken)
.then((it) => Promise.all(ids.map((id) => it.coverArt(id, type, size))))
.then((coverArts) => coverArts.filter((it) => it))
.then(shuffle)
.then((coverArts) => {
if (coverArts.length == 1) {
const coverArt = coverArts[0]!;
res.status(200);
res.setHeader("content-type", coverArt.contentType);
return res.send(coverArt.data);
} else if (coverArts.length > 1) {
const gravity = [...GRAVITY_9];
return sharp({
create: {
width: size * 3,
height: size * 3,
channels: 3,
background: { r: 255, g: 255, b: 255 },
},
})
.composite(
takeWithRepeats(coverArts, 9).map((art) => ({
input: art?.data,
gravity: gravity.pop(),
}))
)
.png()
.toBuffer()
.then((image) => sharp(image).resize(size).png().toBuffer())
.then((image) => {
res.status(200);
res.setHeader("content-type", "image/png");
return res.send(image);
});
} else {
return res.status(404).send();
}
})
.catch((e: Error) => {
logger.error(
`Failed fetching image ${type}/${ids.join("&")}/size/${size}`,
{
cause: e,
}
);
return res.status(500).send();
});
});
bindSmapiSoapServiceToExpress(
@@ -370,7 +534,7 @@ function server(
i8n
);
if (applyContextPath) {
if (serverOpts.applyContextPath) {
const container = express();
container.use(bonobUrl.path(), app);
return container;

View File

@@ -5,7 +5,6 @@ import { readFileSync } from "fs";
import path from "path";
import logger from "./logger";
import { LinkCodes } from "./link_codes";
import {
Album,
@@ -14,7 +13,7 @@ import {
ArtistSummary,
Genre,
MusicService,
PlaylistSummary,
Playlist,
slice2,
Track,
} from "./music_service";
@@ -22,7 +21,9 @@ import { AccessTokens } from "./access_tokens";
import { BONOB_ACCESS_TOKEN_HEADER } from "./server";
import { Clock } from "./clock";
import { URLBuilder } from "./url_builder";
import { I8N, LANG } from "./i8n";
import { asLANGs, I8N } from "./i8n";
import { ICON, iconForGenre } from "./icon";
import { uniq } from "underscore";
export const LOGIN_ROUTE = "/login";
export const CREATE_REGISTRATION_ROUTE = "/registration/add";
@@ -183,11 +184,14 @@ class SonosSoap {
},
};
} else {
logger.info("Client not linked, awaiting user to associate account with link code by logging in.")
logger.info(
"Client not linked, awaiting user to associate account with link code by logging in."
);
throw {
Fault: {
faultcode: "Client.NOT_LINKED_RETRY",
faultstring: "Link Code not found yet, sonos app will keep polling until you log in to bonob",
faultstring:
"Link Code not found yet, sonos app will keep polling until you log in to bonob",
detail: {
ExceptionInfo: "NOT_LINKED_RETRY",
SonosError: "5",
@@ -207,16 +211,21 @@ export type Container = {
displayType: string | undefined;
};
const genre = (genre: Genre) => ({
const genre = (bonobUrl: URLBuilder, genre: Genre) => ({
itemType: "container",
id: `genre:${genre.id}`,
title: genre.name,
albumArtURI: iconArtURI(
bonobUrl,
iconForGenre(genre.name)
).href(),
});
const playlist = (playlist: PlaylistSummary) => ({
const playlist = (bonobUrl: URLBuilder, playlist: Playlist) => ({
itemType: "playlist",
id: `playlist:${playlist.id}`,
title: playlist.name,
albumArtURI: playlistAlbumArtURL(bonobUrl, playlist).href(),
canPlay: true,
attributes: {
readOnly: false,
@@ -225,19 +234,41 @@ const playlist = (playlist: PlaylistSummary) => ({
},
});
export const playlistAlbumArtURL = (
bonobUrl: URLBuilder,
playlist: Playlist
) => {
const ids = uniq(playlist.entries.map((it) => it.album?.id).filter((it) => it));
if (ids.length == 0) {
return iconArtURI(bonobUrl, "error");
} else {
return bonobUrl.append({
pathname: `/art/album/${ids.slice(0, 9).join("&")}/size/180`
});
}
};
export const defaultAlbumArtURI = (bonobUrl: URLBuilder, album: AlbumSummary) =>
bonobUrl.append({ pathname: `/album/${album.id}/art/size/180` });
bonobUrl.append({ pathname: `/art/album/${album.id}/size/180` });
export const iconArtURI = (
bonobUrl: URLBuilder,
icon: ICON
) =>
bonobUrl.append({
pathname: `/icon/${icon}/size/legacy`
});
export const defaultArtistArtURI = (
bonobUrl: URLBuilder,
artist: ArtistSummary
) => bonobUrl.append({ pathname: `/artist/${artist.id}/art/size/180` });
) => bonobUrl.append({ pathname: `/art/artist/${artist.id}/size/180` });
export const album = (bonobUrl: URLBuilder, album: AlbumSummary) => ({
itemType: "album",
id: `album:${album.id}`,
artist: album.artistName,
artistId: album.artistId,
artistId: `artist:${album.artistId}`,
title: album.name,
albumArtURI: defaultAlbumArtURI(bonobUrl, album).href(),
canPlay: true,
@@ -333,11 +364,11 @@ function bindSmapiSoapServiceToExpress(
i8n: I8N
) {
const sonosSoap = new SonosSoap(bonobUrl, linkCodes);
const urlWithToken = (accessToken: string) =>
bonobUrl.append({
searchParams: {
"bonob-access-token": accessToken,
"bat": accessToken,
},
});
@@ -361,7 +392,7 @@ function bindSmapiSoapServiceToExpress(
getMediaURI: async (
{ id }: { id: string },
_,
soapyHeaders: SoapyHeaders,
soapyHeaders: SoapyHeaders
) =>
auth(musicService, accessTokens, soapyHeaders?.credentials)
.then(splitId(id))
@@ -381,22 +412,19 @@ function bindSmapiSoapServiceToExpress(
getMediaMetadata: async (
{ id }: { id: string },
_,
soapyHeaders: SoapyHeaders,
soapyHeaders: SoapyHeaders
) =>
auth(musicService, accessTokens, soapyHeaders?.credentials)
.then(splitId(id))
.then(async ({ musicLibrary, accessToken, typeId }) =>
musicLibrary.track(typeId!).then((it) => ({
getMediaMetadataResult: track(
urlWithToken(accessToken),
it
),
getMediaMetadataResult: track(urlWithToken(accessToken), it),
}))
),
search: async (
{ id, term }: { id: string; term: string },
_,
soapyHeaders: SoapyHeaders,
soapyHeaders: SoapyHeaders
) =>
auth(musicService, accessTokens, soapyHeaders?.credentials)
.then(splitId(id))
@@ -441,7 +469,7 @@ function bindSmapiSoapServiceToExpress(
}: // recursive,
{ id: string; index: number; count: number; recursive: boolean },
_,
soapyHeaders: SoapyHeaders,
soapyHeaders: SoapyHeaders
) =>
auth(musicService, accessTokens, soapyHeaders?.credentials)
.then(splitId(id))
@@ -462,7 +490,8 @@ function bindSmapiSoapServiceToExpress(
album(urlWithToken(accessToken), it)
),
relatedBrowse:
artist.similarArtists.filter(it => it.inLibrary).length > 0
artist.similarArtists.filter((it) => it.inLibrary)
.length > 0
? [
{
id: `relatedArtists:${artist.id}`,
@@ -530,16 +559,17 @@ function bindSmapiSoapServiceToExpress(
{ id: string; index: number; count: number; recursive: boolean },
_,
soapyHeaders: SoapyHeaders,
{ headers }: Pick<Request, 'headers'>
{ headers }: Pick<Request, "headers">
) =>
auth(musicService, accessTokens, soapyHeaders?.credentials)
.then(splitId(id))
.then(({ musicLibrary, accessToken, type, typeId }) => {
const paging = { _index: index, _count: count };
const lang = i8n((headers["accept-language"] || "en-US") as LANG);
const acceptLanguage = headers["accept-language"];
logger.debug(
`Fetching metadata type=${type}, typeId=${typeId}, lang=${lang}`
`Fetching metadata type=${type}, typeId=${typeId}, acceptLanguage=${acceptLanguage}`
);
const lang = i8n(...asLANGs(acceptLanguage));
const albums = (q: AlbumQuery): Promise<GetMetadataResponse> =>
musicLibrary.albums(q).then((result) => {
@@ -557,19 +587,22 @@ function bindSmapiSoapServiceToExpress(
return getMetadataResult({
mediaCollection: [
{
itemType: "container",
id: "artists",
title: lang("artists"),
albumArtURI: iconArtURI(bonobUrl, "artists").href(),
itemType: "container",
},
{
itemType: "albumList",
id: "albums",
title: lang("albums"),
albumArtURI: iconArtURI(bonobUrl, "albums").href(),
itemType: "albumList",
},
{
itemType: "playlist",
id: "playlists",
title: lang("playlists"),
albumArtURI: iconArtURI(bonobUrl, "playlists").href(),
itemType: "playlist",
attributes: {
readOnly: false,
userContent: true,
@@ -577,34 +610,49 @@ function bindSmapiSoapServiceToExpress(
},
},
{
itemType: "container",
id: "genres",
title: lang("genres"),
albumArtURI: iconArtURI(bonobUrl, "genres").href(),
itemType: "container",
},
{
itemType: "albumList",
id: "randomAlbums",
title: lang("random"),
albumArtURI: iconArtURI(bonobUrl, "random").href(),
itemType: "albumList",
},
{
itemType: "albumList",
id: "starredAlbums",
title: lang("starred"),
albumArtURI: iconArtURI(bonobUrl, "starred").href(),
itemType: "albumList",
},
{
itemType: "albumList",
id: "recentlyAdded",
title: lang("recentlyAdded"),
albumArtURI: iconArtURI(
bonobUrl,
"recentlyAdded"
).href(),
itemType: "albumList",
},
{
itemType: "albumList",
id: "recentlyPlayed",
title: lang("recentlyPlayed"),
albumArtURI: iconArtURI(
bonobUrl,
"recentlyPlayed"
).href(),
itemType: "albumList",
},
{
itemType: "albumList",
id: "mostPlayed",
title: lang("mostPlayed"),
albumArtURI: iconArtURI(
bonobUrl,
"mostPlayed"
).href(),
itemType: "albumList",
},
],
index: 0,
@@ -613,9 +661,21 @@ function bindSmapiSoapServiceToExpress(
case "search":
return getMetadataResult({
mediaCollection: [
{ itemType: "search", id: "artists", title: lang("artists") },
{ itemType: "search", id: "albums", title: lang("albums") },
{ itemType: "search", id: "tracks", title: lang("tracks") },
{
itemType: "search",
id: "artists",
title: lang("artists"),
},
{
itemType: "search",
id: "albums",
title: lang("albums"),
},
{
itemType: "search",
id: "tracks",
title: lang("tracks"),
},
],
index: 0,
total: 3,
@@ -632,7 +692,7 @@ function bindSmapiSoapServiceToExpress(
});
case "albums": {
return albums({
type: "alphabeticalByArtist",
type: "alphabeticalByName",
...paging,
});
}
@@ -673,7 +733,9 @@ function bindSmapiSoapServiceToExpress(
.then(slice2(paging))
.then(([page, total]) =>
getMetadataResult({
mediaCollection: page.map(genre),
mediaCollection: page.map((it) =>
genre(bonobUrl, it)
),
index: paging._index,
total,
})
@@ -681,14 +743,23 @@ function bindSmapiSoapServiceToExpress(
case "playlists":
return musicLibrary
.playlists()
.then((it) =>
Promise.all(
it.map((playlist) =>
musicLibrary.playlist(playlist.id)
)
)
)
.then(slice2(paging))
.then(([page, total]) =>
getMetadataResult({
mediaCollection: page.map(playlist),
.then(([page, total]) => {
return getMetadataResult({
mediaCollection: page.map((it) =>
playlist(urlWithToken(accessToken), it)
),
index: paging._index,
total,
})
);
});
});
case "playlist":
return musicLibrary
.playlist(typeId!)
@@ -721,7 +792,9 @@ function bindSmapiSoapServiceToExpress(
return musicLibrary
.artist(typeId!)
.then((artist) => artist.similarArtists)
.then(similarArtists => similarArtists.filter(it => it.inLibrary))
.then((similarArtists) =>
similarArtists.filter((it) => it.inLibrary)
)
.then(slice2(paging))
.then(([page, total]) => {
return getMetadataResult({
@@ -752,7 +825,7 @@ function bindSmapiSoapServiceToExpress(
createContainer: async (
{ title, seedId }: { title: string; seedId: string | undefined },
_,
soapyHeaders: SoapyHeaders,
soapyHeaders: SoapyHeaders
) =>
auth(musicService, accessTokens, soapyHeaders?.credentials)
.then(({ musicLibrary }) =>
@@ -778,7 +851,7 @@ function bindSmapiSoapServiceToExpress(
deleteContainer: async (
{ id }: { id: string },
_,
soapyHeaders: SoapyHeaders,
soapyHeaders: SoapyHeaders
) =>
auth(musicService, accessTokens, soapyHeaders?.credentials)
.then(({ musicLibrary }) => musicLibrary.deletePlaylist(id))
@@ -786,7 +859,7 @@ function bindSmapiSoapServiceToExpress(
addToContainer: async (
{ id, parentId }: { id: string; parentId: string },
_,
soapyHeaders: SoapyHeaders,
soapyHeaders: SoapyHeaders
) =>
auth(musicService, accessTokens, soapyHeaders?.credentials)
.then(splitId(id))
@@ -797,7 +870,7 @@ function bindSmapiSoapServiceToExpress(
removeFromContainer: async (
{ id, indices }: { id: string; indices: string },
_,
soapyHeaders: SoapyHeaders,
soapyHeaders: SoapyHeaders
) =>
auth(musicService, accessTokens, soapyHeaders?.credentials)
.then(splitId(id))
@@ -820,7 +893,7 @@ function bindSmapiSoapServiceToExpress(
setPlayedSeconds: async (
{ id, seconds }: { id: string; seconds: string },
_,
soapyHeaders: SoapyHeaders,
soapyHeaders: SoapyHeaders
) =>
auth(musicService, accessTokens, soapyHeaders?.credentials)
.then(splitId(id))

View File

@@ -7,10 +7,11 @@ import logger from "./logger";
import { SOAP_PATH, STRINGS_ROUTE, PRESENTATION_MAP_ROUTE } from "./smapi";
import qs from "querystring";
import { URLBuilder } from "./url_builder";
import { LANG } from "./i8n";
export const SONOS_LANG = ["en-US", "da-DK", "de-DE", "es-ES", "fr-FR", "it-IT", "ja-JP", "nb-NO", "nl-NL", "pt-BR", "sv-SE", "zh-CN"]
export const SONOS_LANG: LANG[] = ["en-US", "da-DK", "de-DE", "es-ES", "fr-FR", "it-IT", "ja-JP", "nb-NO", "nl-NL", "pt-BR", "sv-SE", "zh-CN"]
export const PRESENTATION_AND_STRINGS_VERSION = "19";
export const PRESENTATION_AND_STRINGS_VERSION = "21";
// NOTE: manifest requires https for the URL,
// otherwise you will get an error trying to register
@@ -162,7 +163,7 @@ export function autoDiscoverySonos(sonosSeedHost?: string): Sonos {
}
})
.catch((e) => {
logger.error(`Failed looking for sonos devices ${e}`);
logger.error(`Failed looking for sonos devices`, { cause: e });
return [];
});
};

7
src/utils.ts Normal file
View File

@@ -0,0 +1,7 @@
export function takeWithRepeats<T>(things:T[], count: number) {
const result = [];
for(let i = 0; i < count; i++) {
result.push(things[i % things.length])
}
return result;
}

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,70 @@ 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");

View File

@@ -1,4 +1,4 @@
import i8n, { langs, LANG, KEY, keys, asLANGs } from "../src/i8n";
import i8n, { langs, LANG, KEY, keys, asLANGs, SUPPORTED_LANG } from "../src/i8n";
describe("i8n", () => {
describe("asLANGs", () => {
@@ -41,7 +41,7 @@ describe("i8n", () => {
describe("validity of translations", () => {
it("all langs should have same keys as US", () => {
langs().forEach((l) => {
expect(keys(l as LANG)).toEqual(keys("en-US"));
expect(keys(l as SUPPORTED_LANG)).toEqual(keys("en-US"));
});
});
});
@@ -54,79 +54,129 @@ describe("i8n", () => {
describe("fetching translations", () => {
describe("with a single lang", () => {
describe("and there is no templating", () => {
it("should return the value", () => {
expect(i8n("foo")("en-US")("artists")).toEqual("Artists");
expect(i8n("foo")("nl-NL")("artists")).toEqual("Artiesten");
describe("and the lang is not represented", () => {
describe("and there is no templating", () => {
it("should return the en-US value", () => {
expect(i8n("foo")("en-AU" as LANG)("artists")).toEqual("Artists");
});
});
describe("and there is templating of the service name", () => {
it("should return the en-US value templated", () => {
expect(i8n("service123")("en-AU" as LANG)("AppLinkMessage")).toEqual(
"Linking sonos with service123"
);
});
});
});
describe("and there is templating of the service name", () => {
it("should return the value", () => {
expect(i8n("service123")("en-US")("AppLinkMessage")).toEqual(
"Linking sonos with service123"
);
expect(i8n("service456")("nl-NL")("AppLinkMessage")).toEqual(
"Sonos koppelen aan service456"
);
describe("and the lang is represented", () => {
describe("and there is no templating", () => {
it("should return the value", () => {
expect(i8n("foo")("en-US")("artists")).toEqual("Artists");
expect(i8n("foo")("nl-NL")("artists")).toEqual("Artiesten");
});
});
describe("and there is templating of the service name", () => {
it("should return the value", () => {
expect(i8n("service123")("en-US")("AppLinkMessage")).toEqual(
"Linking sonos with service123"
);
expect(i8n("service456")("nl-NL")("AppLinkMessage")).toEqual(
"Sonos koppelen aan service456"
);
});
});
});
});
describe("with multiple langs", () => {
describe("and the first lang is a match", () => {
function itShouldReturn(serviceName: string, langs: string[], key: KEY, expected: string) {
it(`should return '${expected}' for the serviceName=${serviceName}, langs=${langs}`, () => {
expect(i8n(serviceName)(...langs)(key)).toEqual(expected);
});
};
describe("and the first lang is an exact match", () => {
describe("and there is no templating", () => {
it("should return the value for the first lang", () => {
expect(i8n("foo")("en-US", "nl-NL")("artists")).toEqual("Artists");
expect(i8n("foo")("nl-NL", "en-US")("artists")).toEqual("Artiesten");
});
itShouldReturn("foo", ["en-US", "nl-NL"], "artists", "Artists");
itShouldReturn("foo", ["nl-NL", "en-US"], "artists", "Artiesten");
});
describe("and there is templating of the service name", () => {
it("should return the value for the firt lang", () => {
expect(i8n("service123")("en-US", "nl-NL")("AppLinkMessage")).toEqual(
"Linking sonos with service123"
);
expect(i8n("service456")("nl-NL", "en-US")("AppLinkMessage")).toEqual(
"Sonos koppelen aan service456"
);
});
itShouldReturn("service123", ["en-US", "nl-NL"], "AppLinkMessage", "Linking sonos with service123");
itShouldReturn("service456", ["nl-NL", "en-US"], "AppLinkMessage", "Sonos koppelen aan service456");
});
});
describe("and the first lang is not a match, however there is a match in the provided langs", () => {
describe("and the first lang is a case insensitive match", () => {
describe("and there is no templating", () => {
it("should return the value for the first lang", () => {
expect(i8n("foo")("something", "en-US", "nl-NL")("artists")).toEqual("Artists");
expect(i8n("foo")("something", "nl-NL", "en-US")("artists")).toEqual("Artiesten");
});
itShouldReturn("foo", ["en-us", "nl-NL"], "artists", "Artists");
itShouldReturn("foo", ["nl-nl", "en-US"], "artists", "Artiesten");
});
describe("and there is templating of the service name", () => {
it("should return the value for the firt lang", () => {
expect(i8n("service123")("something", "en-US", "nl-NL")("AppLinkMessage")).toEqual(
"Linking sonos with service123"
);
expect(i8n("service456")("something", "nl-NL", "en-US")("AppLinkMessage")).toEqual(
"Sonos koppelen aan service456"
);
});
itShouldReturn("service123", ["en-us", "nl-NL"], "AppLinkMessage", "Linking sonos with service123");
itShouldReturn("service456", ["nl-nl", "en-US"], "AppLinkMessage", "Sonos koppelen aan service456");
});
});
describe("and the first lang is a lang match without region", () => {
describe("and there is no templating", () => {
itShouldReturn("foo", ["en", "nl-NL"], "artists", "Artists");
itShouldReturn("foo", ["nl", "en-US"], "artists", "Artiesten");
});
describe("and there is templating of the service name", () => {
itShouldReturn("service123", ["en", "nl-NL"], "AppLinkMessage", "Linking sonos with service123");
itShouldReturn("service456", ["nl", "en-US"], "AppLinkMessage", "Sonos koppelen aan service456");
});
});
describe("and the first lang is not a match, however there is an exact match in the provided langs", () => {
describe("and there is no templating", () => {
itShouldReturn("foo", ["something", "en-US", "nl-NL"], "artists", "Artists")
itShouldReturn("foo", ["something", "nl-NL", "en-US"], "artists", "Artiesten")
});
describe("and there is templating of the service name", () => {
itShouldReturn("service123", ["something", "en-US", "nl-NL"], "AppLinkMessage", "Linking sonos with service123")
itShouldReturn("service456", ["something", "nl-NL", "en-US"], "AppLinkMessage", "Sonos koppelen aan service456")
});
});
describe("and the first lang is not a match, however there is a case insensitive match in the provided langs", () => {
describe("and there is no templating", () => {
itShouldReturn("foo", ["something", "en-us", "nl-nl"], "artists", "Artists")
itShouldReturn("foo", ["something", "nl-nl", "en-us"], "artists", "Artiesten")
});
describe("and there is templating of the service name", () => {
itShouldReturn("service123", ["something", "en-us", "nl-nl"], "AppLinkMessage", "Linking sonos with service123")
itShouldReturn("service456", ["something", "nl-nl", "en-us"], "AppLinkMessage", "Sonos koppelen aan service456")
});
});
describe("and the first lang is not a match, however there is a lang match without region", () => {
describe("and there is no templating", () => {
itShouldReturn("foo", ["something", "en", "nl-nl"], "artists", "Artists")
itShouldReturn("foo", ["something", "nl", "en-us"], "artists", "Artiesten")
});
describe("and there is templating of the service name", () => {
itShouldReturn("service123", ["something", "en", "nl-nl"], "AppLinkMessage", "Linking sonos with service123")
itShouldReturn("service456", ["something", "nl", "en-us"], "AppLinkMessage", "Sonos koppelen aan service456")
});
});
describe("and no lang is a match", () => {
describe("and there is no templating", () => {
it("should return the value for the first lang", () => {
expect(i8n("foo")("something", "something2")("artists")).toEqual("Artists");
});
itShouldReturn("foo", ["something", "something2"], "artists", "Artists")
});
describe("and there is templating of the service name", () => {
it("should return the value for the firt lang", () => {
expect(i8n("service123")("something", "something2")("AppLinkMessage")).toEqual(
"Linking sonos with service123"
);
});
itShouldReturn("service123", ["something", "something2"], "AppLinkMessage", "Linking sonos with service123")
});
});
});
@@ -139,20 +189,5 @@ describe("i8n", () => {
});
});
describe("when the lang is not represented", () => {
describe("and there is no templating", () => {
it("should return the en-US value", () => {
expect(i8n("foo")("en-AU" as LANG)("artists")).toEqual("Artists");
});
});
describe("and there is templating of the service name", () => {
it("should return the en-US value templated", () => {
expect(i8n("service123")("en-AU" as LANG)("AppLinkMessage")).toEqual(
"Linking sonos with service123"
);
});
});
});
});
});

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

@@ -0,0 +1,836 @@
import dayjs from "dayjs";
import libxmljs from "libxmljs2";
import {
contains,
containsWord,
eq,
HOLI_COLORS,
Icon,
iconForGenre,
SvgIcon,
IconFeatures,
IconSpec,
ICONS,
Transformer,
transform,
maybeTransform,
festivals,
allOf,
features,
STAR_WARS,
} 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="path1"/>
<path d="path2" fill="none" stroke="#000"/>
<path d="path3"/>
</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="path1"/>
<path d="path2" fill="none" stroke="#000"/>
<path d="path3"/>
</svg>
`;
describe("with no features", () => {
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({ features: { 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="path1"/>
<path d="path2" fill="none" stroke="#000"/>
<path d="path3"/>
</svg>
`)
);
});
});
describe("when the viewPort is of size 0 0 128 128", () => {
it("should resize the viewPort", () => {
expect(
new SvgIcon(svgIcon128)
.with({ features: { 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="path1"/>
<path d="path2" fill="none" stroke="#000"/>
<path d="path3"/>
</svg>
`)
);
});
});
});
describe("of 0%", () => {
it("should do nothing", () => {
expect(
new SvgIcon(svgIcon24)
.with({ features: { 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({ features: { 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" fill="red"/>
<path d="path1"/>
<path d="path2" fill="none" stroke="#000"/>
<path d="path3"/>
</svg>
`)
);
});
});
describe("with a viewPort increase", () => {
it("should add a rectangle the same size as the original viewPort", () => {
expect(
new SvgIcon(svgIcon24)
.with({
features: {
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" fill="pink"/>
<path d="path1"/>
<path d="path2" fill="none" stroke="#000"/>
<path d="path3"/>
</svg>
`)
);
});
});
describe("of undefined", () => {
it("should not do anything", () => {
expect(
new SvgIcon(svgIcon24)
.with({ features: { 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="path1"/>
<path d="path2" fill="none" stroke="#000"/>
<path d="path3"/>
</svg>
`)
);
});
});
describe("multiple times", () => {
it("should use the most recent", () => {
expect(
new SvgIcon(svgIcon24)
.with({ features: { backgroundColor: "green" } })
.with({ features: { 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" fill="red"/>
<path d="path1"/>
<path d="path2" fill="none" stroke="#000"/>
<path d="path3"/>
</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({ features: { 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="path1" fill="red"/>
<path d="path2" fill="none" stroke="red"/>
<path d="path3" 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({
features: {
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="path1" fill="pink"/>
<path d="path2" fill="none" stroke="pink"/>
<path d="path3" fill="pink"/>
</svg>
`)
);
});
});
describe("of undefined", () => {
it("should not do anything", () => {
expect(
new SvgIcon(svgIcon24)
.with({ features: { 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="path1"/>
<path d="path2" fill="none" stroke="#000"/>
<path d="path3"/>
</svg>
`)
);
});
});
describe("mutliple times", () => {
it("should use the most recent", () => {
expect(
new SvgIcon(svgIcon24)
.with({ features: { foregroundColor: "blue" } })
.with({ features: { 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="path1" fill="red"/>
<path d="path2" fill="none" stroke="red"/>
<path d="path3" fill="red"/>
</svg>
`)
);
});
});
});
describe("swapping the svg", () => {
describe("with no other changes", () => {
it("should swap out the svg, but maintain the IconFeatures", () => {
expect(
new SvgIcon(svgIcon24, {
foregroundColor: "blue",
backgroundColor: "green",
viewPortIncreasePercent: 50,
})
.with({ svg: svgIcon128 })
.toString()
).toEqual(
xmlTidy(`<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-21 -21 170 170">
<rect x="-21" y="-21" width="191" height="191" fill="green"/>
<path d="path1" fill="blue"/>
<path d="path2" fill="none" stroke="blue"/>
<path d="path3" fill="blue"/>
</svg>
`)
);
});
});
describe("with no other changes", () => {
it("should swap out the svg, but maintain the IconFeatures", () => {
expect(
new SvgIcon(svgIcon24, {
foregroundColor: "blue",
backgroundColor: "green",
viewPortIncreasePercent: 50,
})
.with({
svg: svgIcon128,
features: {
foregroundColor: "pink",
backgroundColor: "red",
viewPortIncreasePercent: 0,
},
})
.toString()
).toEqual(
xmlTidy(`<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128">
<rect x="0" y="0" width="128" height="128" fill="red"/>
<path d="path1" fill="pink"/>
<path d="path2" fill="none" stroke="pink"/>
<path d="path3" fill="pink"/>
</svg>
`)
);
});
});
});
});
class DummyIcon implements Icon {
svg: string;
features: Partial<IconFeatures>;
constructor(svg: string, features: Partial<IconFeatures>) {
this.svg = svg;
this.features = features;
}
public apply = (transformer: Transformer): Icon => transformer(this);
public with = ({ svg, features }: Partial<IconSpec>) => {
return new DummyIcon(svg || this.svg, {
...this.features,
...(features || {}),
});
};
public toString = () =>
JSON.stringify({ svg: this.svg, features: this.features });
}
describe("transform", () => {
describe("when the features contains no svg", () => {
it("should apply the overriding transform ontop of the requested transform", () => {
const original = new DummyIcon("original", {
backgroundColor: "black",
foregroundColor: "black",
});
const result = original
.with({
features: {
viewPortIncreasePercent: 100,
foregroundColor: "blue",
backgroundColor: "blue",
},
})
.apply(
transform({
features: {
foregroundColor: "override1",
backgroundColor: "override2",
},
})
) as DummyIcon;
expect(result.svg).toEqual("original");
expect(result.features).toEqual({
viewPortIncreasePercent: 100,
foregroundColor: "override1",
backgroundColor: "override2",
});
});
});
describe("when the features contains an svg", () => {
it("should use the newly provided svg", () => {
const original = new DummyIcon("original", {
backgroundColor: "black",
foregroundColor: "black",
});
const result = original
.with({
features: {
viewPortIncreasePercent: 100,
foregroundColor: "blue",
backgroundColor: "blue",
},
})
.apply(
transform({
svg: "new",
})
) as DummyIcon;
expect(result.svg).toEqual("new");
expect(result.features).toEqual({
viewPortIncreasePercent: 100,
foregroundColor: "blue",
backgroundColor: "blue",
});
});
});
});
describe("features", () => {
it("should apply the features", () => {
const original = new DummyIcon("original", {
backgroundColor: "black",
foregroundColor: "black",
});
const result = original.apply(
features({
viewPortIncreasePercent: 100,
foregroundColor: "blue",
backgroundColor: "blue",
})
) as DummyIcon;
expect(result.features).toEqual({
viewPortIncreasePercent: 100,
foregroundColor: "blue",
backgroundColor: "blue",
});
});
});
describe("allOf", () => {
it("should apply all composed transforms", () => {
const result = new DummyIcon("original", {
foregroundColor: "black",
backgroundColor: "black",
viewPortIncreasePercent: 0,
}).apply(
allOf(
(icon: Icon) => icon.with({ svg: "foo" }),
(icon: Icon) => icon.with({ features: { backgroundColor: "red" } }),
(icon: Icon) => icon.with({ features: { foregroundColor: "blue" } })
)
) as DummyIcon;
expect(result.svg).toEqual("foo");
expect(result.features).toEqual({
foregroundColor: "blue",
backgroundColor: "red",
viewPortIncreasePercent: 0,
});
});
});
describe("maybeTransform", () => {
describe("when the rule matches", () => {
const original = new DummyIcon("original", {
backgroundColor: "black",
foregroundColor: "black",
});
describe("transforming the color", () => {
const result = original
.with({
features: {
viewPortIncreasePercent: 99,
backgroundColor: "shouldBeIgnored",
foregroundColor: "shouldBeIgnored",
},
})
.apply(
maybeTransform(
() => true,
transform({
features: {
backgroundColor: "blue",
foregroundColor: "red",
},
})
)
) as DummyIcon;
describe("with", () => {
it("should be the with of the underlieing icon with the overriden colors", () => {
expect(result.svg).toEqual("original");
expect(result.features).toEqual({
viewPortIncreasePercent: 99,
backgroundColor: "blue",
foregroundColor: "red",
});
});
});
});
describe("overriding all options", () => {
const result = original
.with({
features: {
viewPortIncreasePercent: 99,
backgroundColor: "shouldBeIgnored",
foregroundColor: "shouldBeIgnored",
},
})
.apply(
maybeTransform(
() => true,
transform({
features: {
backgroundColor: "blue",
foregroundColor: "red",
},
})
)
) as DummyIcon;
describe("with", () => {
it("should be the with of the underlieing icon with the overriden colors", () => {
expect(result.features).toEqual({
viewPortIncreasePercent: 99,
backgroundColor: "blue",
foregroundColor: "red",
});
});
});
});
});
describe("when the rule doesnt match", () => {
const original = new DummyIcon("original", {
backgroundColor: "black",
foregroundColor: "black",
});
const result = original
.with({
features: {
viewPortIncreasePercent: 88,
backgroundColor: "shouldBeUsed",
foregroundColor: "shouldBeUsed",
},
})
.apply(
maybeTransform(
() => false,
transform({
features: { backgroundColor: "blue", foregroundColor: "red" },
})
)
) as DummyIcon;
describe("with", () => {
it("should use the provided features", () => {
expect(result.features).toEqual({
viewPortIncreasePercent: 88,
backgroundColor: "shouldBeUsed",
foregroundColor: "shouldBeUsed",
});
});
});
});
});
describe("festivals", () => {
const original = new DummyIcon("original", {
backgroundColor: "black",
foregroundColor: "black",
});
let now = dayjs();
const clock = { now: () => now };
describe("on a day that isn't festive", () => {
beforeEach(() => {
now = dayjs("2022/10/12");
});
it("should use the given colors", () => {
const result = original
.apply(
features({
viewPortIncreasePercent: 88,
backgroundColor: "shouldBeUsed",
foregroundColor: "shouldBeUsed",
})
)
.apply(festivals(clock)) as DummyIcon;
expect(result.toString()).toEqual(
new DummyIcon("original", {
backgroundColor: "shouldBeUsed",
foregroundColor: "shouldBeUsed",
viewPortIncreasePercent: 88,
}).toString()
);
});
});
describe("on christmas day", () => {
beforeEach(() => {
now = dayjs("2022/12/25");
});
it("should use the christmas theme colors", () => {
const result = original.apply(
allOf(
features({
viewPortIncreasePercent: 25,
backgroundColor: "shouldNotBeUsed",
foregroundColor: "shouldNotBeUsed",
}),
festivals(clock)
)
) as DummyIcon;
expect(result.svg).toEqual(ICONS.christmas.svg);
expect(result.features).toEqual({
backgroundColor: "green",
foregroundColor: "red",
viewPortIncreasePercent: 25,
});
});
});
describe("on halloween", () => {
beforeEach(() => {
now = dayjs("2022/10/31");
});
it("should use the given colors", () => {
const result = original
.apply(
features({
viewPortIncreasePercent: 12,
backgroundColor: "shouldNotBeUsed",
foregroundColor: "shouldNotBeUsed",
})
)
.apply(festivals(clock)) as DummyIcon;
expect(result.svg).toEqual(ICONS.halloween.svg);
expect(result.features).toEqual({
viewPortIncreasePercent: 12,
backgroundColor: "black",
foregroundColor: "orange",
});
});
});
describe("on may 4", () => {
beforeEach(() => {
now = dayjs("2022/5/4");
});
it("should use the undefined colors, so no color", () => {
const result = original
.apply(
features({
viewPortIncreasePercent: 12,
backgroundColor: "shouldNotBeUsed",
foregroundColor: "shouldNotBeUsed",
})
)
.apply(festivals(clock)) as DummyIcon;
expect(STAR_WARS.map(it => it.svg)).toContain(result.svg);
expect(result.features).toEqual({
viewPortIncreasePercent: 12,
backgroundColor: undefined,
foregroundColor: undefined,
});
});
});
describe("on cny", () => {
describe("2022", () => {
beforeEach(() => {
now = dayjs("2022/02/01");
});
it("should use the cny theme", () => {
const result = original
.apply(
features({
viewPortIncreasePercent: 12,
backgroundColor: "shouldNotBeUsed",
foregroundColor: "shouldNotBeUsed",
})
)
.apply(festivals(clock)) as DummyIcon;
expect(result.svg).toEqual(ICONS.yoTiger.svg);
expect(result.features).toEqual({
viewPortIncreasePercent: 12,
backgroundColor: "red",
foregroundColor: "yellow",
});
});
});
describe("2023", () => {
beforeEach(() => {
now = dayjs("2023/01/22");
});
it("should use the cny theme", () => {
const result = original
.apply(
features({
viewPortIncreasePercent: 12,
backgroundColor: "shouldNotBeUsed",
foregroundColor: "shouldNotBeUsed",
})
)
.apply(festivals(clock)) as DummyIcon;
expect(result.svg).toEqual(ICONS.yoRabbit.svg);
expect(result.features).toEqual({
viewPortIncreasePercent: 12,
backgroundColor: "red",
foregroundColor: "yellow",
});
});
});
describe("2024", () => {
beforeEach(() => {
now = dayjs("2024/02/10");
});
it("should use the cny theme", () => {
const result = original
.apply(
features({
viewPortIncreasePercent: 12,
backgroundColor: "shouldNotBeUsed",
foregroundColor: "shouldNotBeUsed",
})
)
.apply(festivals(clock)) as DummyIcon;
expect(result.svg).toEqual(ICONS.yoDragon.svg);
expect(result.features).toEqual({
viewPortIncreasePercent: 12,
backgroundColor: "red",
foregroundColor: "yellow",
});
});
});
});
describe("on holi", () => {
beforeEach(() => {
now = dayjs("2022/03/18");
});
it("should use the given colors", () => {
const result = original
.apply(
features({
viewPortIncreasePercent: 12,
backgroundColor: "shouldNotBeUsed",
foregroundColor: "shouldNotBeUsed",
})
)
.apply(festivals(clock)) as DummyIcon;
expect(result.features.viewPortIncreasePercent).toEqual(12);
expect(HOLI_COLORS.includes(result.features.backgroundColor!)).toEqual(
true
);
expect(HOLI_COLORS.includes(result.features.foregroundColor!)).toEqual(
true
);
expect(result.features.backgroundColor).not.toEqual(
result.features.foregroundColor
);
});
});
});
describe("eq", () => {
it("should be true when ===", () => {
expect(eq("Foo")("foo")).toEqual(true);
});
it("should be false when not ===", () => {
expect(eq("Foo")("bar")).toEqual(false);
});
});
describe("contains", () => {
it("should be true word is a substring", () => {
expect(contains("Foo")("some foo bar")).toEqual(true);
});
it("should be false when not ===", () => {
expect(contains("Foo")("some bar")).toEqual(false);
});
});
describe("containsWord", () => {
it("should be true word is a substring with space delim", () => {
expect(containsWord("Foo")("some foo bar")).toEqual(true);
});
it("should be true word is a substring with hyphen delim", () => {
expect(containsWord("Foo")("some----foo-bar")).toEqual(true);
});
it("should be false when not ===", () => {
expect(containsWord("Foo")("somefoobar")).toEqual(false);
});
});
describe("iconForGenre", () => {
[
["Acid House", "mushroom"],
["African", "african"],
["Alternative Rock", "rock"],
["Americana", "americana"],
["Anti-Folk", "guitar"],
["Audio-Book", "book"],
["Australian Hip Hop", "oz"],
["Rap", "rap"],
["Hip Hop", "hipHop"],
["Hip-Hop", "hipHop"],
["Metal", "metal"],
["Horrorcore", "horror"],
["Punk", "punk"],
["blah", "music"],
].forEach(([genre, expected]) => {
describe(`a genre of ${genre}`, () => {
it(`should have an icon of ${expected}`, () => {
const name = iconForGenre(genre!)!;
expect(name).toEqual(expected);
});
});
describe(`a genre of ${genre!.toLowerCase()}`, () => {
it(`should have an icon of ${expected}`, () => {
const name = iconForGenre(genre!)!;
expect(name).toEqual(expected);
});
});
});
});

View File

@@ -16,6 +16,7 @@ import {
HIP_HOP,
SKA,
} from "./builders";
import _ from "underscore";
describe("InMemoryMusicService", () => {
const service = new InMemoryMusicService();
@@ -210,6 +211,7 @@ describe("InMemoryMusicService", () => {
const artist3_album2 = anAlbum({ genre: POP });
const artist1 = anArtist({
name: "artist1",
albums: [
artist1_album1,
artist1_album2,
@@ -218,8 +220,8 @@ describe("InMemoryMusicService", () => {
artist1_album5,
],
});
const artist2 = anArtist({ albums: [artist2_album1] });
const artist3 = anArtist({ albums: [artist3_album1, artist3_album2] });
const artist2 = anArtist({ name: "artist2", albums: [artist2_album1] });
const artist3 = anArtist({ name: "artist3", albums: [artist3_album1, artist3_album2] });
const artistWithNoAlbums = anArtist({ albums: [] });
const allAlbums = [artist1, artist2, artist3, artistWithNoAlbums].flatMap(
@@ -265,29 +267,48 @@ describe("InMemoryMusicService", () => {
describe("fetching multiple albums", () => {
describe("with no filtering", () => {
describe("fetching all on one page", () => {
it("should return all the albums for all the artists", async () => {
expect(
await musicLibrary.albums({
_index: 0,
_count: 100,
type: "alphabeticalByArtist",
})
).toEqual({
results: [
albumToAlbumSummary(artist1_album1),
albumToAlbumSummary(artist1_album2),
albumToAlbumSummary(artist1_album3),
albumToAlbumSummary(artist1_album4),
albumToAlbumSummary(artist1_album5),
albumToAlbumSummary(artist2_album1),
albumToAlbumSummary(artist3_album1),
albumToAlbumSummary(artist3_album2),
],
total: totalAlbumCount,
describe("alphabeticalByArtist", () => {
it("should return all the albums for all the artists", async () => {
expect(
await musicLibrary.albums({
_index: 0,
_count: 100,
type: "alphabeticalByArtist",
})
).toEqual({
results: [
albumToAlbumSummary(artist1_album1),
albumToAlbumSummary(artist1_album2),
albumToAlbumSummary(artist1_album3),
albumToAlbumSummary(artist1_album4),
albumToAlbumSummary(artist1_album5),
albumToAlbumSummary(artist2_album1),
albumToAlbumSummary(artist3_album1),
albumToAlbumSummary(artist3_album2),
],
total: totalAlbumCount,
});
});
});
describe("alphabeticalByName", () => {
it("should return all the albums for all the artists", async () => {
expect(
await musicLibrary.albums({
_index: 0,
_count: 100,
type: "alphabeticalByName",
})
).toEqual({
results:
_.sortBy(allAlbums, 'name').map(albumToAlbumSummary),
total: totalAlbumCount,
});
});
});
});
describe("fetching a page", () => {

View File

@@ -77,7 +77,9 @@ export class InMemoryMusicService implements MusicService {
switch (q.type) {
case "alphabeticalByArtist":
return artist2Album;
case "byGenre":
case "alphabeticalByName":
return artist2Album.sort((a, b) => a.album.name.localeCompare(b.album.name));
case "byGenre":
return artist2Album.filter(
(it) => it.album.genre?.id === q.genre
);

File diff suppressed because it is too large Load Diff

View File

@@ -9,6 +9,7 @@ import {
GetMetadataResponse,
} from "../src/smapi";
import {
aDevice,
BLONDIE,
BOB_MARLEY,
getAppLinkMessage,
@@ -19,7 +20,7 @@ import { InMemoryMusicService } from "./in_memory_music_service";
import { InMemoryLinkCodes } from "../src/link_codes";
import { Credentials } from "../src/music_service";
import makeServer from "../src/server";
import { Service, bonobService, SONOS_DISABLED } from "../src/sonos";
import { Service, bonobService, Sonos } from "../src/sonos";
import supersoap from "./supersoap";
import url, { URLBuilder } from "../src/url_builder";
@@ -138,8 +139,6 @@ class SonosDriver {
return m![1]!;
});
console.log(`posting to action ${action}`);
return request(this.server)
.post(action)
.type("form")
@@ -173,6 +172,17 @@ describe("scenarios", () => {
);
const linkCodes = new InMemoryLinkCodes();
const fakeSonos: Sonos = {
devices: () => Promise.resolve([aDevice({
name: "device1",
ip: "172.0.0.1",
port: 4301,
})]),
services: () => Promise.resolve([]),
remove: () => Promise.resolve(true),
register: () => Promise.resolve(true),
};
beforeEach(() => {
musicService.clear();
linkCodes.clear();
@@ -245,7 +255,7 @@ describe("scenarios", () => {
...BLONDIE.albums,
...BOB_MARLEY.albums,
...MADONNA.albums,
].map((it) => it.name)
].map((it) => it.name).sort()
)
);
});
@@ -257,11 +267,13 @@ describe("scenarios", () => {
const bonobUrl = url("http://localhost:1234");
const bonob = bonobService("bonob", 123, bonobUrl);
const server = makeServer(
SONOS_DISABLED,
fakeSonos,
bonob,
bonobUrl,
musicService,
linkCodes
{
linkCodes: () => linkCodes
}
);
const sonosDriver = new SonosDriver(server, bonobUrl, bonob);
@@ -273,11 +285,13 @@ describe("scenarios", () => {
const bonobUrl = url("http://localhost:1234/");
const bonob = bonobService("bonob", 123, bonobUrl);
const server = makeServer(
SONOS_DISABLED,
fakeSonos,
bonob,
bonobUrl,
musicService,
linkCodes
{
linkCodes: () => linkCodes
}
);
const sonosDriver = new SonosDriver(server, bonobUrl, bonob);
@@ -289,11 +303,13 @@ describe("scenarios", () => {
const bonobUrl = url("http://localhost:1234/context-for-bonob");
const bonob = bonobService("bonob", 123, bonobUrl);
const server = makeServer(
SONOS_DISABLED,
fakeSonos,
bonob,
bonobUrl,
musicService,
linkCodes
{
linkCodes: () => linkCodes
}
);
const sonosDriver = new SonosDriver(server, bonobUrl, bonob);

File diff suppressed because it is too large Load Diff

View File

@@ -21,6 +21,8 @@ import {
defaultAlbumArtURI,
defaultArtistArtURI,
searchResult,
iconArtURI,
playlistAlbumArtURL,
} from "../src/smapi";
import {
@@ -42,10 +44,12 @@ import {
albumToAlbumSummary,
artistToArtistSummary,
MusicService,
playlistToPlaylistSummary,
} from "../src/music_service";
import { AccessTokens } from "../src/access_tokens";
import dayjs from "dayjs";
import url from "../src/url_builder";
import { iconForGenre } from "../src/icon";
const parseXML = (value: string) => new DOMParserImpl().parseFromString(value);
@@ -54,82 +58,107 @@ describe("service config", () => {
const bonobWithContextPath = url("http://localhost:5678/some-context-path");
[bonobWithNoContextPath, bonobWithContextPath].forEach((bonobUrl) => {
const server = makeServer(
SONOS_DISABLED,
aService({ name: "music land" }),
bonobUrl,
new InMemoryMusicService()
);
describe(bonobUrl.href(), () => {
const server = makeServer(
SONOS_DISABLED,
aService({ name: "music land" }),
bonobUrl,
new InMemoryMusicService()
);
const stringsUrl = bonobUrl.append({ pathname: STRINGS_ROUTE });
const presentationUrl = bonobUrl.append({
pathname: PRESENTATION_MAP_ROUTE,
});
describe(`${stringsUrl}`, () => {
async function fetchStringsXml() {
const res = await request(server).get(stringsUrl.path()).send();
expect(res.status).toEqual(200);
// removing the sonos xml ns as makes xpath queries with xpath-ts painful
return parseXML(
res.text.replace('xmlns="http://sonos.com/sonosapi"', "")
);
}
it("should return xml for the strings", async () => {
const xml = await fetchStringsXml();
const sonosString = (id: string, lang: string) =>
xpath.select(
`string(/stringtables/stringtable[@xml:lang="${lang}"]/string[@stringId="${id}"])`,
xml
);
expect(sonosString("AppLinkMessage", "en-US")).toEqual(
"Linking sonos with music land"
);
expect(sonosString("AppLinkMessage", "nl-NL")).toEqual(
"Sonos koppelen aan music land"
);
// no fr-FR translation, so use en-US
expect(sonosString("AppLinkMessage", "fr-FR")).toEqual(
"Linking sonos with music land"
);
const stringsUrl = bonobUrl.append({ pathname: STRINGS_ROUTE });
const presentationUrl = bonobUrl.append({
pathname: PRESENTATION_MAP_ROUTE,
});
it("should return a section for all sonos supported languages", async () => {
const xml = await fetchStringsXml();
SONOS_LANG.forEach(lang => {
expect(xpath.select(
`string(/stringtables/stringtable[@xml:lang="${lang}"]/string[@stringId="AppLinkMessage"])`,
xml
)).toBeDefined();
describe(STRINGS_ROUTE, () => {
async function fetchStringsXml() {
const res = await request(server).get(stringsUrl.path()).send();
expect(res.status).toEqual(200);
// removing the sonos xml ns as makes xpath queries with xpath-ts painful
return parseXML(
res.text.replace('xmlns="http://sonos.com/sonosapi"', "")
);
}
it("should return xml for the strings", async () => {
const xml = await fetchStringsXml();
const sonosString = (id: string, lang: string) =>
xpath.select(
`string(/stringtables/stringtable[@xml:lang="${lang}"]/string[@stringId="${id}"])`,
xml
);
expect(sonosString("AppLinkMessage", "en-US")).toEqual(
"Linking sonos with music land"
);
expect(sonosString("AppLinkMessage", "nl-NL")).toEqual(
"Sonos koppelen aan music land"
);
// no fr-FR translation, so use en-US
expect(sonosString("AppLinkMessage", "fr-FR")).toEqual(
"Linking sonos with music land"
);
});
it("should return a section for all sonos supported languages", async () => {
const xml = await fetchStringsXml();
SONOS_LANG.forEach((lang) => {
expect(
xpath.select(
`string(/stringtables/stringtable[@xml:lang="${lang}"]/string[@stringId="AppLinkMessage"])`,
xml
)
).toBeDefined();
});
});
});
});
describe(`${presentationUrl}`, () => {
it("should have an ArtWorkSizeMap for all sizes recommended by sonos", async () => {
const res = await request(server).get(presentationUrl.path()).send();
describe(PRESENTATION_MAP_ROUTE, () => {
it("should have an ArtWorkSizeMap for all sizes recommended by sonos", async () => {
const res = await request(server).get(presentationUrl.path()).send();
expect(res.status).toEqual(200);
expect(res.status).toEqual(200);
// removing the sonos xml ns as makes xpath queries with xpath-ts painful
const xml = parseXML(
res.text.replace('xmlns="http://sonos.com/sonosapi"', "")
);
const imageSizeMap = (size: string) =>
xpath.select(
`string(/Presentation/PresentationMap[@type="ArtWorkSizeMap"]/Match/imageSizeMap/sizeEntry[@size="${size}"]/@substitution)`,
xml
// removing the sonos xml ns as makes xpath queries with xpath-ts painful
const xml = parseXML(
res.text.replace('xmlns="http://sonos.com/sonosapi"', "")
);
SONOS_RECOMMENDED_IMAGE_SIZES.forEach((size) => {
expect(imageSizeMap(size)).toEqual(`/art/size/${size}`);
const imageSizeMap = (size: string) =>
xpath.select(
`string(/Presentation/PresentationMap[@type="ArtWorkSizeMap"]/Match/imageSizeMap/sizeEntry[@size="${size}"]/@substitution)`,
xml
);
SONOS_RECOMMENDED_IMAGE_SIZES.forEach((size) => {
expect(imageSizeMap(size)).toEqual(`/size/${size}`);
});
});
it("should have an BrowseIconSizeMap for all sizes recommended by sonos", async () => {
const res = await request(server).get(presentationUrl.path()).send();
expect(res.status).toEqual(200);
// removing the sonos xml ns as makes xpath queries with xpath-ts painful
const xml = parseXML(
res.text.replace('xmlns="http://sonos.com/sonosapi"', "")
);
const imageSizeMap = (size: string) =>
xpath.select(
`string(/Presentation/PresentationMap[@type="BrowseIconSizeMap"]/Match/browseIconSizeMap/sizeEntry[@size="${size}"]/@substitution)`,
xml
);
SONOS_RECOMMENDED_IMAGE_SIZES.forEach((size) => {
expect(imageSizeMap(size)).toEqual(`/size/${size}`);
});
});
});
});
@@ -246,7 +275,7 @@ describe("track", () => {
albumId: someTrack.album.id,
albumArtist: someTrack.artist.name,
albumArtistId: someTrack.artist.id,
albumArtURI: `http://localhost:4567/foo/album/${someTrack.album.id}/art/size/180?access-token=1234`,
albumArtURI: `http://localhost:4567/foo/art/album/${someTrack.album.id}/size/180?access-token=1234`,
artist: someTrack.artist.name,
artistId: someTrack.artist.id,
duration: someTrack.duration,
@@ -270,7 +299,82 @@ describe("album", () => {
albumArtURI: defaultAlbumArtURI(bonobUrl, someAlbum).href(),
canPlay: true,
artist: someAlbum.artistName,
artistId: someAlbum.artistId,
artistId: `artist:${someAlbum.artistId}`,
});
});
});
describe("playlistAlbumArtURL", () => {
describe("when the playlist has no albumIds", () => {
it("should return question mark icon", () => {
const bonobUrl = url("http://localhost:1234/context-path?search=yes");
const playlist = aPlaylist({
entries: [aTrack({ album: undefined }), aTrack({ album: undefined })],
});
expect(playlistAlbumArtURL(bonobUrl, playlist).href()).toEqual(
`http://localhost:1234/context-path/icon/error/size/legacy?search=yes`
);
});
});
describe("when the playlist has 2 distinct albumIds", () => {
it("should return them on the url to the image", () => {
const bonobUrl = url("http://localhost:1234/context-path?search=yes");
const playlist = aPlaylist({
entries: [
aTrack({ album: albumToAlbumSummary(anAlbum({ id: "1" })) }),
aTrack({ album: albumToAlbumSummary(anAlbum({ id: "2" })) }),
aTrack({ album: albumToAlbumSummary(anAlbum({ id: "1" })) }),
aTrack({ album: albumToAlbumSummary(anAlbum({ id: "2" })) }),
],
});
expect(playlistAlbumArtURL(bonobUrl, playlist).href()).toEqual(
`http://localhost:1234/context-path/art/album/1&2/size/180?search=yes`
);
});
});
describe("when the playlist has 4 distinct albumIds", () => {
it("should return them on the url to the image", () => {
const bonobUrl = url("http://localhost:1234/context-path?search=yes");
const playlist = aPlaylist({
entries: [
aTrack({ album: albumToAlbumSummary(anAlbum({ id: "1" })) }),
aTrack({ album: albumToAlbumSummary(anAlbum({ id: "2" })) }),
aTrack({ album: albumToAlbumSummary(anAlbum({ id: "2" })) }),
aTrack({ album: albumToAlbumSummary(anAlbum({ id: "3" })) }),
aTrack({ album: albumToAlbumSummary(anAlbum({ id: "4" })) }),
],
});
expect(playlistAlbumArtURL(bonobUrl, playlist).href()).toEqual(
`http://localhost:1234/context-path/art/album/1&2&3&4/size/180?search=yes`
);
});
});
describe("when the playlist has 9 distinct albumIds", () => {
it("should return 9 of the ids on the url", () => {
const bonobUrl = url("http://localhost:1234/context-path?search=yes");
const playlist = aPlaylist({
entries: [
aTrack({ album: albumToAlbumSummary(anAlbum({ id: "1" })) }),
aTrack({ album: albumToAlbumSummary(anAlbum({ id: "2" })) }),
aTrack({ album: albumToAlbumSummary(anAlbum({ id: "3" })) }),
aTrack({ album: albumToAlbumSummary(anAlbum({ id: "4" })) }),
aTrack({ album: albumToAlbumSummary(anAlbum({ id: "5" })) }),
aTrack({ album: albumToAlbumSummary(anAlbum({ id: "6" })) }),
aTrack({ album: albumToAlbumSummary(anAlbum({ id: "7" })) }),
aTrack({ album: albumToAlbumSummary(anAlbum({ id: "8" })) }),
aTrack({ album: albumToAlbumSummary(anAlbum({ id: "9" })) }),
],
});
expect(playlistAlbumArtURL(bonobUrl, playlist).href()).toEqual(
`http://localhost:1234/context-path/art/album/1&2&3&4&5&6&7&8&9/size/180?search=yes`
);
});
});
});
@@ -281,7 +385,7 @@ describe("defaultAlbumArtURI", () => {
const album = anAlbum();
expect(defaultAlbumArtURI(bonobUrl, album).href()).toEqual(
`http://localhost:1234/context-path/album/${album.id}/art/size/180?search=yes`
`http://localhost:1234/context-path/art/album/${album.id}/size/180?search=yes`
);
});
});
@@ -292,7 +396,7 @@ describe("defaultArtistArtURI", () => {
const artist = anArtist();
expect(defaultArtistArtURI(bonobUrl, artist).href()).toEqual(
`http://localhost:1234/something/artist/${artist.id}/art/size/180?s=123`
`http://localhost:1234/something/art/artist/${artist.id}/size/180?s=123`
);
});
});
@@ -344,7 +448,7 @@ describe("api", () => {
const accessToken = `accessToken-${uuid()}`;
const bonobUrlWithAccessToken = bonobUrl.append({
searchParams: { "bonob-access-token": accessToken },
searchParams: { "bat": accessToken },
});
const service = bonobService("test-api", 133, bonobUrl, "AppLink");
@@ -353,9 +457,11 @@ describe("api", () => {
service,
bonobUrl,
musicService as unknown as MusicService,
linkCodes as unknown as LinkCodes,
accessTokens as unknown as AccessTokens,
clock
{
linkCodes: () => linkCodes as unknown as LinkCodes,
accessTokens: () => accessTokens as unknown as AccessTokens,
clock,
}
);
beforeEach(() => {
@@ -450,7 +556,8 @@ describe("api", () => {
.catch((e: any) => {
expect(e.root.Envelope.Body.Fault).toEqual({
faultcode: "Client.NOT_LINKED_RETRY",
faultstring: "Link Code not found yet, sonos app will keep polling until you log in to bonob",
faultstring:
"Link Code not found yet, sonos app will keep polling until you log in to bonob",
detail: {
ExceptionInfo: "NOT_LINKED_RETRY",
SonosError: "5",
@@ -707,46 +814,72 @@ describe("api", () => {
getMetadataResult({
mediaCollection: [
{
itemType: "container",
id: "artists",
title: "Artists",
albumArtURI: iconArtURI(bonobUrl, "artists").href(),
itemType: "container",
},
{
id: "albums",
title: "Albums",
albumArtURI: iconArtURI(bonobUrl, "albums").href(),
itemType: "albumList",
},
{ itemType: "albumList", id: "albums", title: "Albums" },
{
itemType: "playlist",
id: "playlists",
title: "Playlists",
albumArtURI: iconArtURI(bonobUrl, "playlists").href(),
itemType: "playlist",
attributes: {
readOnly: "false",
renameable: "false",
userContent: "true",
},
},
{ itemType: "container", id: "genres", title: "Genres" },
{
itemType: "albumList",
id: "genres",
title: "Genres",
albumArtURI: iconArtURI(bonobUrl, "genres").href(),
itemType: "container",
},
{
id: "randomAlbums",
title: "Random",
albumArtURI: iconArtURI(bonobUrl, "random").href(),
itemType: "albumList",
},
{
itemType: "albumList",
id: "starredAlbums",
title: "Starred",
albumArtURI: iconArtURI(bonobUrl, "starred").href(),
itemType: "albumList",
},
{
itemType: "albumList",
id: "recentlyAdded",
title: "Recently added",
albumArtURI: iconArtURI(
bonobUrl,
"recentlyAdded"
).href(),
itemType: "albumList",
},
{
itemType: "albumList",
id: "recentlyPlayed",
title: "Recently played",
albumArtURI: iconArtURI(
bonobUrl,
"recentlyPlayed"
).href(),
itemType: "albumList",
},
{
itemType: "albumList",
id: "mostPlayed",
title: "Most played",
albumArtURI: iconArtURI(
bonobUrl,
"mostPlayed"
).href(),
itemType: "albumList",
},
],
index: 0,
@@ -758,7 +891,7 @@ describe("api", () => {
describe("when an accept-language header is present with value nl-NL", () => {
it("should return nl-NL", async () => {
ws.addHttpHeader("accept-language", "nl-NL")
ws.addHttpHeader("accept-language", "nl-NL, en-US;q=0.9");
const root = await ws.getMetadataAsync({
id: "root",
index: 0,
@@ -768,46 +901,72 @@ describe("api", () => {
getMetadataResult({
mediaCollection: [
{
itemType: "container",
id: "artists",
title: "Artiesten",
albumArtURI: iconArtURI(bonobUrl, "artists").href(),
itemType: "container",
},
{
id: "albums",
title: "Albums",
albumArtURI: iconArtURI(bonobUrl, "albums").href(),
itemType: "albumList",
},
{ itemType: "albumList", id: "albums", title: "Albums" },
{
itemType: "playlist",
id: "playlists",
title: "Afspeellijsten",
albumArtURI: iconArtURI(bonobUrl, "playlists").href(),
itemType: "playlist",
attributes: {
readOnly: "false",
renameable: "false",
userContent: "true",
},
},
{ itemType: "container", id: "genres", title: "Genres" },
{
itemType: "albumList",
id: "genres",
title: "Genres",
albumArtURI: iconArtURI(bonobUrl, "genres").href(),
itemType: "container",
},
{
id: "randomAlbums",
title: "Willekeurig",
albumArtURI: iconArtURI(bonobUrl, "random").href(),
itemType: "albumList",
},
{
itemType: "albumList",
id: "starredAlbums",
title: "Favorieten",
albumArtURI: iconArtURI(bonobUrl, "starred").href(),
itemType: "albumList",
},
{
itemType: "albumList",
id: "recentlyAdded",
title: "Onlangs toegevoegd",
albumArtURI: iconArtURI(
bonobUrl,
"recentlyAdded"
).href(),
itemType: "albumList",
},
{
itemType: "albumList",
id: "recentlyPlayed",
title: "Onlangs afgespeeld",
albumArtURI: iconArtURI(
bonobUrl,
"recentlyPlayed"
).href(),
itemType: "albumList",
},
{
itemType: "albumList",
id: "mostPlayed",
title: "Meest afgespeeld",
albumArtURI: iconArtURI(
bonobUrl,
"mostPlayed"
).href(),
itemType: "albumList",
},
],
index: 0,
@@ -859,6 +1018,10 @@ describe("api", () => {
itemType: "container",
id: `genre:${genre.id}`,
title: genre.name,
albumArtURI: iconArtURI(
bonobUrl,
iconForGenre(genre.name),
).href(),
})),
index: 0,
total: expectedGenres.length,
@@ -880,6 +1043,10 @@ describe("api", () => {
itemType: "container",
id: `genre:${genre.id}`,
title: genre.name,
albumArtURI: iconArtURI(
bonobUrl,
iconForGenre(genre.name),
).href(),
})),
index: 1,
total: expectedGenres.length,
@@ -890,30 +1057,40 @@ describe("api", () => {
});
describe("asking for playlists", () => {
const expectedPlayLists = [
{ id: "1", name: "pl1" },
{ id: "2", name: "pl2" },
{ id: "3", name: "pl3" },
{ id: "4", name: "pl4" },
];
const playlist1 = aPlaylist({ id: "1", name: "pl1" });
const playlist2 = aPlaylist({ id: "2", name: "pl2" });
const playlist3 = aPlaylist({ id: "3", name: "pl3" });
const playlist4 = aPlaylist({ id: "4", name: "pl4" });
const playlists = [playlist1, playlist2, playlist3, playlist4];
beforeEach(() => {
musicLibrary.playlists.mockResolvedValue(expectedPlayLists);
musicLibrary.playlists.mockResolvedValue(
playlists.map(playlistToPlaylistSummary)
);
musicLibrary.playlist.mockResolvedValueOnce(playlist1);
musicLibrary.playlist.mockResolvedValueOnce(playlist2);
musicLibrary.playlist.mockResolvedValueOnce(playlist3);
musicLibrary.playlist.mockResolvedValueOnce(playlist4);
});
describe("asking for all playlists", () => {
it("should return a collection of playlists", async () => {
const result = await ws.getMetadataAsync({
id: `playlists`,
id: "playlists",
index: 0,
count: 100,
});
expect(result[0]).toEqual(
getMetadataResult({
mediaCollection: expectedPlayLists.map((playlist) => ({
mediaCollection: playlists.map((playlist) => ({
itemType: "playlist",
id: `playlist:${playlist.id}`,
title: playlist.name,
albumArtURI: playlistAlbumArtURL(
bonobUrlWithAccessToken,
playlist
).href(),
canPlay: true,
attributes: {
readOnly: "false",
@@ -922,7 +1099,7 @@ describe("api", () => {
},
})),
index: 0,
total: expectedPlayLists.length,
total: playlists.length,
})
);
});
@@ -937,22 +1114,25 @@ describe("api", () => {
});
expect(result[0]).toEqual(
getMetadataResult({
mediaCollection: [
expectedPlayLists[1]!,
expectedPlayLists[2]!,
].map((playlist) => ({
itemType: "playlist",
id: `playlist:${playlist.id}`,
title: playlist.name,
canPlay: true,
attributes: {
readOnly: "false",
userContent: "false",
renameable: "false",
},
})),
mediaCollection: [playlists[1]!, playlists[2]!].map(
(playlist) => ({
itemType: "playlist",
id: `playlist:${playlist.id}`,
title: playlist.name,
albumArtURI: playlistAlbumArtURL(
bonobUrlWithAccessToken,
playlist
).href(),
canPlay: true,
attributes: {
readOnly: "false",
userContent: "false",
renameable: "false",
},
})
),
index: 1,
total: expectedPlayLists.length,
total: playlists.length,
})
);
});
@@ -988,7 +1168,7 @@ describe("api", () => {
it
).href(),
canPlay: true,
artistId: it.artistId,
artistId: `artist:${it.artistId}`,
artist: it.artistName,
})
),
@@ -1025,7 +1205,7 @@ describe("api", () => {
it
).href(),
canPlay: true,
artistId: it.artistId,
artistId: `artist:${it.artistId}`,
artist: it.artistName,
})),
index: 2,
@@ -1329,7 +1509,7 @@ describe("api", () => {
it
).href(),
canPlay: true,
artistId: it.artistId,
artistId: `artist:${it.artistId}`,
artist: it.artistName,
})),
index: 0,
@@ -1377,7 +1557,7 @@ describe("api", () => {
it
).href(),
canPlay: true,
artistId: it.artistId,
artistId: `artist:${it.artistId}`,
artist: it.artistName,
})),
index: 0,
@@ -1425,7 +1605,7 @@ describe("api", () => {
it
).href(),
canPlay: true,
artistId: it.artistId,
artistId: `artist:${it.artistId}`,
artist: it.artistName,
})),
index: 0,
@@ -1473,7 +1653,7 @@ describe("api", () => {
it
).href(),
canPlay: true,
artistId: it.artistId,
artistId: `artist:${it.artistId}`,
artist: it.artistName,
})),
index: 0,
@@ -1521,7 +1701,7 @@ describe("api", () => {
it
).href(),
canPlay: true,
artistId: it.artistId,
artistId: `artist:${it.artistId}`,
artist: it.artistName,
})),
index: 0,
@@ -1567,7 +1747,7 @@ describe("api", () => {
it
).href(),
canPlay: true,
artistId: it.artistId,
artistId: `artist:${it.artistId}`,
artist: it.artistName,
})),
index: 0,
@@ -1576,7 +1756,7 @@ describe("api", () => {
);
expect(musicLibrary.albums).toHaveBeenCalledWith({
type: "alphabeticalByArtist",
type: "alphabeticalByName",
_index: paging.index,
_count: paging.count,
});
@@ -1613,7 +1793,7 @@ describe("api", () => {
it
).href(),
canPlay: true,
artistId: it.artistId,
artistId: `artist:${it.artistId}`,
artist: it.artistName,
})),
index: 2,
@@ -1622,7 +1802,7 @@ describe("api", () => {
);
expect(musicLibrary.albums).toHaveBeenCalledWith({
type: "alphabeticalByArtist",
type: "alphabeticalByName",
_index: paging.index,
_count: paging.count,
});
@@ -1657,7 +1837,7 @@ describe("api", () => {
it
).href(),
canPlay: true,
artistId: it.artistId,
artistId: `artist:${it.artistId}`,
artist: it.artistName,
})),
index: 0,
@@ -1704,7 +1884,7 @@ describe("api", () => {
it
).href(),
canPlay: true,
artistId: it.artistId,
artistId: `artist:${it.artistId}`,
artist: it.artistName,
})),
index: 0,
@@ -2164,7 +2344,7 @@ describe("api", () => {
album
).href(),
canPlay: true,
artistId: album.artistId,
artistId: `artist:${album.artistId}`,
artist: album.artistName,
},
},
@@ -2248,7 +2428,7 @@ describe("api", () => {
})
.href(),
httpHeaders: {
header: "bonob-access-token",
header: "bat",
value: accessToken,
},
});
@@ -2330,7 +2510,7 @@ describe("api", () => {
expect(root[0]).toEqual({
getMediaMetadataResult: track(
bonobUrl.with({
searchParams: { "bonob-access-token": accessToken },
searchParams: { "bat": accessToken },
}),
someTrack
),

View File

@@ -10,6 +10,7 @@
"strict": true,
"noImplicitAny": false,
"typeRoots" : [
"../typings",
"../node_modules/@types"
]
},

View File

@@ -138,15 +138,19 @@ describe("URLBuilder", () => {
describe("with URLSearchParams", () => {
it("should return a new URLBuilder with the new search params appended", () => {
const original = url("https://example.com/some-path?a=b&c=d");
const searchParams = new URLSearchParams({ x: "y" });
searchParams.append("z", "1");
searchParams.append("z", "2");
const updated = original.append({
searchParams: new URLSearchParams({ x: "y", z: "1" }),
searchParams,
});
expect(original.href()).toEqual("https://example.com/some-path?a=b&c=d");
expect(`${original.searchParams()}`).toEqual("a=b&c=d")
expect(updated.href()).toEqual("https://example.com/some-path?a=b&c=d&x=y&z=1");
expect(`${updated.searchParams()}`).toEqual("a=b&c=d&x=y&z=1")
expect(updated.href()).toEqual("https://example.com/some-path?a=b&c=d&x=y&z=1&z=2");
expect(`${updated.searchParams()}`).toEqual("a=b&c=d&x=y&z=1&z=2")
});
});
});
@@ -168,15 +172,19 @@ describe("URLBuilder", () => {
it("should return a new URLBuilder with the new search params", () => {
const original = url("https://example.com/some-path?a=b&c=d");
const searchParams = new URLSearchParams({ x: "y" });
searchParams.append("z", "1");
searchParams.append("z", "2");
const updated = original.with({
searchParams: { x: "y", z: "1" },
searchParams,
});
expect(original.href()).toEqual("https://example.com/some-path?a=b&c=d");
expect(`${original.searchParams()}`).toEqual("a=b&c=d")
expect(updated.href()).toEqual("https://example.com/some-path?x=y&z=1");
expect(`${updated.searchParams()}`).toEqual("x=y&z=1")
expect(updated.href()).toEqual("https://example.com/some-path?x=y&z=1&z=2");
expect(`${updated.searchParams()}`).toEqual("x=y&z=1&z=2")
});
});
@@ -196,15 +204,19 @@ describe("URLBuilder", () => {
it("should return a new URLBuilder with the new search params", () => {
const original = url("https://example.com/some-path?a=b&c=d");
const searchParams = new URLSearchParams({ x: "y" });
searchParams.append("z", "1");
searchParams.append("z", "2");
const updated = original.with({
searchParams: new URLSearchParams({ x: "y", z: "1" }),
searchParams,
});
expect(original.href()).toEqual("https://example.com/some-path?a=b&c=d");
expect(`${original.searchParams()}`).toEqual("a=b&c=d")
expect(updated.href()).toEqual("https://example.com/some-path?x=y&z=1");
expect(`${updated.searchParams()}`).toEqual("x=y&z=1")
expect(updated.href()).toEqual("https://example.com/some-path?x=y&z=1&z=2");
expect(`${updated.searchParams()}`).toEqual("x=y&z=1&z=2")
});
});
});

35
tests/utils.test.ts Normal file
View File

@@ -0,0 +1,35 @@
import { takeWithRepeats } from "../src/utils";
describe("takeWithRepeat", () => {
describe("when there is nothing in the input", () => {
it("should return an array of undefineds", () => {
expect(takeWithRepeats([], 3)).toEqual([undefined, undefined, undefined]);
});
});
describe("when there are exactly the amount required", () => {
it("should return them all", () => {
expect(takeWithRepeats(["a", undefined, "c"], 3)).toEqual([
"a",
undefined,
"c",
]);
expect(takeWithRepeats(["a"], 1)).toEqual(["a"]);
expect(takeWithRepeats([undefined], 1)).toEqual([undefined]);
});
});
describe("when there are less than the amount required", () => {
it("should cycle through the ones available", () => {
expect(takeWithRepeats(["a", "b"], 3)).toEqual(["a", "b", "a"]);
expect(takeWithRepeats(["a", "b"], 5)).toEqual(["a", "b", "a", "b", "a"]);
});
});
describe("when there more than the amount required", () => {
it("should return the first n items", () => {
expect(takeWithRepeats(["a", "b", "c"], 2)).toEqual(["a", "b"]);
expect(takeWithRepeats(["a", undefined, "c"], 2)).toEqual(["a", undefined]);
});
});
});

View File

@@ -4,9 +4,11 @@
/* Basic Options */
// "incremental": true, /* Enable incremental compilation */
"target": "ES2019", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
"lib": ["es2019"], /* Specify library files to be included in the compilation. */
"target": "ES2019" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */,
"module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */,
"lib": [
"es2019"
] /* Specify library files to be included in the compilation. */,
// "allowJs": true, /* Allow javascript files to be compiled. */
// "checkJs": true, /* Report errors in .js files. */
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
@@ -14,8 +16,8 @@
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
// "sourceMap": true, /* Generates corresponding '.map' file. */
// "outFile": "./", /* Concatenate and emit output to single file. */
"outDir": "./build", /* Redirect output structure to the directory. */
"rootDir": ".", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
"outDir": "./build" /* Redirect output structure to the directory. */,
"rootDir": "." /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */,
// "composite": true, /* Enable project compilation */
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
// "removeComments": true, /* Do not emit comments to output. */
@@ -25,31 +27,35 @@
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
/* Strict Type-Checking Options */
"strict": true, /* Enable all strict type-checking options. */
"noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
"strict": true /* Enable all strict type-checking options. */,
"noImplicitAny": true /* Raise error on expressions and declarations with an implied 'any' type. */,
// "strictNullChecks": true, /* Enable strict null checks. */
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
"strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
"strictPropertyInitialization": true /* Enable strict checking of property initialization in classes. */,
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
/* Additional Checks */
"noUnusedLocals": true, /* Report errors on unused locals. */
"noUnusedParameters": true, /* Report errors on unused parameters. */
"noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
"noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
"noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */
"noUnusedLocals": true /* Report errors on unused locals. */,
"noUnusedParameters": true /* Report errors on unused parameters. */,
"noImplicitReturns": true /* Report error when not all code paths in function return a value. */,
"noFallthroughCasesInSwitch": true /* Report errors for fallthrough cases in switch statement. */,
"noUncheckedIndexedAccess": true /* Include 'undefined' in index signature results */,
/* Module Resolution Options */
// "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
// "typeRoots": [], /* List of folders to include type definitions from. */
// "types": [], /* Type declaration files to be included in compilation. */
"typeRoots": [
"./typings",
"node_modules/@types"
]
/* List of folders to include type definitions from. */,
// "types": ["src/customTypes/scale-that-svg.d.ts"], /* Type declaration files to be included in compilation. */
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */,
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
@@ -64,7 +70,7 @@
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
/* Advanced Options */
"skipLibCheck": true, /* Skip type checking of declaration files. */
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
"skipLibCheck": true /* Skip type checking of declaration files. */,
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
}
}

4
typings/scale-that-svg/index.d.ts vendored Normal file
View File

@@ -0,0 +1,4 @@
declare module "scale-that-svg" {
const noTypesYet: any;
export default noTypesYet;
}

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<path d="M4.96 2.126l.613.416c.301.204.663.321.982.425.158.051.396.128.466.173l.035.023.01.006c.13.084.309.2.535.272C7.748 3.488 7.906 3.513 8.06 3.513c.339 0 .651-.12.881-.339.274.105.601.224.972.25.038.135.068.273.1.423.057.267.122.569.239.889.089.259.208.609.491.91.17.203.355.324.48.407l.031.021c.029.04.076.119.111.179.075.128.166.282.29.437.124.187.296.347.49.456-.088.076-.18.15-.278.222-.365.24-.64.498-.839.788L11.004 8.19l-.02.035-.027.047c-.217.377-.542.941-.423 1.627.034.285.108.529.174.745.017.055.034.11.05.166l-.022.011-.034.02c-.096.056-.938.57-.938 1.369 0 .095.01.182.027.262-.218.2-.351.476-.354.785-.104.14-.181.278-.247.397-.023.042-.046.085-.071.126-.071.037-.216.084-.395.12-.093-.214-.222-.361-.308-.457-.158-.228-.362-.396-.544-.545-.039-.032-.089-.073-.131-.109-.014-.118-.032-.235-.059-.34-.099-.385-.315-.667-.489-.894-.032-.041-.073-.095-.105-.14.02-.049.046-.108.068-.156.099-.221.234-.523.265-.894l.004-.043v-.043c0-.499-.127-.942-.395-1.371.057-.163.105-.375.093-.624C7.139 8.2 7.158 8.088 7.158 7.957c0-.008-.022-.643-.291-1.164C6.855 6.754 6.841 6.716 6.825 6.678 6.626 6.218 6.181 5.985 5.5 5.985c-.04 0-.09 0-.148 0-.586 0-1.988 0-2.326-.001C2.999 5.971 2.972 5.955 2.946 5.94L2.741 5.824C2.63 5.762 2.451 5.662 2.269 5.557c.006-.015.011-.03.016-.045.221-.641.094-1.106-.069-1.397.039-.033.081-.064.126-.094l.06-.034c.237-.13.73-.402.92-1.048.023-.09.036-.175.042-.252.069-.054.145-.12.219-.201.005 0 .01 0 .014 0 .419 0 .775-.15 1.034-.259C4.678 2.209 4.72 2.191 4.761 2.175 4.827 2.156 4.893 2.14 4.96 2.126M5.889 1C5.382 1.035 4.911 1.071 4.441 1.211c-.25.091-.553.26-.841.26-.046 0-.092-.004-.137-.014-.109-.035-.181-.105-.29-.105-.109 0-.217.035-.254.105-.036.071 0 .105 0 .176C2.883 1.913 2.448 1.984 2.376 2.23c-.036.141 0 .316-.036.457C2.268 2.932 2.014 3.038 1.833 3.144c-.326.211-.579.457-.76.808C1.036 4.022 1 4.093 1 4.162c0 .141.217.281.29.386C1.434 4.725 1.398 4.97 1.326 5.181c-.073.211-.217.386-.29.562C1 5.814 1 5.885 1 5.919c.036.035.073.07.109.106.326.246 1.145.686 1.326.792.157.091.341.18.505.182C3 7 5 7 5.5 7c0 0 .5 0 .375.133C6.02 7.238 6.142 7.817 6.142 7.957c0 .14-.073.246-.036.352.036.387-.349.597-.096.913.254.316.398.632.398 1.054-.036.422-.362.773-.362 1.194 0 .457.543.808.652 1.23.036.141.036.316.073.457.073.316.639.597.82.878.073.106.181.175.217.316 0 .071 0 .141 0 .211 0 .175.145.351.326.422C8.166 14.995 8.201 15 8.238 15c.09 0 .192-.025.294-.05.398-.035 1.123-.175 1.376-.527.158-.219.254-.492.434-.668.109-.105.217-.211.181-.351-.036-.071-.073-.106-.073-.141 0-.071.145-.106.217-.106.073 0 .145 0 .217 0 .181-.035.254-.246.217-.386-.036-.175-.217-.281-.29-.457-.036-.035-.036-.07-.036-.105 0-.141.254-.387.434-.492.217-.105.471-.211.579-.422.073-.175.073-.351 0-.527-.073-.352-.217-.668-.254-1.019-.073-.351.145-.703.326-1.019.145-.211.362-.387.579-.527C13.167 7.677 13.891 6.878 14 6c-.254.211-.688.272-1.05.306-.109 0-.217 0-.29-.035-.073-.035-.145-.106-.181-.175C12.262 5.85 12.153 5.498 11.9 5.288c-.145-.106-.29-.175-.398-.317-.145-.141-.217-.352-.29-.563-.217-.598-.217-1.09-.471-1.687-.073-.14-.145-.316-.29-.316-.073 0-.145 0-.254 0-.045.007-.091.009-.136.009-.456 0-.912-.296-1.373-.391C8.645 2.009 8.594 2 8.544 2c-.07 0-.138.017-.18.058C8.256 2.164 8.348 2.335 8.24 2.44 8.197 2.481 8.13 2.498 8.06 2.498c-.049 0-.101-.008-.146-.023C7.805 2.44 7.701 2.369 7.592 2.299 7.23 2.053 6.506 1.948 6.144 1.702c.073-.105.109-.211.145-.317.036-.105-.038-.28-.146-.35C6.07 1 5.961 1 5.889 1L5.889 1zM13.875 10c-.099.215-.232.465-.398.571-.132.071-.299.143-.365.285-.099.179 0 .393-.033.571 0 .071-.033.143-.066.215-.033.179 0 .393.066.571.033.107.099.25.199.285.066 0 .166-.035.232-.107.066-.071.132-.215.166-.321s.033-.215.033-.321c.033-.321.199-.643.266-.965.033-.215.033-.429 0-.643C13.942 10.071 13.909 10 13.875 10l.017.035c.009.009.017.018.017.035l-.017-.035C13.884 10.027 13.875 10.018 13.875 10L13.875 10z"/>
</svg>

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 26 26">
<path fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="1.9" d="M19 3L19 22M22 9L22 16M25 11L25 14M16 7L16 18M10 9L10 16M13 11L13 14M7 4L7 21M1 11L1 14M4 8L4 17"/>
</svg>

After

Width:  |  Height:  |  Size: 293 B

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 30">
<path fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M4,14c0,0.691,4.925,1,11,1s11-0.309,11-1"/>
<path d="M15 12c5.66 0 8-.588 8-.588S22.07 3 19.5 3 17.436 4 15 4s-1.991-1-4.5-1S7 11.412 7 11.412 9.34 12 15 12zM12 24h-2c-1.657 0-3-1.343-3-3v-1c0-1.105.895-2 2-2h3c1.105 0 2 .895 2 2v2C14 23.105 13.105 24 12 24zM16 22v-2c0-1.105.895-2 2-2h3c1.105 0 2 .895 2 2v1c0 1.657-1.343 3-3 3h-2C16.895 24 16 23.105 16 22z"/>
<path fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M13 20L17 20"/>
</svg>

After

Width:  |  Height:  |  Size: 695 B

4
web/icons/Book-22940.svg Normal file
View File

@@ -0,0 +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="M25,27H9c-1.105,0-2-0.895-2-2V7c0-1.105,0.895-2,2-2h16V27z"/>
<path fill="none" stroke="#000" stroke-miterlimit="10" stroke-width="2" d="M7 25c0-1.105.895-2 2-2h16M11 10L22 10"/>
</svg>

After

Width:  |  Height:  |  Size: 325 B

15
web/icons/C-3PO-31823.svg Normal file
View File

@@ -0,0 +1,15 @@
<svg xmlns="http://www.w3.org/2000/svg" id="c3po" viewBox="0 0 48 48">
<path fill="#f4b10b" d="M16 39h16v6H16V39zM39 18C39 18 39 18.1 39 18c-.3-3.8-1.8-7.2-4.2-9.8-1.8-1.9-4.1-3.1-6.8-3.8-.8-.4-1.6-.8-2.3-1.4l-1.1-.9c-.4-.3-.9-.3-1.3 0l-1.1.9c-.7.6-1.5 1-2.3 1.4-2.6.6-4.9 1.9-6.7 3.8-2.5 2.5-3.9 5.9-4.2 9.7 0 0 0 0-.1 0 0 0-2 1.7-2 2 .1 2.6.4 4.8.8 7 .1.3 2.3 1 2.3 1 .2 1 .5 2 .8 3l1.1 2h24l1.1-2c.3-1 .5-2 .8-3 0 0 2.2-.7 2.3-1 .5-2.2.8-4.4.9-7C41 19.7 39 18 39 18z"/>
<path fill="#b57f08" d="M12,32h24v4H12V32z"/>
<path fill="#ffd600" d="M24,42c-3.7,0-6.2-1.5-7.7-2.7c-0.8-0.7-1.4-1.6-1.7-2.6c-1.3-4.9-3.5-10.6-3.6-17 C10.8,12.5,15.3,6,24,6s13.2,6.5,13,13.7c-0.2,6.3-2.3,12.1-3.6,17c-0.3,1-0.8,2-1.7,2.6C30.2,40.5,27.7,42,24,42z"/>
<path fill="#684903" d="M26,36h-4c-0.6,0-1-0.4-1-1s0.4-1,1-1h4c0.6,0,1,0.4,1,1S26.6,36,26,36z"/>
<path fill="#ffc107" d="M27,32l-3-3l-3,3l-6-6l4-2h10l4,2L27,32z"/>
<path fill="#f4b10b" d="M29,18c-2.8,0-4.3,2.3-5,3c-0.7-0.7-2.2-3-5-3s-5,2.2-5,5s2.2,5,5,5c2.9,0,5-3,5-3s2.1,3,5,3 c2.8,0,5-2.2,5-5S31.8,18,29,18z"/>
<path fill="#ffea00" d="M22 23c0 1.7-1.3 3-3 3s-3-1.3-3-3 1.3-3 3-3S22 21.3 22 23zM32 23c0 1.7-1.3 3-3 3s-3-1.3-3-3 1.3-3 3-3S32 21.3 32 23z"/>
<path fill="#684903" d="M30,23c0,0.6-0.4,1-1,1s-1-0.4-1-1s0.4-1,1-1S30,22.4,30,23z"/>
<path fill="#ffd600" d="M25,4c0,0.6-0.4,1-1,1s-1-0.4-1-1s0.4-1,1-1S25,3.4,25,4z"/>
<path fill="#684903" d="M20,23c0,0.6-0.4,1-1,1s-1-0.4-1-1s0.4-1,1-1S20,22.4,20,23z"/>
<path fill="#dd9f05" d="M12,38c-0.6,0-1-0.4-1-1v-6c0-0.6,0.4-1,1-1s1,0.4,1,1v6C13,37.6,12.6,38,12,38z"/>
<path fill="#ffd600" d="M33,46H15c-0.6,0-1-0.4-1-1s0.4-1,1-1h18c0.6,0,1,0.4,1,1S33.6,46,33,46z"/>
<path fill="#dd9f05" d="M36,38c-0.6,0-1-0.4-1-1v-6c0-0.6,0.4-1,1-1s1,0.4,1,1v6C37,37.6,36.6,38,36,38z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<path d="M28,18.77v-2.557l-1.875-0.256c-0.092-0.013-1.249-0.121-2.58-0.127c1.445-2.708,2-5.083,2.006-5.111l0.311-1.358l-1.166-0.762l-0.071-0.046l-0.993-0.649l-1.046,0.56c-0.252,0.135-1.802,0.987-3.357,2.336c-0.293-2.623-0.967-4.824-1.245-5.53L17.488,4h-1.363h-0.219h-1.33l-0.514,1.226c-0.058,0.138-1.26,3.041-1.557,6.491c-1.651-1.342-4.443-2.884-4.443-2.884l-2.107,1.644l0.201,1.035C6.373,12.529,7.453,15,7.453,15C6.333,15,4,15.393,4,15.393v2.644c0,0,1.875,1.587,4.036,2.731c-0.092,0.087-0.152,0.149-0.179,0.177l-0.94,0.804l0.279,1.075l0.296,1.142L8.645,24.3c0.167,0.043,1.237,0.258,2.41,0.258c0.827,0,1.61-0.106,2.328-0.314c0.075-0.02,0.15-0.041,0.223-0.063c0.211,1.205,0.468,2.166,0.517,2.346L14.527,28H21c-1.292-1.333-2.18-3.417-2.18-3.417c-0.01-0.013-0.019-0.027-0.029-0.042c0.792,0.333,1.833,0.458,2.803,0.481l1.362-0.083l0.547-0.895l0.538-0.919c0,0-0.458-1.083-0.979-1.549c1.541-0.57,3.388-1.408,4.172-2.155L28,18.77z M21.5,19.986c-0.583,0.167-2.268,0.396-2.896,0.473c0,0,2.415,1.398,2.811,2.44c-0.001,0.01,0.006,0.023,0.005,0.034c-0.023-0.001-0.044-0.015-0.068-0.013c-1.875,0.146-4.823-1.502-5.161-1.79c0.072,1.068,0.455,3.57,1.356,4.838L17.519,26h-1.467c0,0-1.095-2.928-1.021-4.969c-0.513,0.437-1.105,1.001-2.205,1.293c-0.609,0.177-1.226,0.235-1.771,0.235c-0.858,0-1.715-0.144-1.913-0.195l-0.01-0.04c0,0,1.422-1.311,3.255-1.748c0.22-0.073,0.44-0.146,0.66-0.146c-0.733-0.146-1.466-0.291-2.273-0.583c-2.406-0.888-4.471-2.46-4.665-2.656L6.105,17.15c0,0,0.767-0.042,1.348-0.042c0.944,0,2.493,0.112,4.127,0.698c0.44,0.146,0.88,0.292,1.247,0.51c-0.607-0.536-3.639-3.066-4.539-6.874l0,0c0,0,4.4,2.266,6.691,5.891c-0.333-1.583-0.539-3.305-0.539-4.131C14.44,9.496,15.906,6,15.906,6h0.219c0.345,0.88,1.246,3.995,1.246,7.203c0,1.087-0.303,3.604-0.365,4.093c0.163-0.388,0.777-1.594,1.607-2.858c1.599-2.434,4.919-4.211,4.919-4.211l0.071,0.046c-0.115,0.501-0.891,3.306-2.637,6.002c-0.435,0.672-1.79,1.949-1.906,2.041c0.205-0.052,0.911-0.329,3.007-0.447c0.504-0.029,0.965-0.039,1.375-0.039c1.295,0,2.411,0.108,2.411,0.108l0.003,0.035C25.244,18.554,23.259,19.482,21.5,19.986z"/>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<path fill="none" stroke="#000" stroke-linejoin="round" stroke-miterlimit="10" d="M10.5 7.125L10.494 3.529 7.494 0.5 4.5 3.5 4.5 7.125"/>
<path fill="none" stroke="#000" stroke-linejoin="round" stroke-miterlimit="10" d="M2.5 13.5L2.5 8.5 7.5 5.5 12.5 8.5 12.5 13.5M7 13.5L2 13.5"/>
<path fill="none" stroke="#000" stroke-linejoin="round" stroke-miterlimit="10" d="M8.5 13.5v-3c0-.552-.448-1-1-1s-1 .448-1 1v3M13 13.5L8 13.5"/>
<path fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" d="M10.5 10.5L10.5 13.5M4.5 10.5L4.5 13.5M7.5 3.5L7.5 5.5"/>
</svg>

After

Width:  |  Height:  |  Size: 665 B

View File

@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" id="chewy" viewBox="0 0 48 48">
<path fill="#6d4c41" d="M39,25c0-2,0-0.216,0-2C39,9,34,2,24,2S9,10,9,22c0,1.784,0,1,0,3H39z"/>
<path fill="#a1887f" d="M39,25L39,25c0-7-4-12-7-12v-3c-3,0-4,2-4,2V8c0,0-3,1-4,3c-1-2-4-3-4-3s0,1,0,3c0,0-1-1-4-1v3 c-3,0-7,5-7,12v0c0,9-2.375,14.125-3,15h5v5l6-3l3,4l4-3l4,3l3-4l6,3v-5h5C41.375,39.125,39,34,39,25z"/>
<path fill="#8d6e63" d="M23 29c-.127 0-.877 0-1 0-6 0-7 6-7 6s2-3 4-3 3 0 3 0c.552 0 1-.448 1-1V29zM25 29c.127 0 .877 0 1 0 6 0 7 6 7 6s-2-3-4-3-3 0-3 0c-.552 0-1-.448-1-1V29z"/>
<path fill="#212121" d="M29,36H19c-1.1,0-2-0.9-2-2v0c0-1.1,0.9-2,2-2h10c1.1,0,2,0.9,2,2v0C31,35.1,30.1,36,29,36z"/>
<path fill="#fff" d="M19 32L20 34 21 32zM27 32L28 34 29 32zM27 36L26 34 25 36zM23 36L22 34 21 36z"/>
<path fill="#424242" d="M27,28c0,0.82-0.67,1.63-2,1.9c-0.3,0.07-0.63,0.1-1,0.1s-0.7-0.03-1-0.1c-1.33-0.27-2-1.08-2-1.9 c0-0.5,1-3,3-3S27,27.5,27,28z"/>
<path fill="#212121" d="M23 29.5v.4c-1.33-.27-2-1.08-2-1.9h.5C22.33 28 23 28.67 23 29.5zM27 28c0 .82-.67 1.63-2 1.9v-.4c0-.83.67-1.5 1.5-1.5H27z"/>
<path fill="#6d4c41" d="M29,18c-1.657,0-3,1.343-3,3s1.343,3,3,3c5,0,6,2,6,2S35,18,29,18z"/>
<path fill="#212121" d="M29 20A1 1 0 1 0 29 22A1 1 0 1 0 29 20Z"/>
<path fill="#6d4c41" d="M19,18c1.657,0,3,1.343,3,3s-1.343,3-3,3c-5,0-6,2-6,2S13,18,19,18z"/>
<path fill="#212121" d="M19 20A1 1 0 1 0 19 22A1 1 0 1 0 19 20Z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 30">
<path d="M9 2A3 3 0 1 0 9 8 3 3 0 1 0 9 2zM6 25.967C6 26.537 6.463 27 7.033 27c.544 0 .995-.422 1.031-.965L8.466 20H6V25.967zM9.936 26.036C9.972 26.578 10.423 27 10.967 27 11.537 27 12 26.537 12 25.967V20H9.534L9.936 26.036zM11 10h-.277C10.376 10.595 9.738 11 9 11s-1.376-.405-1.723-1H7c-1.654 0-3 1.346-3 3v6c0 .553.447 1 1 1s1-.447 1-1v-2h6v2c0 .553.447 1 1 1s1-.447 1-1v-6C14 11.346 12.654 10 11 10zM24 19v-6c0-1.657-1.343-3-3-3s-3 1.343-3 3v6H24zM18 22v3.967C18 26.537 18.463 27 19.033 27c.544 0 .995-.422 1.031-.964L20.333 22H18zM21.667 22l.269 4.035C21.972 26.578 22.423 27 22.967 27 23.537 27 24 26.537 24 25.967V22H21.667zM21 2A3 3 0 1 0 21 8 3 3 0 1 0 21 2z"/>
<path d="M26.249 6.751c.827.827.749 2.247.749 2.247s-1.42.078-2.247-.749-.749-2.247-.749-2.247S25.422 5.924 26.249 6.751zM17.998 6.002c0 0 .078 1.42-.749 2.247s-2.247.749-2.247.749-.078-1.42.749-2.247S17.998 6.002 17.998 6.002z"/>
<path fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M25,19l-1.515-6.06C23.2,11.8,22.175,11,21,11h0c-1.175,0-2.2,0.8-2.485,1.94L17,19"/>
<path d="M24 17L18 17 17 20 25 20z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 30" id="christmas">
<path d="M21.5 17A1.5 1.5 0 1 0 21.5 20 1.5 1.5 0 1 0 21.5 17zM15 12A2 2 0 1 0 15 16 2 2 0 1 0 15 12zM8.5 17A1.5 1.5 0 1 0 8.5 20 1.5 1.5 0 1 0 8.5 17z"/>
<path d="M15 14L13.679 12.498 7.634 17.276 8.5 20 15 20zM23.5 25A1.5 1.5 0 1 0 23.5 28 1.5 1.5 0 1 0 23.5 25z"/>
<path d="M15 18A2 2 0 1 0 15 22 2 2 0 1 0 15 18zM6.5 25A1.5 1.5 0 1 0 6.5 28 1.5 1.5 0 1 0 6.5 25zM15 6A1 1 0 1 0 15 8 1 1 0 1 0 15 6zM20 11A1 1 0 1 0 20 13 1 1 0 1 0 20 11z"/>
<path d="M15 7L15.645 6.241 20.613 11.21 20 13 15 13zM10 11A1 1 0 1 0 10 13 1 1 0 1 0 10 11zM15.146 1.091l.559 1.133 1.25.182c.133.019.187.183.09.277l-.905.882.214 1.245c.023.133-.117.234-.236.171L15 4.393l-1.118.588c-.119.063-.259-.039-.236-.171l.214-1.245-.905-.882c-.096-.094-.043-.258.09-.277l1.25-.182.559-1.133C14.914.97 15.086.97 15.146 1.091zM16.321 18.5L15 21v7h8.5l.869-2.722L16.321 18.5zM18 26c-.552 0-1-.448-1-1 0-.552.448-1 1-1s1 .448 1 1C19 25.552 18.552 26 18 26zM13.679 18.5l-8.049 6.778L6.5 28H15v-7L13.679 18.5zM13 24c-.552 0-1-.448-1-1 0-.552.448-1 1-1s1 .448 1 1C14 23.552 13.552 24 13 24z"/>
<path d="M16.32 12.498L15 14v6h6.5l.866-2.724L16.32 12.498zM17 18c-.552 0-1-.448-1-1 0-.552.448-1 1-1s1 .448 1 1C18 17.552 17.552 18 17 18zM14.355 6.241L9.388 11.21 10 13h5v-3c0 .552-.448 1-1 1s-1-.448-1-1c0-.552.448-1 1-1s1 .448 1 1V7L14.355 6.241z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" id="christmas">
<path d="M17 51c-.552 0-1 .448-1 1v2c0 .552.448 1 1 1s1-.448 1-1v-2C18 51.448 17.552 51 17 51zM22 51c-.552 0-1 .448-1 1v2c0 .552.448 1 1 1s1-.448 1-1v-2C23 51.448 22.552 51 22 51zM27 51c-.552 0-1 .448-1 1v2c0 .552.448 1 1 1s1-.448 1-1v-2C28 51.448 27.552 51 27 51zM32 51c-.552 0-1 .448-1 1v2c0 .552.448 1 1 1s1-.448 1-1v-2C33 51.448 32.552 51 32 51zM37 51c-.552 0-1 .448-1 1v2c0 .552.448 1 1 1s1-.448 1-1v-2C38 51.448 37.552 51 37 51zM42 51c-.552 0-1 .448-1 1v2c0 .552.448 1 1 1s1-.448 1-1v-2C43 51.448 42.552 51 42 51zM48 52c0-.552-.448-1-1-1s-1 .448-1 1v2c0 .552.448 1 1 1s1-.448 1-1V52z"/>
<path d="M54.641,55.858L46.417,44h1.576c0.776,0,1.469-0.434,1.807-1.131c0.336-0.695,0.247-1.503-0.233-2.109L41.036,30h1.946c0.806,0,1.51-0.453,1.84-1.182c0.325-0.721,0.202-1.539-0.321-2.133l-10.769-12.23l2.399,1.359c0.28,0.175,0.625,0.229,0.945,0.146c0.321-0.082,0.601-0.296,0.761-0.582c0.146-0.255,0.196-0.556,0.143-0.848c-0.01-0.053-0.023-0.104-0.042-0.155l-1.492-4.18l3.097-2.849c0.378-0.307,0.541-0.81,0.416-1.283c-0.004-0.016-0.009-0.031-0.014-0.046c-0.148-0.468-0.558-0.804-1.051-0.855l-4.198-0.401L33.143,0.77c-0.125-0.31-0.374-0.558-0.682-0.682c-0.307-0.123-0.653-0.115-0.945,0.017c-0.293,0.129-0.521,0.366-0.648,0.678L29.32,4.76l-4.218,0.4c-0.667,0.068-1.159,0.673-1.097,1.343c0.027,0.306,0.166,0.589,0.395,0.802l3.148,2.895l-1.45,4.185c-0.015,0.043-0.027,0.088-0.036,0.133c-0.13,0.658,0.292,1.309,0.94,1.452C27.092,15.99,27.183,16,27.271,16c0.221,0,0.434-0.059,0.606-0.167l2.373-1.36L19.5,26.685c-0.524,0.595-0.647,1.413-0.321,2.133c0.33,0.729,1.035,1.182,1.84,1.182h1.895l-8.482,10.764c-0.478,0.605-0.566,1.413-0.229,2.107C14.54,43.567,15.231,44,16.006,44h1.544L9.356,55.862c-0.424,0.614-0.472,1.408-0.125,2.069C9.577,58.591,10.254,59,10.998,59H24v3c0,1.103,0.897,2,2,2h12c1.103,0,2-0.897,2-2v-3h13.002c0.744,0,1.422-0.41,1.768-1.071C55.116,57.267,55.067,56.473,54.641,55.858z M28.669,13.075l0.983-2.839c0.13-0.376,0.025-0.794-0.268-1.063L27.01,6.989l3.113-0.295c0.376-0.036,0.701-0.281,0.838-0.633l1.047-2.691l1.047,2.691c0.137,0.353,0.46,0.597,0.837,0.633l3.088,0.295l-2.375,2.184c-0.296,0.272-0.4,0.694-0.265,1.073l1.01,2.828l-2.862-1.622c-0.308-0.173-0.684-0.173-0.991,0.002L28.669,13.075z M26,62v-3h12l0.002,3H26z M10.969,57.046L19.98,44H28c0.552,0,1-0.447,1-1s-0.448-1-1-1l-11.998,0.002L25.46,30h3.492c0.552,0,1-0.448,1-1s-0.448-1-1-1L21,28.006l10.999-12.492L42.982,28h-3.871c-0.152,0-0.292,0.039-0.421,0.1c-0.109,0.036-0.216,0.083-0.311,0.159c-0.433,0.343-0.506,0.972-0.162,1.405L47.992,42h-5.975c-0.553,0-1,0.447-1,1s0.447,1,1,1h1.965l9.019,13L10.969,57.046z"/>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@@ -0,0 +1,3 @@
<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="M6 20L26 20M12 20L12 24M9 20L9 24M20 20L20 24M23 20L23 24M17 20L17 24M20 12l-3.801-4.561C15.439 6.527 14.314 6 13.127 6H10c-2.209 0-4 1.791-4 4v16h20V16c0-2.209-1.791-4-4-4H20z"/>
</svg>

After

Width:  |  Height:  |  Size: 324 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" version="1.0" viewBox="0 0 24 24">
<path d="M6,7.5C6,7.5,7.4,7,8.5,7S11,7.5,11,7.5s-0.9,2.1-2.5,2.1S6,7.5,6,7.5z"/>
<path d="M19,4v7c0,4.5-5,7.7-7,8.8c-2-1.1-7-4.2-7-8.8V4H19 M19,2H5C3.9,2,3,2.9,3,4c0,0,0,3,0,7c0,7.1,9,11,9,11s9-4,9-11 c0-3.9,0-7,0-7C21,2.9,20.1,2,19,2L19,2z"/>
<path d="M15.3,13c0,0-1.8,0.8-3.3,0.8c-1.5,0-3.3-0.8-3.3-0.8s1.2,3,3.3,3C14.2,16,15.3,13,15.3,13L15.3,13z"/>
<path d="M19,2h-7v20c0,0,9-4,9-11c0-3.9,0-7,0-7C21,2.9,20.1,2,19,2z M15.5,9.6c-1.6,0-2.5-2.1-2.5-2.1S14.4,7,15.5,7 S18,7.5,18,7.5S17.1,9.6,15.5,9.6z"/>
</svg>

After

Width:  |  Height:  |  Size: 594 B

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 30">
<path d="M26,14c-1.7,0-3,1.3-3,3c0,3.56-5.5,4-8,4s-8,0-8-4c0-1.7-1.3-3-3-3s-3,1.3-3,3c0,3.9,3.8,7,14,7s14-3.2,14-7C29,15.3,27.7,14,26,14z"/>
<path d="M21,8.5C20.296,6.221,19.572,5,18,5c-1.152,0-1.726,1-3,1s-1.848-1-3-1c-1.572,0-2.296,1.221-3,3.5c-0.677,2.19-2,8.144-2,8.144S10.018,18,15,18s8-1.356,8-1.356S21.677,10.69,21,8.5z"/>
</svg>

After

Width:  |  Height:  |  Size: 401 B

View File

@@ -0,0 +1,12 @@
<svg xmlns="http://www.w3.org/2000/svg" id="darth" viewBox="0 0 48 48">
<path fill="#263238" d="M13,16h11h11l8,20v2c0,0-4.813,4-8,4c-3.438,2-11,2.017-11,2.017S16.062,43.75,13,42c-1.438,0-8-2.563-8-4s0-2,0-2L13,16z"/>
<path fill="#455a64" d="M44,37c0-2.825-2.049-7.746-4.029-12.504c-0.453-1.089-0.907-2.182-1.323-3.222c-0.02-0.067-0.039-0.128-0.06-0.2c-0.003-0.01-0.006-0.022-0.009-0.032c-0.04-0.135-0.082-0.28-0.126-0.43c-0.014-0.047-0.028-0.096-0.042-0.145c-0.039-0.135-0.079-0.275-0.119-0.418c-0.011-0.039-0.022-0.077-0.033-0.117c-0.051-0.181-0.102-0.368-0.154-0.56c-0.011-0.042-0.023-0.085-0.034-0.127c-0.042-0.155-0.084-0.313-0.126-0.473c-0.015-0.057-0.03-0.113-0.044-0.17c-0.053-0.203-0.105-0.408-0.156-0.615c-0.003-0.013-0.006-0.026-0.009-0.039c-0.048-0.193-0.094-0.387-0.14-0.581c-0.014-0.06-0.028-0.119-0.041-0.179C37.292,16.05,37.076,14.913,37,14c-0.009-0.107-0.039-0.177-0.062-0.262c-0.036-0.345-0.089-0.646-0.156-0.906C35.768,7.811,31.308,4,26,4h-4c-5.308,0-9.768,3.811-10.782,8.832c-0.068,0.261-0.12,0.561-0.156,0.906C11.039,13.823,11.009,13.893,11,14c-0.076,0.913-0.292,2.05-0.554,3.189c-0.014,0.06-0.027,0.119-0.041,0.179c-0.046,0.194-0.092,0.388-0.14,0.581c-0.003,0.013-0.006,0.026-0.009,0.039c-0.051,0.207-0.104,0.413-0.156,0.615c-0.015,0.057-0.03,0.113-0.044,0.17c-0.042,0.16-0.084,0.318-0.126,0.473c-0.011,0.042-0.023,0.085-0.034,0.127c-0.052,0.192-0.104,0.379-0.154,0.56c-0.011,0.04-0.022,0.078-0.033,0.117c-0.041,0.144-0.08,0.284-0.119,0.418c-0.014,0.049-0.028,0.097-0.042,0.145c-0.043,0.15-0.086,0.294-0.126,0.43c-0.003,0.01-0.006,0.022-0.009,0.032c-0.044,0.149-0.085,0.286-0.124,0.414c-0.01,0.032-0.019,0.063-0.028,0.093c-0.031,0.102-0.06,0.197-0.086,0.282c-0.005,0.016-0.01,0.033-0.015,0.048c-0.03,0.096-0.056,0.179-0.078,0.25c-0.006,0.021-0.011,0.036-0.017,0.054c-0.016,0.052-0.032,0.1-0.042,0.131c-0.002,0.006-0.005,0.014-0.006,0.02C9.007,22.393,9,22.417,9,22.417l2.219-0.385c0.21-0.639,0.451-1.182,0.691-1.675c1.466-3.023,3.434-3.272,4.448-3.356H17c2.417,0,5,1,5,1h4c0,0,2.667-1,5-1h0.642c1.014,0.084,2.858,0.25,4.448,3.356c0.104,0.202,0.202,0.423,0.299,0.648c0.06,0.155,0.127,0.32,0.189,0.479c0.068,0.18,0.138,0.353,0.203,0.549l0.016,0.003c0.408,1.019,0.856,2.099,1.327,3.231c1.816,4.363,3.876,9.31,3.879,11.679c-0.005,0.033-0.717,4.041-10.169,5.518C29.726,42.578,26.878,42.509,24,42c-2.91-0.514-5.781-2.536-8-5c0,0-1,3.063-3,5c0,2.209,4.925,3,11,3c5.853,0,10.625-0.7,10.967-2.764c4.615-0.783,6.887-2.043,7.993-3.168C43.966,38.046,44.003,37.152,44,37z"/>
<path fill="#546e7a" d="M29,19c-3.625,0-5,3-5,3l1,3c1.031,1.031,5,1,5,1s4,0.125,4-2S32.625,19,29,19z M19,18.999c-3.625,0-5,2.875-5,5s4,2,4,2s3.969,0.031,5-1l1-3C24,21.998,22.625,18.999,19,18.999z M29.978,27.999l-0.016,0c-1.126,0-2.324-0.089-3.482-0.34c0.902,1.35,2.019,3.03,3.147,4.728c0.297,0.447,0.593,0.894,0.886,1.335c0.589-1.479,1.575-3.981,2.349-6.094C32.074,27.866,31.16,28,30.124,28C30.059,28,30.01,27.999,29.978,27.999z M20,33.552V35h1v-2.953c-0.317,0.477-0.637,0.959-0.961,1.446L20,33.552z M28,35h0.961c-0.324-0.488-0.644-0.971-0.961-1.448V35z M29,35.059V35h-0.039C28.974,35.02,28.987,35.039,29,35.059z M22,35h1v-4h-1V35z M26,31v4h1v-2.954c-0.24-0.361-0.467-0.703-0.696-1.046H26z M24,35h1v-4h-1V35z"/>
<path fill="#455a64" d="M19 18.999c-.507 0-.965.061-1.387.164 3.448.756 5.258 3.989 5.387 4.839.159-.125.302-.236.449-.351L24 21.999C24 21.998 22.625 18.999 19 18.999zM29 19c-.307 0-.591.028-.866.067 2.86 1.276 3.691 5.761 3.838 6.735C32.981 25.571 34 25.072 34 24 34 21.875 32.625 19 29 19z"/>
<path fill="#78909c" d="M26,29h-4v-9.188l0.636,0.489c1.011,0.496,1.699,0.534,2.702,0.022L26,19.813V29z"/>
<path fill="#37474f" d="M38.984,22.366c-0.002-0.005-0.004-0.013-0.006-0.02c-0.01-0.031-0.025-0.08-0.042-0.131c-0.006-0.018-0.011-0.034-0.017-0.054c-0.022-0.07-0.048-0.154-0.078-0.25c-0.005-0.015-0.01-0.033-0.015-0.048c-0.026-0.085-0.055-0.181-0.086-0.282c-0.009-0.031-0.019-0.061-0.028-0.093c-0.039-0.128-0.08-0.265-0.124-0.414c-0.003-0.01-0.006-0.022-0.009-0.032c-0.04-0.135-0.082-0.28-0.126-0.43c-0.014-0.047-0.028-0.096-0.042-0.145c-0.039-0.135-0.079-0.275-0.119-0.418c-0.011-0.039-0.022-0.077-0.033-0.117c-0.051-0.181-0.102-0.368-0.154-0.56c-0.011-0.042-0.023-0.085-0.034-0.127c-0.042-0.155-0.084-0.313-0.126-0.473c-0.015-0.057-0.03-0.113-0.044-0.17c-0.053-0.203-0.105-0.408-0.156-0.615c-0.003-0.013-0.006-0.026-0.009-0.039c-0.048-0.193-0.094-0.387-0.14-0.581c-0.014-0.06-0.028-0.119-0.041-0.179C37.292,16.05,37.076,14.913,37,14c-0.009-0.107-0.039-0.177-0.062-0.262c-0.036-0.345-0.089-0.646-0.156-0.906C35.768,7.811,31.308,4,26,4c3.463,1.079,6.733,3.241,7.707,6.189c0.296,0.897-0.436,1.811-1.38,1.805C28.838,11.975,29.132,11.956,26,13v5c0,0,2.667-1,5-1c0.204,0,0.421,0,0.642,0c1.014,0.084,2.941,0.333,4.448,3.356c0.245,0.491,0.481,1.036,0.691,1.675L39,22.417C39,22.417,38.993,22.393,38.984,22.366z"/>
<path fill="#455a64" d="M23,22v1h2v-1H23z M23,25h2v-1h-2V25z"/>
<path fill="#c5cae9" d="M18,34c-0.552,0-1,0.448-1,1c0,0.552,0.448,1,1,1s1-0.448,1-1C19,34.448,18.552,34,18,34z M24,26c-1.1,0-2,0.9-2,2v2h4v-2C26,26.9,25.1,26,24,26z M30,34c-0.552,0-1,0.448-1,1c0,0.552,0.448,1,1,1s1-0.448,1-1C31,34.448,30.552,34,30,34z"/>
<path fill="#607d8b" d="M25,3h-2c-0.552,0-1,0.448-1,1v14h4V4C26,3.448,25.552,3,25,3z M15.677,11.994c1.657-0.009,2.461-0.017,3.185,0.088C19.464,12.171,20,11.705,20,11.098V6.102c0-0.622-0.671-1.022-1.214-0.719c-2.075,1.159-3.769,2.748-4.455,4.695C14.001,11.014,14.684,12,15.677,11.994z M18.022,27.999C17.99,27.999,17.941,28,17.876,28c-1.037,0-1.95-0.134-2.739-0.372c0.775,2.113,1.76,4.615,2.349,6.094c0.292-0.441,0.589-0.888,0.886-1.335c1.128-1.699,2.245-3.379,3.147-4.728c-1.158,0.251-2.356,0.34-3.482,0.34L18.022,27.999z M11.203,22.034l0.016-0.003c0.092-0.276,0.191-0.523,0.29-0.77c0.035-0.09,0.073-0.185,0.108-0.274C13.605,16.383,16.913,16.988,17,17c1.043,0,2.116,0.186,3,0.398v-2.266c0-0.506-0.38-0.937-0.883-0.994C17.89,14.001,17,14,17,14c-5,0-6.268,1.924-6.51,3c-0.361,1.6-0.824,3.225-1.137,4.273c-0.416,1.041-0.87,2.134-1.323,3.223C6.049,29.254,4,34.175,4,37c-0.003,0.152,0.034,1.046,1.04,2.068c1.107,1.125,3.378,2.385,7.994,3.168C13.02,42.158,13,42.082,13,42c0.053-0.051,0.103-0.107,0.154-0.16c-6.602-1.75-7.153-4.868-7.157-4.897C6,34.574,8.06,29.628,9.876,25.265C10.347,24.133,10.795,23.053,11.203,22.034z"/>
<path fill="#455a64" d="M37.51,17C37.268,15.924,36,14,31,14c0,0-2.568,0-5,0.677V18c0,0,2.583-1,5-1c0.093-0.013,3.75-0.75,5.781,5.031L39,22.417C39,22.417,38.111,19.666,37.51,17z"/>
</svg>

After

Width:  |  Height:  |  Size: 6.3 KiB

View File

@@ -0,0 +1,12 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 50">
<path fill="none" stroke="#000" stroke-miterlimit="10" stroke-width="2" d="M10.128,33.863C13.609,34.555,18.971,35,25,35c6.029,0,11.392-0.445,14.872-1.136"/>
<path fill="none" stroke="#000" stroke-miterlimit="10" stroke-width="2" d="M23.136 17.128C22.445 20.608 22 25.971 22 32c0 6.029.445 11.392 1.136 14.872M26.864 46.872C27.555 43.392 28 38.029 28 32c0-6.029-.445-11.391-1.136-14.872"/>
<path d="M33,38.102c0,0,2.563,3.496,3.688,4.949C35.563,44.504,33,48,33,48s3.496-2.563,4.949-3.684C39.402,45.438,42.902,48,42.902,48s-2.566-3.496-3.688-4.949c1.121-1.453,3.688-4.949,3.688-4.949s-3.5,2.563-4.953,3.684C36.496,40.664,33,38.102,33,38.102z"/>
<path fill="none" stroke="#000" stroke-linecap="round" stroke-miterlimit="10" stroke-width="2" d="M11.387,38.271C14.839,39.952,19.658,41,25,41c2.121,0,4.154-0.171,6.057-0.476"/>
<path fill="none" stroke="#000" stroke-miterlimit="10" stroke-width="2" d="M25 17L25 7M27 7h-4c-1.105 0-2-.895-2-2V3h8v2C29 6.105 28.105 7 27 7z"/>
<path fill="none" stroke="#000" stroke-linecap="round" stroke-miterlimit="10" stroke-width="2" d="M33.903,34.649C33.963,33.783,34,32.901,34,32c0-5.342-1.048-10.161-2.729-13.613"/>
<path fill="none" stroke="#000" stroke-linecap="round" stroke-miterlimit="10" stroke-width="2" d="M39.051 37.211C39.654 35.586 40 33.835 40 32c0-8.284-6.716-15-15-15-2.367 0-4.597.563-6.588 1.539M10.03 31.399C10.022 31.6 10 31.797 10 32c0 8.284 6.716 15 15 15 2.095 0 4.088-.432 5.899-1.208"/>
<path d="M8 13c0 0 2.563 3.496 3.688 4.949C10.563 19.402 8 22.898 8 22.898s3.496-2.563 4.949-3.684c1.453 1.121 4.953 3.684 4.953 3.684s-2.566-3.496-3.688-4.949C15.336 16.496 17.902 13 17.902 13s-3.5 2.563-4.953 3.684C11.496 15.563 8 13 8 13zM13 25c0 0 1.098 1.938 1.797 3.043C14.098 29.117 13 31 13 31s1.91-1.07 3-1.754C17.09 29.93 19 31 19 31s-1.098-1.883-1.797-2.957C17.902 26.938 19 25 19 25s-1.91 1.125-3 1.84C14.91 26.125 13 25 13 25z"/>
<path fill="none" stroke="#000" stroke-linecap="round" stroke-miterlimit="10" stroke-width="2" d="M16.018,32.752c0.094,5.039,1.109,9.573,2.711,12.861"/>
<path fill="none" stroke="#000" stroke-miterlimit="10" stroke-width="2" d="M39.872 30.136C36.391 29.444 31.029 29 25 29c-1.256 0-2.473.023-3.66.06M38.613 25.728C35.161 24.047 30.342 23 25 23c-1.055 0-2.087.045-3.094.124"/>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@@ -0,0 +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="M5 19L27 19M8 14L15 14M12 19L12 23M9 19L9 23M20 19L20 23M23 19L23 23M17 19L17 23M5 9H27V25H5z"/>
<path d="M22.5 13A1.5 1.5 0 1 0 22.5 16 1.5 1.5 0 1 0 22.5 13zM18.5 13A1.5 1.5 0 1 0 18.5 16 1.5 1.5 0 1 0 18.5 13z"/>
</svg>

After

Width:  |  Height:  |  Size: 362 B

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M21.382,18.967L12.585,4.331c-0.265-0.441-0.904-0.441-1.169,0L2.618,18.967C2.345,19.421,2.672,20,3.202,20h17.595C21.328,20,21.655,19.421,21.382,18.967z"/>
<path d="M13,18h-2v-2h2V18z M13,14h-2V9h2V14z"/>
</svg>

After

Width:  |  Height:  |  Size: 396 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M9,0C4.029,0,0,4.029,0,9s4.029,9,9,9s9-4.029,9-9S13.971,0,9,0z M9,1.751c1.39,0,2.037,0.345,2.037,1.441 c0,1.096-1.612,2.127-2.037,2.127c-0.427,0-2.039-1.031-2.039-2.127C6.961,2.096,7.61,1.751,9,1.751z M2.991,9.299 c-1.043-0.338-1.172-1.06-0.744-2.381c0.429-1.323,0.956-1.834,2-1.497c1.04,0.338,1.527,2.189,1.394,2.594 C5.511,8.42,4.034,9.636,2.991,9.299z M7.345,14.868c-0.602,0.918-1.332,0.856-2.498,0.097c-1.161-0.759-1.516-1.402-0.918-2.319 c0.598-0.919,2.51-0.902,2.867-0.669C7.154,12.209,7.94,13.956,7.345,14.868z M9,10.35c-0.746,0-1.35-0.604-1.35-1.35 S8.254,7.65,9,7.65c0.747,0,1.35,0.604,1.35,1.35S9.747,10.35,9,10.35z M13.231,14.844c-1.132,0.803-1.861,0.899-2.494,0.005 c-0.636-0.893,0.083-2.667,0.427-2.912c0.348-0.249,2.261-0.339,2.893,0.548C14.692,13.381,14.365,14.038,13.231,14.844z M15.005,9.299c-1.042,0.337-2.519-0.879-2.649-1.284c-0.134-0.405,0.354-2.256,1.393-2.594c1.045-0.337,1.572,0.174,2,1.497 C16.177,8.238,16.048,8.961,15.005,9.299z"/><path d="M21,19.489c0-1.499,0.618-2.416,1.335-3.479c0.78-1.157,1.664-2.468,1.664-4.548 c0-3.251-3.609-6.168-6.73-8.084c0.711,1.044,1.227,2.23,1.502,3.505c1.808,1.433,3.228,3.054,3.228,4.579 c0,3.352-2.999,4.243-2.999,8.027c0,3.204,3.535,4.411,3.686,4.46l0.629-1.898C23.291,22.043,21,21.236,21,19.489z"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

4
web/icons/Fridge-282.svg Normal file
View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 26 26">
<path d="M19,2c0.552,0,1,0.448,1,1v19c0,0.552-0.448,1-1,1H7c-0.552,0-1-0.448-1-1V3c0-0.552,0.448-1,1-1H19 M19,0 H7C5.344,0,4,1.344,4,3v19c0,1.656,1.344,3,3,3h12c1.656,0,3-1.344,3-3V3C22,1.344,20.656,0,19,0L19,0z"/>
<path d="M9 9C8.449 9 8 8.551 8 8V6c0-.551.449-1 1-1l0 0c.551 0 1 .449 1 1v2C10 8.551 9.551 9 9 9L9 9zM9 18c-.551 0-1-.449-1-1v-3c0-.551.449-1 1-1l0 0c.551 0 1 .449 1 1v3C10 17.551 9.551 18 9 18L9 18zM7 26c-.551 0-1-.448-1-1v-.469h3V25c0 .552-.449 1-1 1H7zM18 26c-.551 0-1-.448-1-1v-.469h3V25c0 .552-.449 1-1 1H18zM5 10H21V12H5z"/>
</svg>

After

Width:  |  Height:  |  Size: 618 B

6
web/icons/Globe-1301.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 30 KiB

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128">
<path d="M51.7 69.2A7 7 0 1 0 51.7 83.2A7 7 0 1 0 51.7 69.2Z"/>
<path d="M110.8,21.1l-4-4c-0.4-0.4-0.9-0.8-1.4-1c-2.6-1.4-5.6-0.7-7.4,1.3L90,26.4c-1,1.1-1.5,2.5-1.5,3.8L67.9,50.8c-3.1-2.2-6.7-3.4-10.4-3.4c-4.6,0-8.8,1.8-11.9,5c-0.7,0.7-1.3,1.4-1.8,2.1c-3.3,4.5-8.3,6.7-11.8,7.7c-4.3,1.2-8.2,3.5-11.4,6.7c-7.9,7.9-8.6,20.6-1.7,30.2c6.7,9.4,19,17.9,27.9,17.9h0c7.3,0,14-3.8,18.8-10.8c1.5-2.1,2.4-4.6,3.2-7c1.5-5.2,4.3-9.4,7.9-12.2c0.7-0.6,1.4-1.2,2.1-1.8c3.5-3.5,5.2-7.9,4.9-12.5c-0.1-1.6-1.2-4-2.6-6.4c-2.2-3.8-1.6-8.7,1.5-11.8l15.1-15.1c1.4,0,2.8-0.6,3.9-1.5l9.1-8.1c1.2-1.1,1.9-2.7,2-4.3C112.6,23.9,112,22.3,110.8,21.1z M71.5,77.9c-0.5,0.5-1,0.9-1.5,1.3c-4.7,3.6-8.2,8.9-10.1,15.4c-1,3.3-2.7,6.2-5.1,8.6c-3.2,3.2-6.9,4.8-11.1,4.8c-6.9,0-14.4-4.6-20-12.4c-5.2-7.2-4.7-16.7,1.1-22.5c2.4-2.4,5.5-4.2,8.8-5.1c6.4-1.8,11.6-5.2,15-9.9c0.4-0.5,0.8-1,1.3-1.5c2-2,4.7-3.1,7.6-3.1c2.5,0,5.1,0.8,7.2,2.4c5.9,4.4,9.6,9.7,10,14.3C74.8,73,73.8,75.6,71.5,77.9z M73.2,55.4c-0.2-0.2-0.5-0.5-0.7-0.7l18.8-18.8l0.7,0.7L73.2,55.4z M97.5,33.5C97.5,33.5,97.5,33.5,97.5,33.5l-3.1-3.1l8.1-9.1l4,4L97.5,33.5z"/>
<path d="M39,80.4c-1.2-1.2-3.1-1.2-4.2,0c-1.2,1.2-1.2,3.1,0,4.2l8.5,8.5c0.6,0.6,1.4,0.9,2.1,0.9s1.5-0.3,2.1-0.9c1.2-1.2,1.2-3.1,0-4.2L39,80.4z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<path fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M6,19c0,1.725,0.818,7,3.318,7C11,26,12.5,25,16,25c3.455,0,5,1,6.682,1C25.136,26,26,20.725,26,19"/>
<path d="M17.818,6.136C17.818,6.773,17,6.455,16,6.455s-1.818,0.273-1.818-0.318C14.182,5.5,15,5,16,5S17.818,5.5,17.818,6.136z"/>
<path fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M16,7C10.5,7,6,9.409,6,14.409c0,0.545,0,4,0,4.591c0,0,4.455-1,10-1c5.5,0,10,1,10,1c0-0.727,0-3.682,0-4.591C26,9.409,21.5,7,16,7z"/>
<path fill="none" stroke="#000" stroke-miterlimit="10" stroke-width="2" d="M18 11.123C18 10.503 17.497 10 16.877 10h-1.754C14.503 10 14 10.503 14 11.123v0c0 .515.351.965.851 1.09l2.299.575c.5.125.851.574.851 1.09v0C18 14.497 17.497 15 16.877 15h-1.754C14.503 15 14 14.497 14 13.877M16 10L16 8M16 17L16 15"/>
</svg>

After

Width:  |  Height:  |  Size: 986 B

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M17 20c0 .6-.4 1-1 1s-1-.4-1-1 1-2 1-2S17 19.4 17 20zM21 15.5c0 .8-.7 1.5-1.5 1.5S18 16.3 18 15.5c0-.8 1.5-4 1.5-4S21 14.7 21 15.5z"/>
<path fill="none" stroke="#000" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M9,12l4.5,4c5-5,5.5-11.5,5.5-13L3,17.5L7,21c0,0,3-2,5-6"/>
</svg>

After

Width:  |  Height:  |  Size: 373 B

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 30">
<path d="M17,21h-4v5c0,1.105,0.895,2,2,2h0c1.105,0,2-0.895,2-2V21z"/>
<path fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M8 10v10c0 .552.448 1 1 1h12c.552 0 1-.448 1-1V10c0-3.866-3.134-7-7-7h0C11.134 3 8 6.134 8 10zM12 10c0-1.657 1.343-3 3-3"/>
</svg>

After

Width:  |  Height:  |  Size: 387 B

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" id="halloween" viewBox="0 0 30 30">
<path d="M17.532,5c-2.517,0-3.596,0.966-4.435,1.265c-0.779,0.3-2.037,0.12-4.375,0.479c-6.472,1.079-5.693,8.648-5.693,8.648C3.029,23.124,10.64,26,12.498,26c1.258,0,1.318-0.599,1.918-0.599c0.599,0,1.258,0.599,2.517,0.599C21.307,26,27,22.165,27,14.554C27,6.284,20.049,5,17.532,5z M19.721,7.572l1.765,2.353l-1.176,1.765l-4.118,0.588L19.721,7.572z M16.191,14.042l-1.765,1.765l-1.765-1.765H16.191z M9.132,9.336l3.529,2.941H8.544l-1.176-1.176L9.132,9.336z M23.25,20.513c0-0.588-0.588-1.765-0.588-1.765c0,1.176-0.588,2.353-1.177,2.941c-0.176-0.588-0.588-1.176-0.588-1.176c-0.588,1.176-1.177,1.765-1.177,1.765c-0.412-0.647-1.176-1.176-1.176-1.176c0,0.882-0.588,1.765-0.588,1.765c-0.706-0.471-1.177-1.177-1.177-1.177s-0.588,0.824-0.588,1.765c-0.353-0.412-0.941-1.235-1.177-1.765c0,0-0.588,0.588-0.588,1.765c-0.588-0.471-1.765-1.765-1.765-1.765s-0.588,1.177-0.588,1.765c0,0-0.765-0.706-1.176-1.765c0,0-0.588,0.588-0.588,1.177c-1.765-1.177-1.765-2.353-1.765-2.353s-0.588,0.588-0.588,1.765c-1.765-1.765-2.941-3.529-2.941-6.471c0-1.118,0.588-2.941,1.177-3.529c-0.588,4.118,0.588,5.294,0.588,5.294c0-0.235,0-1.177,1.177-2.353c0,2.353,0.588,3.529,0.588,3.529c0-0.588,0.588-1.765,1.176-2.353c0,2.353,1.765,3.529,1.765,3.529c0-0.588,0.588-1.765,1.177-2.353c0,0.588,1.765,2.941,1.765,2.941s0-1.765,0.588-2.941c0.588,1.176,1.765,2.353,1.765,2.353s0.588-1.176,0.588-2.941c0,0,1.765,1.176,2.353,2.941c0,0,1.177-1.176,1.177-4.118c0,0,1.176,1.176,1.176,2.353c0,0,1.176-2.941,0.588-4.118c0,0,1.176,1.176,1.176,2.353c0,0,0.588-2.353-0.588-5.882c1.235,1.176,1.765,2.941,1.765,5.294C25.015,18.16,23.838,19.925,23.25,20.513z"/>
<path fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M12.795,7.572C10.998,3.317,15,3,15,3"/>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" id="halloween" viewBox="0 0 16 16">
<path fill="#ffab40" d="M4.41,8.67c0-0.008-0.001-0.014-0.001-0.022c-0.006,0.006-0.01,0.012-0.016,0.018C4.399,8.668,4.405,8.669,4.41,8.67z"/>
<path fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" d="M5.496,2.5C7.5,2.5,7.5,4.25,7.5,4.25"/>
<path d="M9.274,3c-1.26,0-1.819,0.553-2.221,0.71C6.65,3.867,6.039,3.779,4.863,3.943C1.619,4.482,2.015,8.736,2.015,8.736C1.999,12.575,5.808,14,6.754,14c0.63,0,0.63-0.297,0.945-0.297c0.315,0,0.63,0.297,1.26,0.297C11.164,14,14,12.097,14,8.326C14,4.226,10.534,3,9.274,3z M11,5l1,2h-1H9L11,5z M9.5,8L8,9.5L6.5,8H9.5z M5,5l2,2H5H4L5,5z M11,12h-1v-1H9v1H7v-1H6v1H5c0,0-1-0.75-1-2h1v1h1v-1h2v1h1v-1h1v1h1v-1h1C12,11.25,11,12,11,12z"/>
</svg>

After

Width:  |  Height:  |  Size: 807 B

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 50">
<path fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M5,38c9.729,0,8.167-17.084,12-17c-0.384,5.349,3.147,6.326,2.318,9.197c-0.554,1.918-3.321,3.219-3.318,4.734c0.021,9.363-3,8.264-3,10.224C13,45.791,13.525,46,14,46c1.896,0,4.454-4.814,5-8.021c0.912-5.358,7.467-3.904,9-13.979c0,0,3.192,2.212,5.677,0.153"/>
<path fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M1 35c0 0 1.027 3 4 3C15.001 38 6.699 8 22.991 8c5.301 0 9.484 6 12.009 6 3.309 0 4.445-4.962 4.445-4.962s-1.156-.832-1.39-2.044C37.802 5.68 38.493 4 38.493 4s1.495.673 2.26 2.072c.807 1.476 1.64 2.102 1.64 2.102s1.158.447 2.488 1.68c.945.876 1.524 1.958 1.524 1.958s1.946 1.978 2.397 2.654c.507.761-.231 2.029-.231 2.029l-2.518-.352c0 0-2.491.791-3.806-.669-2.336-.876-1.921 4.255-5.009 6.828-1.447 1.206-1.503 3.946-1.689 5.367-.181 1.387-1.749 3.015-3.136 2.473M42.425 3.988c1.501 2.068.345 3.182 1.282 4.769M35.061 20.143c-3.371 4.933.887 7.595-.426 9.263"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,15 @@
<svg xmlns="http://www.w3.org/2000/svg" id="luke" viewBox="0 0 48 48">
<path fill="#6d4c41" d="M31,37c0,0,0.6,3,4,3c-1.8,1.6-4.3,1.7-5,1S31,37,31,37z"/>
<path fill="#6d4c41" d="M30 38c0 0-.9 2.7 2 2 8.6-2 13-16 13-16H30V38zM16.8 37c0 0-.6 3-4 3 1.8 1.6 4.3 1.7 5 1S16.8 37 16.8 37z"/>
<path fill="#6d4c41" d="M17.8,38c0,0,0.9,2.7-2,2c-8.6-2-13-16-13-16h15V38z"/>
<path fill="#fb8c00" d="M16,33h16v13H16V33z"/>
<path fill="#bbdefb" d="M16 40l-2 6h7L16 40zM32 40l2 6h-7L32 40z"/>
<path fill="#ffa726" d="M35.8,24.7c0.2-0.8,0.5-1.5,1-2.1c0.4-0.5,1.1-0.8,1.6-0.5c0.5,0.3,0.6,1,0.6,1.6c0,1,0,2.1,0,3.1 c0,0.6,0,1.2-0.2,1.7c-0.2,0.6-0.5,1.1-0.9,1.6c-0.3,0.4-0.6,0.8-0.9,1.2c-0.3,0.4-0.6,0.7-1,0.6c-0.1,0-0.3-0.1-0.4-0.3 c-0.2-0.2-0.4-0.4-0.7-0.6L35.8,24.7z M12.2,24.7c-0.2-0.8-0.5-1.5-1-2.1c-0.4-0.5-1.1-0.8-1.6-0.5c-0.5,0.3-0.6,1-0.6,1.6 c0,1,0,2.1,0,3.1c0,0.6,0,1.2,0.2,1.7c0.2,0.6,0.5,1.1,0.9,1.6c0.3,0.4,0.6,0.8,0.9,1.2c0.3,0.4,0.6,0.7,1,0.6 c0.1,0,0.3-0.1,0.4-0.3c0.2-0.2,0.4-0.4,0.7-0.6L12.2,24.7z"/>
<path fill="#fb8c00" d="M38,26c0,2.1-1,3-1,3v-2c-0.6,0-1-0.4-1-1s0.4-1,1-1S38,25.4,38,26z M10,26c0,2.1,1,3,1,3v-2 c0.6,0,1-0.4,1-1s-0.4-1-1-1S10,25.4,10,26z"/>
<path fill="#ffb74d" d="M22,43c-3.1,0-10-7.8-10-11V15c0,0,0.3-8,12-8s12,8,12,8v17c0,3.1-7.1,11-10,11H22z"/>
<path fill="#fb8c00" d="M20 34H28V36H20z"/>
<path fill="#8d6e63" d="M39.9,16.9C39.4,11,36.6,2.1,29,2c-2.1,0-3,1-3,1c-1-0.8-3-1-3-1c-9.2,0-13,7.5-13,18l3,1.3 c0,0,8-3,8-13.3c0,11.1,14,12,14,12v1.3c0,3.7,1.3,6.8,5,6.8c0,0,3.4,0.1,5-4C41.1,24,40,17.5,39.9,16.9"/>
<path fill="#8d6e63" d="M8.1,16.9C8.6,11,14.4,2.1,22,2c2.1,0-9,17-9,17v2.3C13,25,11.7,28,8,28c0,0-3.4,0.1-5-4 C6.9,24,8,17.5,8.1,16.9"/>
<path fill="#784719" d="M31 24.5c0 .8-.7 1.5-1.5 1.5S28 25.3 28 24.5s.7-1.5 1.5-1.5S31 23.7 31 24.5M20 24.5c0 .8-.7 1.5-1.5 1.5S17 25.3 17 24.5s.7-1.5 1.5-1.5S20 23.7 20 24.5"/>
<path fill="#8d6e63" d="M21,9c0,0-1.3,12,8,12c-8.1-2.9-6-11-6-11L21,9z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -0,0 +1,5 @@
<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="M10,15V6c0-1.105,0.895-2,2-2h0c1.105,0,2,0.895,2,2v7"/>
<path fill="none" stroke="#000" stroke-miterlimit="10" stroke-width="2" d="M10,15v3.172l-2.586-2.586c-0.781-0.781-2.047-0.781-2.828,0c-0.781,0.781-0.781,2.047,0,2.828l6.791,6.772C12.421,26.226,13.779,27,15.375,27H20c3.314,0,6-2.686,6-6V7c0-1.105-0.895-2-2-2s-2,0.895-2,2v6"/>
<path fill="none" stroke="#000" stroke-miterlimit="10" stroke-width="2" d="M20 10L20 10c-1.105 0-2 .895-2 2v4c0 1.105.895 2 2 2h0c1.105 0 2-.895 2-2v-4C22 10.895 21.105 10 20 10zM16 10L16 10c-1.105 0-2 .895-2 2v4c0 1.105.895 2 2 2h0c1.105 0 2-.895 2-2v-4C18 10.895 17.105 10 16 10z"/>
</svg>

After

Width:  |  Height:  |  Size: 765 B

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<path fill="none" stroke="#000" stroke-linejoin="round" stroke-miterlimit="10" d="M2.5,8c0,3.038,2.462,0.5,5.5,0.5s5.5,2.538,5.5-0.5S11.038,2.5,8,2.5S2.5,4.962,2.5,8z"/>
<path fill="none" stroke="#000" stroke-linejoin="round" stroke-miterlimit="10" d="M9.152,8.625C9.369,10.951,9.5,12.135,9.5,12.5c0,0.828-0.672,1-1.5,1s-1.5-0.172-1.5-1c0-0.365,0.131-1.549,0.348-3.875"/>
</svg>

After

Width:  |  Height:  |  Size: 443 B

View File

@@ -0,0 +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="M10 24L10 8 26 5 26 21M10 12L26 9"/>
<path fill="none" stroke="#000" stroke-miterlimit="10" stroke-width="2" d="M23 18A3 3 0 1 0 23 24 3 3 0 1 0 23 18zM7 21A3 3 0 1 0 7 27 3 3 0 1 0 7 21z"/>
</svg>

After

Width:  |  Height:  |  Size: 337 B

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 50">
<path fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M31,35v12c0,1.102-0.898,2-2,2h-8c-1.102,0-2-0.898-2-2V35"/>
<path fill="none" stroke="#000" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M25 1A5 5 0 1 0 25 11 5 5 0 1 0 25 1zM46.381 13.616c-.824-.822-2.159-.822-2.985 0l-5.263 5.251-5.265-5.251C32.229 12.979 27.316 13 26.838 13L25 24l-1.838-11c-.478 0-5.391-.021-6.03.616l-5.265 5.251-5.263-5.251c-.826-.822-2.161-.822-2.985 0-.826.824-.826 2.156 0 2.979l6.756 6.739c.411.412.951.616 1.492.616.542 0 1.081-.204 1.494-.616L18 19v16h14V19l4.639 4.334c.413.412.951.616 1.493.616.54 0 1.081-.204 1.492-.616l6.756-6.739C47.206 15.772 47.206 14.44 46.381 13.616z"/>
<path fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M44.109 13.109L36.308 5.308"/>
</svg>

After

Width:  |  Height:  |  Size: 980 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M12 11A1 1 0 1 0 12 13A1 1 0 1 0 12 11Z"/>
<path d="M12,18c-3.309,0-6-2.691-6-6s2.691-6,6-6s6,2.691,6,6S15.309,18,12,18z M12,8c-2.206,0-4,1.794-4,4s1.794,4,4,4s4-1.794,4-4S14.206,8,12,8z"/>
<path d="M12,22C6.486,22,2,17.514,2,12S6.486,2,12,2s10,4.486,10,10S17.514,22,12,22z M12,4c-4.411,0-8,3.589-8,8s3.589,8,8,8s8-3.589,8-8S16.411,4,12,4z"/>
<path d="M8.566 14.02C8.215 13.425 8 12.741 8 12c0-2.209 1.791-4 4-4 .741 0 1.425.215 2.02.566l4.304-4.304C16.599 2.85 14.397 2 12 2 6.486 2 2 6.486 2 12c0 2.397.85 4.599 2.262 6.324L8.566 14.02zM15.434 9.98C15.785 10.575 16 11.259 16 12c0 2.209-1.791 4-4 4-.741 0-1.425-.215-2.02-.566l-4.304 4.304C7.401 21.15 9.603 22 12 22c5.514 0 10-4.486 10-10 0-2.397-.85-4.599-2.262-6.324L15.434 9.98z"/>
</svg>

After

Width:  |  Height:  |  Size: 822 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 10.9A1.1 1.1 0 1 0 12 13.1 1.1 1.1 0 1 0 12 10.9zM7.7 14.5C7.3 13.8 7 12.9 7 12c0-2.8 2.2-5 5-5 .9 0 1.8.3 2.5.7L18 1.6c-1.8-1-3.8-1.6-6-1.6C5.4 0 0 5.4 0 12c0 2.2.6 4.3 1.6 6L7.7 14.5zM22.4 6l-6.1 3.5c.4.7.7 1.6.7 2.5 0 2.8-2.2 5-5 5-.9 0-1.8-.3-2.5-.7L6 22.4c1.8 1 3.8 1.6 6 1.6 6.6 0 12-5.4 12-12C24 9.8 23.4 7.7 22.4 6z"/><g><path d="M12,1.5c5.8,0,10.5,4.7,10.5,10.5S17.8,22.5,12,22.5S1.5,17.8,1.5,12S6.2,1.5,12,1.5 M12,17.5 c3,0,5.5-2.5,5.5-5.5S15,6.5,12,6.5S6.5,9,6.5,12S9,17.5,12,17.5 M12,0C5.4,0,0,5.4,0,12c0,6.6,5.4,12,12,12s12-5.4,12-12 C24,5.4,18.6,0,12,0L12,0z M12,16c-2.2,0-4-1.8-4-4s1.8-4,4-4s4,1.8,4,4S14.2,16,12,16L12,16z"/></g></svg>

After

Width:  |  Height:  |  Size: 723 B

4
web/icons/New-47652.svg Normal file
View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<path d="M1.929 5h1.362l1.049-.338c.153-.05.273-.169.323-.323l.44-1.366 1.403.302c.156.033.321-.01.44-.118L8.01 2.194l1.065.963c.119.107.282.149.44.118l1.402-.302.44 1.366c.049.153.169.273.322.323L12.729 5h1.362l.028-.13c.054-.251-.092-.502-.336-.581l-1.552-.5L11.73 2.236c-.078-.243-.326-.39-.581-.335L9.556 2.244l-1.21-1.095c-.19-.172-.48-.172-.671 0l-1.21 1.094L4.87 1.9c-.25-.057-.502.091-.581.335l-.5 1.553-1.552.5c-.245.079-.39.33-.335.581L1.929 5zM14.087 11h-1.295l-1.111.358c-.153.049-.273.169-.322.322l-.44 1.365-1.403-.301c-.155-.032-.32.011-.439.118L8.01 13.826l-1.064-.963c-.093-.084-.213-.129-.335-.129-.035 0-.07.004-.105.011l-1.403.301-.44-1.365c-.05-.153-.169-.273-.323-.322L3.228 11H1.934l-.032.15c-.054.251.091.502.335.58l1.552.501.5 1.552c.079.244.332.384.581.336l1.594-.342 1.21 1.094C7.77 14.957 7.89 15 8.01 15s.24-.043.335-.129l1.21-1.094 1.595.342c.255.05.502-.092.58-.336l.501-1.552 1.552-.501c.244-.078.39-.329.336-.58L14.087 11z"/>
<path fill="none" stroke="#000" stroke-miterlimit="10" d="M2.5 6L2.5 10M4.5 6L4.5 10M2.5 7.25L4.5 8.75M9 9.5L6.5 9.5 6.5 6.5 8.5 6.5 8.5 7.75 6.5 8.5M10.5 6L10.5 9.375 12 8.5 13.5 9.375 13.5 6"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 30">
<path fill="none" stroke="#000" stroke-linecap="round" stroke-miterlimit="10" stroke-width="2" d="M5 16v2c0 2.5 2 3 2 3s2 6 8 6M25 16v2c0 2.5-2 3-2 3s-2 6-8 6"/>
<path fill="none" stroke="#000" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M11 20L11 20c-1.105 0-2-.895-2-2v-1c0-.552.448-1 1-1h2c.552 0 1 .448 1 1v1C13 19.105 12.105 20 11 20zM19 20L19 20c-1.105 0-2-.895-2-2v-1c0-.552.448-1 1-1h2c.552 0 1 .448 1 1v1C21 19.105 20.105 20 19 20z"/>
<path fill="none" stroke="#000" stroke-linecap="round" stroke-miterlimit="10" stroke-width="2" d="M24,15c0-0.512,0-0.488,0-1c0-4.971-4.029-9-9-9s-9,4.029-9,9c0,0.512,0,0.488,0,1"/>
<path d="M15 4v4c0 4 8 3 8 7 2 0 2 4 2 4s2-3 2-6S24 4 15 4zM15 4v4c0 4-8 3-8 7-2 0-2 4-2 4s-2-3-2-6S6 4 15 4z"/>
<path fill="none" stroke="#000" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M13 17L17 17"/>
<path d="M15 1A5 3.5 0 1 0 15 8A5 3.5 0 1 0 15 1Z"/>
</svg>

After

Width:  |  Height:  |  Size: 1008 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 30"><path d="M20 5A3 3 0 1 0 20 11 3 3 0 1 0 20 5zM15 15c-1.657 0-3 1.343-3 3s1.343 3 3 3 3-1.343 3-3S16.657 15 15 15zM15 19c-.552 0-1-.448-1-1 0-.552.448-1 1-1s1 .448 1 1C16 18.552 15.552 19 15 19z"/><path d="M29.094 15.202l-3.277-5.664c-.537-.925-1.525-1.495-2.595-1.495H15V17h2.181C17.067 17.482 17 17.983 17 18.5c0 3.59 2.91 6.5 6.5 6.5s6.5-2.91 6.5-6.5C30 17.294 29.666 16.169 29.094 15.202zM23.5 23c-2.485 0-4.5-2.015-4.5-4.5s2.015-4.5 4.5-4.5 4.5 2.015 4.5 4.5S25.985 23 23.5 23zM10 5A3 3 0 1 0 10 11 3 3 0 1 0 10 5z"/><path d="M0,18.5C0,22.09,2.91,25,6.5,25s6.5-2.91,6.5-6.5c0-0.517-0.067-1.018-0.181-1.5H15V8.043H6.778c-1.07,0-2.058,0.569-2.595,1.495l-3.277,5.664C0.334,16.169,0,17.294,0,18.5z M2,18.5C2,16.015,4.015,14,6.5,14s4.5,2.015,4.5,4.5S8.985,23,6.5,23S2,20.985,2,18.5z"/></svg>

After

Width:  |  Height:  |  Size: 851 B

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128">
<path d="M66.4,103.2c2.1,12,12.3,20.8,24.1,20.8c1.4,0,2.7-0.1,4.1-0.4c6.4-1.1,11.9-4.8,15.6-10.2c3.7-5.4,5-12,3.9-18.6C112,82.8,101.9,74,90,74c-1.4,0-2.7,0.1-4.1,0.4C72.8,76.7,64,89.7,66.4,103.2z M87,80.3c1-0.2,2-0.3,3.1-0.3c8.9,0,16.6,6.7,18.2,15.8c0.9,5-0.2,10.1-3,14.2c-2.8,4.1-6.9,6.8-11.7,7.7c-1,0.2-2,0.3-3.1,0.3c-8.9,0-16.6-6.7-18.2-15.8C70.5,91.9,77.1,82.1,87,80.3z"/>
<path d="M83.7 109.9c.5.4 1.2.6 1.8.6 1 0 2-.5 2.6-1.4l9.6-16.4c1-1.4.6-3.4-.8-4.4-.5-.4-1.2-.6-1.8-.6-1 0-2 .5-2.6 1.4l-9.6 16.4C81.9 106.9 82.3 108.9 83.7 109.9zM20.8 120.9L21 121c3 1.7 6.4 2.6 9.8 2.6 7 0 13.5-3.8 17-9.8l11.7-20.3c0 0 0 0 0 0 0 0 0 0 0 0l11.7-20.3c5.4-9.4 2.2-21.4-7.2-26.8l-.1-.1c-3-1.7-6.4-2.6-9.8-2.6-7 0-13.5 3.8-17 9.8L25.4 73.8c0 0 0 0 0 0 0 0 0 0 0 0L13.7 94.1C8.2 103.5 11.5 115.5 20.8 120.9zM42.3 56.4c2.4-4.2 6.9-6.8 11.8-6.8 2.4 0 4.7.6 6.8 1.8l.1.1c6.5 3.8 8.7 12.1 5 18.6L55.8 87.9 32.1 74.2 42.3 56.4zM18.8 97.1l10.2-17.7 17.4 10.1-10.3 17.9c-2.8 4.8-7.5 7.7-12.6 8.2C17.3 111.7 15.2 103.5 18.8 97.1z"/>
<path d="M50.4,56.6c-1.4-0.8-3.3-0.3-4.1,1.1l-7,12.1c-0.8,1.4-0.3,3.3,1.1,4.1c0.5,0.3,1,0.4,1.5,0.4c1,0,2-0.5,2.6-1.5l7-12.1C52.3,59.2,51.8,57.4,50.4,56.6z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 30">
<path d="M13,21c0-2.57,1.217-4.851,3.1-6.314l-3.873-3.873c-0.391-0.391-0.391-1.024,0-1.414l2.664-2.664c1.154-1.154,2.671-1.731,4.188-1.731c1.516,0,3.033,0.577,4.187,1.731c1.887,1.887,2.205,4.73,1.007,6.971c0.622,0.279,1.202,0.634,1.727,1.055c1.669-3.012,1.233-6.886-1.32-9.44c-1.616-1.616-3.808-2.448-6.113-2.304c-2.004,0.125-3.868,1.082-5.288,2.502l-7.735,7.735c-3.084,3.084-3.363,8.154-0.345,11.302C6.708,26.13,8.748,27,10.921,27c1.367,0,2.679-0.349,3.842-0.996C13.662,24.633,13,22.895,13,21z"/>
<path d="M21,15c-3.314,0-6,2.686-6,6s2.686,6,6,6s6-2.686,6-6S24.314,15,21,15z M22.414,23.828l-4.243-4.243c-0.391-0.391-0.39-1.024,0-1.414c0.391-0.391,1.024-0.391,1.414,0l4.243,4.243c0.391,0.391,0.391,1.024,0,1.414C23.438,24.219,22.805,24.219,22.414,23.828z"/>
</svg>

After

Width:  |  Height:  |  Size: 829 B

View File

@@ -0,0 +1,24 @@
<svg xmlns="http://www.w3.org/2000/svg" id="leia" viewBox="0 0 100 100">
<path fill="#fff" d="M20.3 56.1A.9.9 0 1 0 20.3 57.9.9.9 0 1 0 20.3 56.1zM79.3 31.400000000000002A.7.7 0 1 0 79.3 32.800000000000004.7.7 0 1 0 79.3 31.400000000000002z"/>
<path fill="#f1bc19" d="M76.7 12.2A.9.9 0 1 0 76.7 14 .9.9 0 1 0 76.7 12.2zM20.7 63.800000000000004A.9.9 0 1 0 20.7 65.60000000000001.9.9 0 1 0 20.7 63.800000000000004z"/>
<path fill="#f9dbd2" d="M49.6 12.799999999999997A37.5 37.5 0 1 0 49.6 87.8A37.5 37.5 0 1 0 49.6 12.799999999999997Z"/>
<path fill="#f1bc19" d="M82.1 11.2A4 4 0 1 0 82.1 19.2A4 4 0 1 0 82.1 11.2Z"/>
<path fill="#ee3e54" d="M86.1 22.400000000000002A1.9 1.9 0 1 0 86.1 26.2A1.9 1.9 0 1 0 86.1 22.400000000000002Z"/>
<path fill="#fbcd59" d="M77.1 74.8A1.9 1.9 0 1 0 77.1 78.60000000000001 1.9 1.9 0 1 0 77.1 74.8zM14 59.8A4 4 0 1 0 14 67.8 4 4 0 1 0 14 59.8z"/>
<path fill="#ee3e54" d="M24.2 86A1.9 1.9 0 1 0 24.2 89.80000000000001A1.9 1.9 0 1 0 24.2 86Z"/>
<path fill="#fff" d="M75.8 28.8A1.8 1.8 0 1 0 75.8 32.4 1.8 1.8 0 1 0 75.8 28.8zM17.7 49.2A2.5 2.5 0 1 0 17.7 54.2 2.5 2.5 0 1 0 17.7 49.2z"/>
<path fill="#51302f" d="M62.9 35.8A8.3 12.3 0 1 0 62.9 60.400000000000006 8.3 12.3 0 1 0 62.9 35.8zM37.9 35.8A8.3 12.3 0 1 0 37.9 60.400000000000006 8.3 12.3 0 1 0 37.9 35.8z"/>
<path fill="#d8ad8f" d="M61.3,44.8c0.1-1.4,0.5-3.1,0.6-3.8l-1.1-3.1c-1.3-1.6-2.1-3-3.8-4.1c-1.9-1.2-4.4-0.5-6.6-1 c-5.4-1.4-10.4,4.1-12,9.2c0,0,0,0.1,0,0.1c0.2,0.9,0.3,1.9,0.3,2.8c-1.5,0-2.2,0.8-1.7,2.3c0,0,0.6,2.5,0.9,3.7 c0.1,0.7,0.7,1.2,1.4,1.2c0,0.8,0,1.6,0,2.4c0,1,0.3,1.9,0.8,2.7c1.4,2.1,8.8,6.8,10.5,6.7c2.9,0,8.6-4.9,10-7.3 c0.3-0.5,0.4-1,0.5-1.6c0-0.9,0-1.9,0-2.8c0.6-0.1,1.1-0.5,1.2-1.1c0.4-1.2,0.9-3.7,1-3.7C63.8,45.5,62.7,44.7,61.3,44.8z"/>
<path fill="#b2876b" d="M65.6,69l-0.3-0.1l-0.5-0.2c-1.2-0.5-8.8-3.1-8.8-3.1c-0.5-0.1-0.9-0.5-1.2-1 c-0.2-0.9-0.3-1.8-0.2-2.8c-1.3,0.6-2.6,1-4,1.3c-1.8,0.2-4.9-2.1-4.9-2.1s-0.1,2.9-0.2,3.5c-0.2,0.6-0.6,1-1.2,1.1L37,68 c-0.7,0.2-1.4,0.5-2.1,0.9c-1.6,1-2.7,2.6-3.1,4.4l0,0c0,0.2-0.1,0.4-0.1,0.6c-0.1,0.4-0.1,0.8-0.1,1.3c0,0.4-0.1,6.9-0.1,7.4 c6.5,3.8,11,5.1,21,5.1c4.4,0,13.3-3.9,16.7-5.7C68.8,73.1,69.7,70.3,65.6,69z"/>
<path fill="#fdfcef" d="M65.6,69.1L65.3,69l-0.5-0.2c-1-0.4-6.2-2.2-8.1-2.9c-3.6,1.2-7.4,0.9-11.1,0.9 c-0.8,0-1.5-0.3-2-0.8L37,68.2c-2.6,0.6-4.6,2.7-5.2,5.3l0,0c0,0.2-0.1,0.4-0.1,0.6c-0.1,0.4-0.1,0.8-0.1,1.3 c0,0.4-0.1,6.9-0.1,7.4c6.6,3.8,11.1,5.1,21.1,5.1c4.4,0,13.3-3.9,16.7-5.7C68.8,73.2,69.7,70.4,65.6,69.1z"/>
<path fill="#472b29" d="M39.6 52.6c-.1 0-.2-.1-.2-.2l-.8-7.6c0-.1.1-.2.2-.2.1 0 .2.1.2.2l.8 7.6C39.8 52.5 39.7 52.6 39.6 52.6 39.6 52.6 39.6 52.6 39.6 52.6zM61 52.2C61 52.2 61 52.2 61 52.2c-.1 0-.2-.1-.2-.2l.5-7.7c0-.1.1-.2.2-.2.1 0 .2.1.2.2L61.2 52C61.2 52.1 61.1 52.2 61 52.2zM50.6 63.3c-1.9 0-4.8-1.6-4.9-1.7-.1-.1-.2-.2-.1-.3.1-.1.2-.2.3-.1 0 0 3.2 1.7 4.9 1.6 1.4-.3 2.8-.8 4.1-1.5.1-.1.3 0 .3.1.1.1 0 .3-.1.3-1.3.8-2.7 1.3-4.2 1.6C50.8 63.3 50.7 63.3 50.6 63.3z"/>
<path fill="#472b29" d="M50.4,52.6c0.8,0,1.5-0.3,2.1-0.9c0-0.1,0-0.1,0-0.2l0,0 c-0.1-0.1-0.1-0.1-0.2,0c0,0,0,0,0,0c-1,1-2.7,1-3.7,0c0,0-0.1,0-0.1,0c0,0-0.1,0-0.1,0c0,0.1,0,0.1,0,0.2 C48.9,52.3,49.6,52.6,50.4,52.6z" opacity=".41"/>
<path fill="#472b29" d="M47.9 42.2l.5-1c-2.2-.5-4.5-.4-6.6.2l.6.9C44.2 41.8 46.1 41.8 47.9 42.2zM58.6 42.2l.5-1c-2.2-.5-4.5-.4-6.6.2l.6.9C54.9 41.8 56.8 41.8 58.6 42.2zM44.6 43.6c-.5 0-1 .4-1.2.8.1 0 .1 0 .2 0 .5.1.8.5.7 1-.1.4-.3.7-.7.7-.1 0-.3 0-.4-.1.1.7.7 1.3 1.4 1.3 1-.3 1.7-1.3 1.4-2.3C45.8 44.3 45.3 43.8 44.6 43.6L44.6 43.6zM55.8 43.6c-.5 0-.9.3-1.1.6.1 0 .2-.1.3-.1.5.1.8.5.7 1-.1.4-.3.7-.7.7-.3 0-.5-.1-.6-.4.2.8 1.1 1.3 1.9 1.1s1.3-1.1 1.1-1.9C57.1 44.1 56.5 43.6 55.8 43.6zM56.6 55.1c.1-.1.1-.1 0-.2 0 0 0 0 0 0l0 0c-.1-.1-.1-.1-.2 0 0 0 0 0 0 0-1.8 1.2-3.9 1.8-6 1.7-2.1.1-4.2-.5-6-1.7-.1-.1-.1-.1-.2 0-.1.1-.1.1 0 .2 0 0 0 0 0 0C47.9 57.7 52.8 57.7 56.6 55.1L56.6 55.1z"/>
<path fill="#cd9480" d="M43.2 49.5A2.2 2.2 0 1 0 43.2 53.900000000000006 2.2 2.2 0 1 0 43.2 49.5zM57.4 49.5A2.2 2.2 0 1 0 57.4 53.900000000000006 2.2 2.2 0 1 0 57.4 49.5z"/>
<path fill="#472b29" d="M49.9,88.2c-6.5,0-13-1.7-18.7-5.2c-0.1-0.1-0.2-0.2-0.2-0.4c-0.1-5.4,0.4-12.3,3.8-14 c0.7-0.4,1.4-0.7,2.1-0.9l7.3-2.5c0.5-0.1,0.8-0.4,0.9-0.7c0.1-0.4,0.2-1.9,0.2-2.7c-2.1-1.3-4.7-3.2-5.5-4.5 c-0.6-0.9-0.9-1.9-0.9-3l0-1.9c-0.7-0.2-1.2-0.7-1.4-1.5c-0.4-1.1-0.9-3.6-0.9-3.7c-0.4-1-0.1-1.7,0.2-2.1 c0.2-0.3,0.7-0.7,1.5-0.8c0-0.1,0-0.3,0-0.4c-0.3-0.5-0.4-1.1-0.5-1.7C37.7,36.1,43.2,31,50,31c6.8,0,12.3,5,12.3,11.2 c0,0.7-0.2,1.3-0.5,1.8c0,0.1,0,0.2,0,0.3c0.8,0.1,1.4,0.4,1.8,0.9c0.3,0.4,0.5,1,0.2,2c-0.1,0.6-0.7,2.8-0.9,3.7 c-0.1,0.6-0.6,1.1-1.2,1.4v2.4c0,0.7-0.2,1.3-0.5,1.8c-1.7,2.3-3.7,4.2-6.1,5.8c0,0.7,0,1.4,0.2,2.1c0.1,0.3,0.4,0.5,0.8,0.6 c0.3,0.1,7.7,2.6,8.9,3.1l0.9,0.4c3.6,1.2,3.7,5.8,3.8,12.3l0,1c0,0.2-0.1,0.3-0.2,0.4C63.6,86.3,56.7,88.2,49.9,88.2z M31.9,82.3c11.3,6.8,25.7,6.5,36.8-0.6l0-0.7c-0.1-6.3-0.2-10.5-3.2-11.4c0,0-0.1,0-0.1,0l-0.3-0.2l-0.5-0.2 c-1.1-0.5-8.7-3.1-8.8-3.1c-0.6-0.2-1.2-0.6-1.4-1.3c-0.3-1-0.3-1.9-0.3-2.8c0-0.2,0.1-0.3,0.2-0.4c2.3-1.5,4.3-3.4,6-5.6 c0.2-0.4,0.3-0.8,0.3-1.3V52c0-0.3,0.2-0.5,0.4-0.5c0.4-0.1,0.7-0.3,0.8-0.7c0.4-1.2,0.9-3.6,0.9-3.7c0.2-0.5,0.1-0.9-0.1-1.2 c-0.3-0.3-0.8-0.5-1.5-0.5c-0.1,0-0.3,0-0.4-0.1c-0.1-0.1-0.2-0.2-0.2-0.4c0-0.7,0-1.1,0.2-1.3c0.2-0.4,0.3-0.9,0.3-1.3 C61.2,36.6,56.2,32,50,32c-6.2,0-11.3,4.6-11.3,10.3c0,0.4,0.2,0.9,0.4,1.3c0,0,0,0.1,0,0.2c0,0.4,0.1,0.7,0.1,1.1 c0,0.1,0,0.3-0.1,0.4c-0.1,0.1-0.2,0.2-0.4,0.2c-0.6,0-1,0.1-1.2,0.4c-0.2,0.3-0.1,0.8,0,1.2c0,0.1,0.6,2.5,0.9,3.7 c0.1,0.5,0.4,0.8,0.9,0.8c0.1,0,0.3,0.1,0.4,0.1c0.1,0.1,0.1,0.2,0.1,0.4v2.4c0,0.9,0.2,1.7,0.7,2.4c0.7,1,3,2.8,5.5,4.3 c0.1,0.1,0.2,0.3,0.2,0.4c0,0.7-0.2,2.6-0.3,3.2c-0.2,0.8-0.8,1.3-1.6,1.5l-7.3,2.5c-0.7,0.2-1.4,0.5-2,0.8 C32.9,70.6,31.8,75.2,31.9,82.3z"/>
<path fill="#472b29" d="M50.4,30.5c-7,0-12.7,5.2-12.7,11.6c0,0.6,0.2,1.2,0.5,1.8c1.5-5.7,8.6-8.4,12.2-8.4 s10.6,2.4,12.2,8.4c0.3-0.6,0.4-1.2,0.4-1.8C63.1,35.7,57.4,30.5,50.4,30.5z"/>
<path fill="#472b29" d="M50.4,28.3c-7.6,0-13.8,5.2-13.8,11.6c0,0.6,0.2,1.2,0.5,1.8c1.7-5.7,9.4-8.4,13.3-8.4 s11.6,2.4,13.3,8.4c0.3-0.6,0.4-1.2,0.5-1.8C64.2,33.5,58,28.3,50.4,28.3z"/>
<path fill="#472b29" d="M51.2 34.6c.3.1.5.2.7.3 1.3.7 2.4 2 2.8 3.4.5 1.4.8 2.8 1.5 4.1.6 1.5 1.9 2.6 3.4 3 1.4.2 2.8.2 4.2-.2.3-1.2.3-2.5.2-3.8-.1-1.4-.6-2.7-1.3-3.9-1.9-2.8-6.3-4.3-10.2-3.3M49.6 34.6c-.3.1-.5.2-.7.3-1.3.7-2.4 2-2.8 3.4-.4 1.4-.9 2.8-1.5 4.1-.6 1.5-1.9 2.6-3.4 3-1.4.2-2.8.2-4.2-.2-.3-1.2-.3-2.5-.2-3.8.1-1.4.6-2.7 1.3-3.9 1.9-2.8 6.3-4.3 10.2-3.3"/>
<path fill="#e1e0d8" d="M56.8,68.4c-2.3,0.5-4.6,0.8-7,0.7c-2.4,0.1-4.7-0.2-7-0.7c-0.9-0.1-1.7-0.9-1.6-1.8l0,0 c-0.1-0.9,0.7-1.7,1.6-1.8c0,0,4.4,0.5,6.8,0.5s7.1-0.5,7.1-0.5c0.9,0.1,1.7,0.9,1.6,1.8l0,0C58.4,67.5,57.7,68.3,56.8,68.4z"/>
<path fill="#472b29" d="M49.1,69.4c-2.1,0-4.3-0.2-6.4-0.7c-1.1-0.1-1.9-1-1.9-2.2c-0.1-1.1,0.8-2.1,1.9-2.1 c0.1,0,4.4,0.5,6.9,0.5c2.4,0,7-0.5,7-0.5c1.2,0.1,2,1,2,2.2c0.1,1.1-0.7,2-1.8,2.1c-2.3,0.5-4.7,0.8-7.1,0.7 C49.5,69.4,49.3,69.4,49.1,69.4z M42.7,65.1c-0.7,0-1.2,0.7-1.2,1.4c0,0.8,0.5,1.4,1.3,1.5c2.3,0.5,4.7,0.8,7,0.7 c2.3,0,4.7-0.2,6.9-0.7l0,0c0.7-0.1,1.3-0.7,1.2-1.4c0-0.8-0.5-1.4-1.3-1.5c-0.1,0-4.6,0.5-7.1,0.5 C47.1,65.6,42.9,65.1,42.7,65.1z"/>
</svg>

After

Width:  |  Height:  |  Size: 7.1 KiB

View File

@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 50">
<path fill="none" stroke="#000" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M17,28L36.8,8.1c0.4-0.4,0.8-0.6,1.3-0.6l1.1-0.1c0.5-0.1,1-0.3,1.4-0.7L43,4.3l-0.9-0.9l-5.9,1.1c-0.5,0.1-1,0.4-1.4,0.7L14.6,25.6L17,28z"/>
<path d="M35.3 2.1999999999999997A.6.6 0 1 0 35.3 3.4.6.6 0 1 0 35.3 2.1999999999999997zM37 1.7999999999999998A.6.6 0 1 0 37 3 .6.6 0 1 0 37 1.7999999999999998zM38.6 1.5A.6.6 0 1 0 38.6 2.7.6.6 0 1 0 38.6 1.5zM40.2 1.2000000000000002A.6.6 0 1 0 40.2 2.4.6.6 0 1 0 40.2 1.2000000000000002zM41.8 1A.6.6 0 1 0 41.8 2.2.6.6 0 1 0 41.8 1z"/>
<path fill="none" stroke="#000" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M22.4,34l18.4-18.4c0.4-0.4,0.8-0.6,1.3-0.6l1.1-0.1c0.5-0.1,1-0.3,1.4-0.7l2.4-2.4l-0.9-0.9L40.2,12c-0.5,0.1-1,0.4-1.4,0.7L20,31.6L22.4,34z"/>
<path fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M26.638,29.763L26.638,29.763L22.4,34L20,31.6l4.576-4.6c-0.51,0-1.329,0-1.576,0c-1.105,0-2-0.895-2-2c0-0.365,0-1,0-1l-0.008-0.013L17,28l-2.4-2.4l4.603-4.649l-0.03-0.05C18.359,19.751,17.017,19,15.5,19c-2.316,0-4.223,1.75-4.472,3.999C11.009,23.164,11,23.331,11,23.5V24c-0.1,1.6-1,3-3,3c-0.054,0-0.085,0.018-0.122,0.031C4.027,27.348,1,30.568,1,34.5c0,0.154,0.013,0.382,0.013,0.5c-0.136,2.987,1.018,6.287,3.887,9.1C7.389,46.589,10,48,13.5,48s5.747-2.347,6.4-3c2.3-2.3,1.868-4.153,3.1-6c2-3,6-2.625,6-6.5C29,30.729,27.944,29.763,26.638,29.763z"/>
<path d="M39.3 9.700000000000001A.6.6 0 1 0 39.3 10.9.6.6 0 1 0 39.3 9.700000000000001zM41 9.3A.6.6 0 1 0 41 10.5.6.6 0 1 0 41 9.3zM42.6 9A.6.6 0 1 0 42.6 10.2.6.6 0 1 0 42.6 9zM44.2 8.700000000000001A.6.6 0 1 0 44.2 9.9.6.6 0 1 0 44.2 8.700000000000001zM45.8 8.5A.6.6 0 1 0 45.8 9.7.6.6 0 1 0 45.8 8.5z"/>
<path d="M16.284 34.887H17.715V39.113H16.284z" transform="rotate(-45.001 17 37)"/>
<path d="M10.284 28.887H11.715V33.113H10.284z" transform="rotate(-45.001 11 31)"/>
<path d="M16 42A1 1 0 1 0 16 44 1 1 0 1 0 16 42zM12 43A1 1 0 1 0 12 45 1 1 0 1 0 12 43z"/>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

6
web/icons/Punk-40450.svg Normal file
View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 50">
<path d="M35.5 27A1.5 1.5 0 1 0 35.5 30A1.5 1.5 0 1 0 35.5 27Z"/>
<path d="M47.727,10.313c-0.312-0.329-0.805-0.408-1.203-0.192L36,15.821V3c0-0.458-0.312-0.857-0.755-0.97C34.8,1.919,34.336,2.122,34.12,2.526L28,13.891L21.88,2.526c-0.217-0.404-0.679-0.607-1.125-0.496C20.312,2.143,20,2.542,20,3v12.83L7.472,9.119C7.06,8.897,6.548,8.993,6.241,9.349C5.936,9.705,5.918,10.225,6.2,10.6l7.986,10.649L1.858,23.01c-0.451,0.064-0.801,0.425-0.852,0.877c-0.051,0.452,0.209,0.882,0.635,1.046l11.394,4.382l-9.668,7.911c-0.349,0.286-0.463,0.769-0.28,1.181C3.249,38.771,3.61,39,4,39c0.05,0,0.102-0.004,0.152-0.012l12.685-1.951C15.894,35.111,15,33.108,15,29c0-6.118,3.267-13,13-13c6.638,0,10.512,3.723,11.643,8.759c0.08-0.068,0.153-0.144,0.209-0.235l8-13C48.089,11.139,48.038,10.642,47.727,10.313z"/>
<path fill="none" stroke="#000" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M34,48v-3c0,0,1.529,0,4,0c1.954,0,3-1.794,3-3c0-0.909,0-7,0-7h3c0.892,0,1.16-0.895,0.812-1.521L40,28c0-6.599-4-12-12-12c-9.733,0-13,6.882-13,13c0,8.69,4,7.963,4,17v2"/>
<path fill="none" stroke="#000" stroke-linecap="round" stroke-miterlimit="10" stroke-width="2" d="M25,37L25,37c-2.2,0-4-1.8-4-4v-1c0-1.65,1.35-3,3-3l0,0c0,0,2-0.042,2,2"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

27
web/icons/R2-D2-39423.svg Normal file
View File

@@ -0,0 +1,27 @@
<svg xmlns="http://www.w3.org/2000/svg" id="r2d2" viewBox="0 0 48 48">
<path fill="#9fa8da" d="M9 39H12V41H9zM11 15H37V22H11z"/>
<path fill="#e8eaf6" d="M40 37L39 39 36 39 36 14 40 14z"/>
<path fill="#c5cae9" d="M36 24H40V34H36zM34 15v-4c0-5.5-4.5-10-10-10S14 5.5 14 11v4H34z"/>
<path fill="#3f51b5" d="M22 7H26V14H22z"/>
<path fill="#000001" d="M24 8A1 1 0 1 0 24 10A1 1 0 1 0 24 8Z"/>
<path fill="#ffff8d" d="M24.5 12A0.5 0.5 0 1 0 24.5 13A0.5 0.5 0 1 0 24.5 12Z"/>
<path fill="#7986cb" d="M28 11A1 1 0 1 0 28 13A1 1 0 1 0 28 11Z"/>
<path fill="#3f51b5" d="M31.119 4C29.302 2.152 26.78 1 24 1s-5.302 1.152-7.119 3H31.119zM34 11c0-.337-.018-.671-.051-1H30v4h4V11zM21.06 14v-4H14.11c-.033.329-.051.663-.051 1v3H21.06z"/>
<path fill="#e8eaf6" d="M13 14H35V37H13z"/>
<path fill="#3f51b5" d="M17 15H31V17H17zM17 18H31V20H17zM22 22H26V31H22zM22 33H26V37H22z"/>
<path fill="#90a4ae" d="M30 33H32V36H30zM15 32H17V36H15z"/>
<path fill="#cfd8dc" d="M24 23L24 23c.552 0 1 .448 1 1v1c0 .552-.448 1-1 1h0c-.552 0-1-.448-1-1v-1C23 23.448 23.448 23 24 23zM24 27L24 27c.552 0 1 .448 1 1v1c0 .552-.448 1-1 1h0c-.552 0-1-.448-1-1v-1C23 27.448 23.448 27 24 27z"/>
<path fill="#000001" d="M24 34A1 1 0 1 0 24 36A1 1 0 1 0 24 34Z"/>
<path fill="#9fa8da" d="M31 40L17 40 15 37 33 37z"/>
<path fill="#3f51b5" d="M40 16H41V20H40z"/>
<path fill="#e8eaf6" d="M8 37L9 39 12 39 12 14 8 14z"/>
<path fill="#c5cae9" d="M8 23H12V33H8z"/>
<path fill="#3f51b5" d="M7 16H8V20H7z"/>
<path fill="#c5cae9" d="M8 40L7 46 8 47 13 47 14 46 14 40z"/>
<path fill="#9fa8da" d="M13,45c-0.552,0-1-0.448-1-1v-5c0-0.55,0.45-1,1-1h0c0.55,0,1,0.45,1,1v5C14,44.552,13.552,45,13,45L13,45z"/>
<path fill="#8d6e63" d="M13,45h-3c-0.55,0-1-0.45-1-1l0,0c0-0.55,0.45-1,1-1h3c0.55,0,1,0.45,1,1l0,0C14,44.55,13.55,45,13,45z"/>
<path fill="#9fa8da" d="M36 39H39V41H36z"/>
<path fill="#c5cae9" d="M40 40L41 46 40 47 35 47 34 46 34 40z"/>
<path fill="#9fa8da" d="M35,45c0.552,0,1-0.448,1-1v-5c0-0.55-0.45-1-1-1l0,0c-0.55,0-1,0.45-1,1v5C34,44.552,34.448,45,35,45L35,45z"/>
<path fill="#8d6e63" d="M35,45h3c0.55,0,1-0.45,1-1l0,0c0-0.55-0.45-1-1-1h-3c-0.55,0-1,0.45-1,1l0,0C34,44.55,34.45,45,35,45z"/>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

12
web/icons/Rap-24851.svg Normal file
View File

@@ -0,0 +1,12 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 50">
<path d="M14,44l1.023,0.606C15.262,41.481,18.295,39,22,39c3.86,0,7,2.691,7,6c0,0.091-0.011,0.18-0.016,0.271L30,45l0.959-0.719C30.548,40.206,26.689,37,22,37c-4.238,0-7.793,2.621-8.743,6.134L14,44z"/>
<path fill="none" stroke="#000" stroke-linecap="round" stroke-miterlimit="10" stroke-width="2" d="M36 21v5c0 0 3 .304 3 4 0 1.99-1.092 3.805-3 4M9 34c-1.908-.195-3-2.01-3-4 0-3.696 3-4 3-4v-5"/>
<path d="M38 21.625c0 0 .77 1.116 1.263 5.447C37.706 25.794 36 26.48 36 26.48l-.3-4.855H38zM7.263 21.625c0 0-.77 1.116-1.263 5.447 1.557-1.278 3.263-.592 3.263-.592l.3-4.855H7.263z"/>
<path fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2.066" d="M17.95,3h9.099c5.095,0,9.376,3.831,9.939,8.896L38,21H7l1.012-9.104C8.574,6.831,12.855,3,17.95,3z"/>
<path fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2.066" d="M22,17h-8c-1.1,0-2-0.9-2-2v0c0-1.1,0.9-2,2-2h8c1.1,0,2,0.9,2,2v0C24,16.1,23.1,17,22,17z"/>
<path fill="none" stroke="#000" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2.066" d="M38,21c0,0,4.107,3.36,6,5c3.031,2.625,7.229,8-7.5,8"/>
<path d="M19,46c0,0,1.719-3,3-3s3,3,3,3H19z"/>
<path d="M25,47h-6c-0.356,0-0.687-0.19-0.865-0.499s-0.18-0.688-0.002-0.998C18.736,44.449,20.32,42,22,42s3.264,2.449,3.868,3.503c0.177,0.31,0.176,0.689-0.002,0.998S25.356,47,25,47z M20.897,45H23.1c-0.457-0.56-0.888-0.961-1.117-1.001C21.773,44.039,21.351,44.44,20.897,45z"/>
<path fill="none" stroke="#000" stroke-linecap="round" stroke-miterlimit="10" stroke-width="2" d="M36,34c0,7.18-6.044,13-13.5,13S9,41.18,9,34"/>
<path d="M29,24.007c-0.643,0-1.256-0.014-1.821,0h-9.353c-0.566-0.014-1.181,0-1.826,0c-2.761,0-5-0.279-5,2.664C11,29.615,13.239,32,16,32c2.53,0,4.581-2.016,4.912-4.617c0.006-0.082,0.08-0.383,0.08-0.383c0.207-0.581,0.756-1,1.408-1h0.2c0.652,0,1.202,0.419,1.408,1c0,0,0.074,0.301,0.08,0.383C24.419,29.984,26.47,32,29,32c2.761,0,5-2.385,5-5.329C34,23.727,31.761,24.007,29,24.007z"/>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 50">
<path d="M31.908 28C31.964 27.843 32 27.676 32 27.5c0-.828-.672-1.5-1.5-1.5S29 26.672 29 27.5c0 .176.036.343.092.5H31.908zM21.908 28C21.964 27.843 22 27.676 22 27.5c0-.828-.672-1.5-1.5-1.5S19 26.672 19 27.5c0 .176.036.343.092.5H21.908z"/>
<path fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M36.027 23.729C36.653 26.565 37 29.701 37 33c0 4.398-.93 8.506-2 12M38.745 25.407C40.12 27.407 41 32.744 41 36c0 5.029-.771 8.602-2.03 12M44.705 45C44.897 43.584 45 42.071 45 40.5c0-8.008-2.25-14.5-4-14.5"/>
<path fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M41,26c-3.908,0-7.25-6-15.5-6S13.908,26,10,26c-2.792,0-4-2.131-4-6.5C6,9.835,14.73,2,25.5,2S45,9.835,45,19.5C45,23.869,43.792,26,41,26c-3.908,0-7.25-6-15.5-6S13.908,26,10,26"/>
<path fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M25.5,20c-8.25,0-11.592,6-15.5,6c-2.792,0-4-2.131-4-6.5c0-1.391,0.2-2.737,0.541-4.034C10.54,9.776,17.534,6,25.5,6s14.96,3.776,18.959,9.466C44.8,16.763,45,18.109,45,19.5c0,4.369-1.208,6.5-4,6.5C37.092,26,33.75,20,25.5,20z"/>
<path fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M25.5,12c8.086,0,15.216,3.563,19.444,8.987C44.682,24.351,43.453,26,41,26c-3.908,0-7.25-6-15.5-6S13.908,26,10,26c-2.453,0-3.682-1.649-3.944-5.013C10.284,15.563,17.414,12,25.5,12s15.216,3.563,19.444,8.987C44.682,24.351,43.453,26,41,26c-3.908,0-7.25-6-15.5-6S13.908,26,10,26c-2.453,0-3.682-1.649-3.944-5.013C10.284,15.563,17.414,12,25.5,12z"/>
<path fill="none" stroke="#000" stroke-linecap="round" stroke-miterlimit="10" stroke-width="2" d="M25.5 20c-4.493 0-7.526 1.779-9.992 3.4C15.181 24.85 15 26.394 15 28c0 8.284 4.701 15 10.5 15S36 36.284 36 28c0-1.606-.181-3.15-.508-4.6C33.026 21.779 29.993 20 25.5 20zM27 35L30 36M24 35L21 36"/>
<path d="M32.409,39.793c-0.116-0.21-0.304-0.35-0.512-0.432L32,39l-0.376,0.301c-0.193-0.018-0.392,0.001-0.574,0.102c-0.729,0.403-1.563,0.718-2.346,0.957c-0.767,0.235-1.615-0.022-2.106-0.657C26.267,39.274,25.894,39,25.5,39s-0.767,0.274-1.098,0.703c-0.49,0.635-1.338,0.892-2.106,0.657c-0.783-0.24-1.617-0.554-2.346-0.957c-0.183-0.101-0.381-0.12-0.574-0.102L19,39l0.103,0.361c-0.208,0.082-0.395,0.221-0.512,0.432c-0.268,0.483-0.092,1.092,0.391,1.359c0.239,0.132,0.487,0.253,0.738,0.369l1.391,2.123C21.666,44.49,22.609,45,23.621,45h3.758c1.011,0,1.955-0.51,2.509-1.356l1.391-2.123c0.251-0.116,0.499-0.237,0.738-0.369C32.501,40.885,32.676,40.276,32.409,39.793z"/>
<path fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M16 45c-1.07-3.494-2-7.602-2-12 0-3.299.347-6.435.973-9.271M12.03 48C10.771 44.602 10 41.029 10 36c0-3.256.88-8.593 2.255-10.593M10 26c-1.75 0-4 6.492-4 14.5 0 1.571.103 3.084.295 4.5"/>
</svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 50">
<path fill="none" stroke="#000" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M21.4,31L42.8,9.6c0.4-0.4,0.8-0.6,1.3-0.6l1.1-0.1c0.5-0.1,1-0.3,1.4-0.7L49,5.8l-0.9-0.9L42.2,6c-0.5,0.1-1,0.4-1.4,0.7L19,28.6L21.4,31z"/>
<path fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M23.1,24.2c0.2-1.3,0.1-2.5-1.1-3.7c-1.7-1.7-4.7-1.8-6.6,0l0,0c-1.1,1.1-1.6,2.4-1.7,3.5c-0.1,1.6-1.5,2.8-3.1,2.7c-2.1-0.1-4.7,0.3-6.7,2.3l0,0C0,33-0.3,39,4.9,44.1c5.2,5.2,11.1,4.8,15,0.9c2.3-2.3,2.3-4.4,2.6-6.6c0.5-4.1,6.5-2.3,6.5-6.8c0-1.9-4.6,1.2-5.6-2.6"/>
<path d="M13.4 32.1H19.6V34.2H13.4z" transform="rotate(-134.999 16.537 33.103)"/>
<path d="M11.5 34.1H13.6V40.300000000000004H11.5z" transform="rotate(134.999 12.48 37.159)"/>
<path d="M18 41A1 1 0 1 0 18 43 1 1 0 1 0 18 41zM15 43A1 1 0 1 0 15 45 1 1 0 1 0 15 43zM11 43A1 1 0 1 0 11 45 1 1 0 1 0 11 43zM41.3 3.6999999999999997A.6.6 0 1 0 41.3 4.8999999999999995.6.6 0 1 0 41.3 3.6999999999999997zM43 3.3A.6.6 0 1 0 43 4.5.6.6 0 1 0 43 3.3zM44.6 3A.6.6 0 1 0 44.6 4.2.6.6 0 1 0 44.6 3zM46.2 2.6999999999999997A.6.6 0 1 0 46.2 3.9.6.6 0 1 0 46.2 2.6999999999999997zM47.8 2.5A.6.6 0 1 0 47.8 3.7.6.6 0 1 0 47.8 2.5z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 50">
<path d="M21.6 31.3c-.1 0-.3-.1-.4-.1l-2.4-2.4c-.2-.2-.2-.5 0-.7l5.3-5.3c-.1-.9-.5-1.9-1.4-2.9-1-1-2.5-1.6-4-1.6-1.5 0-2.9.6-4 1.6-1.1 1.1-1.9 2.6-2 4.1-.1 1-.9 1.8-1.9 1.8l-.1 0c-.2 0-.4 0-.6 0-2.8 0-5.1.9-6.8 2.6C-1.4 33-1 39.7 4.2 44.8 6.9 47.5 10 49 13.1 49c2.7 0 5.4-1.2 7.5-3.3 2.3-2.3 2.6-4.5 2.8-6.4 0-.2.1-.5.1-.7.2-1.3 1.1-1.7 2.6-2.4 1.6-.7 3.9-1.5 3.9-4.5 0-.4-.1-1.7-2-1.7-.3 0-.6 0-.9.1-.3 0-.6.1-1 .1-1.1 0-1.5-.3-1.8-1.2l0-.1L22 31.1C21.9 31.2 21.8 31.3 21.6 31.3zM11 45c-.6 0-1-.4-1-1s.4-1 1-1 1 .4 1 1S11.6 45 11 45zM9.5 35.7l1.5-1.5 4.4 4.4-1.5 1.5L9.5 35.7zM15 45c-.6 0-1-.4-1-1s.4-1 1-1 1 .4 1 1S15.6 45 15 45zM18 43c-.6 0-1-.4-1-1s.4-1 1-1 1 .4 1 1S18.6 43 18 43zM18 36l-4.4-4.4 1.5-1.5 4.4 4.4L18 36zM41.3 3.6999999999999997A.6.6 0 1 0 41.3 4.8999999999999995.6.6 0 1 0 41.3 3.6999999999999997zM43 3.3A.6.6 0 1 0 43 4.5.6.6 0 1 0 43 3.3zM44.6 3A.6.6 0 1 0 44.6 4.2.6.6 0 1 0 44.6 3zM46.2 2.6999999999999997A.6.6 0 1 0 46.2 3.9.6.6 0 1 0 46.2 2.6999999999999997zM47.8 2.5A.6.6 0 1 0 47.8 3.7.6.6 0 1 0 47.8 2.5z"/>
<path fill="none" stroke="#000" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M21.4,31L42.8,9.6C43.2,9.2,43.6,9,44.1,9l1.1-0.1c0.5-0.1,1-0.3,1.4-0.7L49,5.8l-0.9-0.9L42.2,6c-0.5,0.1-1,0.4-1.4,0.7L19,28.6L21.4,31z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1,5 @@
<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="M28,5h-2.553c-0.284,0-0.555,0.121-0.745,0.333L13.803,17.496"/>
<path fill="none" stroke="#000" stroke-miterlimit="10" stroke-width="2" d="M15.266,15.891c-2.672-2.672-0.345-5.304-0.588-5.547C14.435,10.101,11,10.122,11,12.872C11,14.014,10.135,15,8.818,15c-0.812,0-2.087,0.357-3.198,1.468l0,0c-2.166,2.221-2.332,5.554,0.555,8.386c2.888,2.888,6.164,2.666,8.33,0.5c1.277-1.277,1.277-2.444,1.444-3.665C16.228,19.411,20,20.875,20,17.4c0-0.448-1.415,1.153-2.656-0.087"/>
<path fill="none" stroke="#000" stroke-miterlimit="10" stroke-width="1.4" d="M10.514 17.986L13.014 20.486M8.25 20.25L10.75 22.75"/>
</svg>

After

Width:  |  Height:  |  Size: 749 B

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 30">
<path d="M25.045 12.818c0 0-.074.694-.369 1.233-.295.538-.295.668-.222 1.13-.812 0-.591-.591-1.773-.591.739 1.387 1.256 3.545.591 3.545-.591 0-.591-1.182-.591-1.182S21.222 18 21.5 20.5c.074.665-.443.591-.591.591-.443 0-.569-.648 0-1.182 0 0-1.56-.372-2.053 2.682 0 0-.114 1.033-1.973 1.491 0 0-1.603.343-1.883 1.068V27c0 0 .591-1.927 2.955-.616.8.443 2.29 1.105 4.727.025 0 0-1.33-.334-1.182-1.028.233-1.096 2.031.502 3.25-.771.517-.539.665-.925 2.068-1.156 0 0-.295-.591-1.182-.591-.21 0-.886 0-.443-.591.471-.628 2.364-1.285 2.807-4.136 0 0-.369.591-1.773.591 0 0 .591-.36.591-1.593 0-1.335.591-1.952.591-1.952s-.591-.18-1.182.591C26.227 15.773 26.597 13.82 25.045 12.818zM4.955 12.818c0 0 .074.694.369 1.233.295.538.295.668.222 1.13.812 0 .591-.591 1.773-.591-.739 1.387-1.256 3.545-.591 3.545.591 0 .591-1.182.591-1.182S8.778 18 8.5 20.5c-.074.665.443.591.591.591.443 0 .569-.648 0-1.182 0 0 1.56-.372 2.053 2.682 0 0 .114 1.033 1.973 1.491 0 0 1.603.343 1.883 1.068V27c0 0-.591-1.927-2.955-.616-.8.443-2.29 1.105-4.727.025 0 0 1.33-.334 1.182-1.028-.233-1.096-2.031.502-3.25-.771-.517-.54-.665-.925-2.068-1.156 0 0 .295-.591 1.182-.591.21 0 .886 0 .443-.591C4.336 21.645 2.443 20.988 2 18.136c0 0 .369.591 1.773.591 0 0-.591-.36-.591-1.593 0-1.335-.591-1.952-.591-1.952s.591-.18 1.182.591C3.773 15.773 3.403 13.82 4.955 12.818z"/>
<path d="M15 8A6 6 0 1 0 15 20A6 6 0 1 0 15 8Z"/>
<path fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M15 27L15 3M16 10c0-3.207 2-6 2-6M17 12c0-3.666 3-6 3-6M14 10c0-3.207-2-6-2-6M13 12c0-3.666-3-6-3-6"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -0,0 +1,5 @@
<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="M8,21v-7.5C8,12.7,7.3,12,6.5,12h0C5.7,12,5,12.7,5,13.5V27h3v-2h16v2h3v-6H8z"/>
<path fill="none" stroke="#000" stroke-miterlimit="10" stroke-width="2" d="M14,21h13v-4c0-1.1-0.9-2-2-2H14V21z"/>
<path fill="none" stroke="#000" stroke-miterlimit="10" stroke-width="2" d="M11 15A3 3 0 1 0 11 21 3 3 0 1 0 11 15zM22 5L27 5 27 6 23 9 23 10 28 10M15 7L19 7 19 7.2 16 10.7 16 11 20 11"/>
</svg>

After

Width:  |  Height:  |  Size: 528 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 50">
<path d="M49 38c-4.118 0-5.011-2.209-5.056-2.33-.014-.04-.043-.068-.061-.104-.029-.059-.058-.115-.099-.168-.044-.058-.095-.104-.15-.15-.029-.025-.047-.058-.079-.08-.017-.012-.038-.012-.056-.023-.064-.037-.131-.06-.201-.083-.056-.018-.109-.038-.166-.045-.062-.008-.123-.003-.187.001-.069.004-.136.008-.203.026-.02.005-.039.001-.059.008-.039.013-.067.042-.103.059-.064.03-.125.06-.181.104-.055.042-.099.091-.144.144-.027.032-.063.051-.087.087C42.151 35.471 40.425 38 37 38c-3.382 0-5.095-2.448-5.17-2.559-.047-.07-.112-.116-.172-.17-.037-.033-.061-.075-.103-.104-.001-.001-.002 0-.003-.001-.11-.073-.233-.123-.361-.148-.077-.015-.153-.004-.23-.001-.051.002-.102-.009-.153.001-.129.025-.252.075-.363.149 0 0 0 0 0 0-.027.018-.041.046-.066.066-.077.062-.154.125-.211.211 0 0-.002.003-.002.003s0 .001-.001.001C30.12 35.516 28.397 38 25 38c-3.382 0-5.095-2.448-5.17-2.559-.046-.069-.11-.114-.169-.167-.037-.034-.062-.077-.106-.106-.001-.001-.002-.001-.003-.001-.11-.073-.233-.123-.361-.148-.077-.015-.153-.004-.23-.001-.051.002-.102-.009-.153.001-.129.025-.252.075-.363.149 0 0 0 0 0 0-.013.008-.019.022-.031.031-.092.068-.18.146-.247.247 0 0-.002.003-.002.003s0 .001-.001.001C18.12 35.516 16.397 38 13 38c-3.382 0-5.095-2.448-5.17-2.559-.048-.072-.115-.12-.177-.175-.035-.031-.058-.071-.098-.098-.001 0-.002 0-.002-.001-.11-.073-.233-.123-.361-.148-.077-.015-.153-.004-.23-.001C6.91 35.02 6.86 35.009 6.809 35.019c-.129.025-.252.075-.362.149 0 0-.001 0-.001 0-.041.027-.064.068-.1.1-.062.056-.129.105-.178.177 0 0-.002.003-.002.003s0 .001-.001.001C6.12 35.516 4.397 38 1 38c-.552 0-1 .448-1 1s.448 1 1 1c2.98 0 4.969-1.457 6-2.478C8.031 38.543 10.02 40 13 40s4.969-1.457 6-2.478C20.031 38.543 22.02 40 25 40s4.969-1.457 6-2.478C32.031 38.543 34.02 40 37 40c2.857 0 4.806-1.341 5.87-2.351C43.773 38.705 45.574 40 49 40c.552 0 1-.448 1-1S49.552 38 49 38zM1 35c2.98 0 4.969-1.457 6-2.478C8.031 33.543 10.02 35 13 35s4.969-1.457 6-2.478C20.031 33.543 22.02 35 25 35s4.969-1.457 6-2.478C32.031 33.543 34.02 35 37 35c2.857 0 4.806-1.341 5.87-2.351C43.773 33.705 45.574 35 49 35c.552 0 1-.448 1-1 0-.195-.07-.366-.167-.52l.015-.009-5-8C44.665 25.178 44.345 25 44 25h-2.974c.26-5.906 2.74-9.237 2.765-9.269.216-.279.269-.651.139-.979-.129-.328-.422-.563-.77-.62-.297-.047-6.582-.997-10.608 2.933C27.985 7.121 16.123 7 16 7c-.353 0-.68.186-.86.49-.18.303-.187.68-.018.989.02.037 1.029 1.916 2.039 5.009-2.944-1.471-5.945-1.663-6.616-1.351-.353.165-.599.539-.599.928 0 .181.05.36.143.515.017.029.943 1.607 1.794 4.14-2.51-1.021-5.056-.665-5.198-.644C6.33 17.129 6.03 17.369 5.9 17.705s-.069.715.158.994C6.082 18.727 8.22 21.393 8.577 25H6c-.345 0-.665.178-.848.47l-5 8 .015.009C.07 33.634 0 33.805 0 34 0 34.552.448 35 1 35zM32 20.329L37.338 25H26.662L32 20.329zM41.289 16.006c-.802 1.526-1.874 4.201-2.19 7.877l-5.603-4.903C35.743 16.388 39.307 15.996 41.289 16.006zM31.007 18.54L23.624 25h-2.432c-.186-7.311-2.35-13.179-3.544-15.878C20.905 9.522 28.055 11.238 31.007 18.54zM17.981 16.343c.623 2.482 1.116 5.43 1.204 8.657h-3.933c-.189-4.859-1.678-8.825-2.643-10.893C13.975 14.316 16.092 14.901 17.981 16.343zM8.668 19.028c1.181.087 2.755.427 3.989 1.467.307 1.368.536 2.883.605 4.504h-2.675C10.38 22.489 9.44 20.377 8.668 19.028z"/>
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 50">
<path fill="none" stroke="#000" stroke-miterlimit="10" stroke-width="2" d="M27.5 4A3.5 3.5 0 1 0 27.5 11 3.5 3.5 0 1 0 27.5 4zM18.5 7A3.5 3.5 0 1 0 18.5 14 3.5 3.5 0 1 0 18.5 7z"/>
<path fill="none" stroke="#000" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M39.478,8.348c0.742-0.812,0.686-2.083-0.126-2.826c-0.812-0.742-2.083-0.686-2.826,0.126l-3.799,4.239l-13.122,6.729c-1.454,0.745-2.044,2.517-1.327,3.985l2.45,5.019l-4.875,5.729c-0.096,0.086-0.139,0.137-0.126,0.148L7.593,41c-0.891,1.047-0.763,2.633,0.284,3.524v0c1.047,0.891,2.633,0.763,3.524-0.284l9.614-11.241c5.378-0.008,9.748-4.263,9.964-9.59L31,23.416l3.127,8.913c0.455,1.297,1.889,1.987,3.187,1.531c1.297-0.455,1.987-1.889,1.531-3.187l-3.147-8.97c-0.455-1.297-1.889-1.987-3.187-1.531l-1.798,0.505c-0.008,0-0.018,0-0.042,0.012l-2.729,0.766L26.407,18l7.749-4.195l0-0.003c0.282-0.107,0.545-0.272,0.762-0.508L39.478,8.348z"/>
<path fill="none" stroke="#000" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M21.015 32.999l-2.823 3.3-2.859 4.952c-.688 1.191-.276 2.728.915 3.415 1.191.688 2.728.276 3.415-.915L27.06 30.94C25.383 32.225 23.292 32.996 21.015 32.999zM34.126 32.328L31 23.416l-.021-.006c-.081 1.989-.742 3.829-1.818 5.354l.842 13.879c.078 1.373 1.265 2.432 2.638 2.354 1.373-.078 2.432-1.265 2.354-2.638l-.571-9.46C34.305 32.723 34.2 32.537 34.126 32.328zM39.996 6.956c.01.497-.158.998-.518 1.391l-4.561 4.947c-.216.236-.48.401-.762.508l0 .003L26.407 18l1.535 3.454 2.729-.766c.024-.011.034-.012.042-.012l1.798-.505c.497-.174 1.014-.178 1.489-.046V18l6-3V7C40 6.985 39.996 6.971 39.996 6.956z"/>
<path fill="none" stroke="#000" stroke-miterlimit="10" stroke-width="2" d="M30.541,19.997C30.839,20.945,31,21.954,31,23c0,5.523-4.477,10-10,10c-2.252,0-4.33-0.744-6.001-2"/>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -0,0 +1,5 @@
<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>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<path d="M1 6.75C1.517 6.09 2.372 5.482 3.405 5H0C.414 5.483.924 6.3 1 6.75zM16 5h-3.527C13.644 5.539 14.521 6.203 15 6.75 15.278 6.006 15.556 5.591 16 5z"/>
<path fill="none" stroke="#000" stroke-miterlimit="10" d="M4 5.5A3.5 3.5 0 1 0 4 12.5A3.5 3.5 0 1 0 4 5.5Z"/>
<path d="M4 8A1 1 0 1 0 4 10A1 1 0 1 0 4 8Z"/>
<path fill="none" stroke="#000" stroke-miterlimit="10" d="M12 5.5A3.5 3.5 0 1 0 12 12.5A3.5 3.5 0 1 0 12 5.5Z"/>
<path fill="none" stroke="#000" stroke-miterlimit="10" d="M12.752,5.565C11.566,4.294,9.876,3.5,8,3.5c-1.81,0-3.447,0.74-4.625,1.933"/>
<path d="M12 8A1 1 0 1 0 12 10 1 1 0 1 0 12 8zM7 11L8 13 9 11 8.5 9 7.5 9z"/>
</svg>

After

Width:  |  Height:  |  Size: 720 B

View File

@@ -0,0 +1,5 @@
<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="M12 18L24.154 18 28 23 29 23 29 10 28 10 24.154 15 12.154 15"/>
<path fill="none" stroke="#000" stroke-miterlimit="10" stroke-width="2" d="M21.417 15H12c-2.209 0-4 1.791-4 4v0c0 2.209 1.791 4 4 4h6.5c1.381 0 2.5-1.119 2.5-2.5v0c0-1.381-1.119-2.5-2.5-2.5M3 19L3 15M12 15L12 12M15 15L15 12M18 15L18 12M12 25L12 20M15 25L15 20M18 25L18 20"/>
<path d="M10 19h3v-2h-1C10.897 17 10 17.897 10 19zM6.812 16H3v2h3.09C6.212 17.282 6.459 16.608 6.812 16z"/>
</svg>

After

Width:  |  Height:  |  Size: 595 B

View File

@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 30">
<path d="M9 11c0-3 2.7-6.373 6-6.373S21 8 21 11H9zM13.5 3c0-.7.7-1.5 1.5-1.5s1.5.8 1.5 1.5H13.5z"/>
<path fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M8 14L22 14"/>
<path fill="none" stroke="#000" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M9 14L9 20M13 14L13 20M17 14L17 20M21 14L21 20"/>
<path fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M6 20L24 20"/>
<path fill="none" stroke="#000" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M7 21L23 21"/>
<path fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M6 26L24 26"/>
<path fill="none" stroke="#000" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M7 20L7 27M11 20L11 27M15 20L15 27M19 20L19 27M23 20L23 27"/>
<path d="M15.322,2h-0.644C14.304,2,14,2.304,14,2.678V6h2V2.678C16,2.304,15.696,2,15.322,2z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M23.2,3.1c-0.4,0.4-1.1,0.4-1.5,0l-0.7-0.7c-0.4-0.4-0.4-1.1,0-1.5c0.4-0.4,1.2,0.3,2.3-0.8L24,0.8 C22.9,1.8,23.6,2.7,23.2,3.1z"/>
<path d="M13 12.8L11.2 11 22.5.8 23.2 1.5zM6.2 15.9L8 17.7 3.2 21.9 2 20.7z"/>
<path d="M12.1,7.5c1.1,0,2.2,1,2.6,1.5l0.1,0.1l0.1,0.1c0.4,0.3,2.4,2.1,1.1,3.9c-0.2,0.2-0.4,0.5-0.6,0.9 C15.2,14,14.9,14,14.8,14c-1.4,0-2.5,1-3.2,1.7c-2.2,2.2-1.8,3.9-1.1,4.9c-0.2,0.3-0.5,0.5-0.7,0.7c-0.8,0.8-1.6,1.2-2.5,1.2 c-1.2,0-2.5-0.7-3.7-2l0,0l0,0c-1.3-1.2-2-2.5-2-3.7c0-0.9,0.4-1.7,1.2-2.5c0.2-0.2,0.4-0.4,0.7-0.7C4,14,4.6,14.1,5.1,14.1 c1.4,0,2.4-0.9,3.2-1.7c1.5-1.5,1.8-2.8,1.6-3.8c0.4-0.2,0.7-0.5,0.9-0.6C11.2,7.7,11.7,7.5,12.1,7.5 M12.1,6 c-0.7,0-1.4,0.2-2.2,0.8C9.5,7.1,8.8,7.5,8,8.1c1,1.1,0.3,2.2-0.8,3.3c-0.7,0.7-1.4,1.2-2.1,1.2c-0.4,0-0.8-0.2-1.2-0.6 c-0.2-0.2-0.3-0.4-0.4-0.6c-0.7,0.6-1.3,1.1-1.9,1.7c-2.6,2.6-1.8,5.8,0.9,8.4C4,23.1,5.7,24,7.3,24c1.2,0,2.4-0.5,3.5-1.6 c0.6-0.6,1.1-1.2,1.7-1.9c-0.2-0.1-0.4-0.2-0.6-0.4c-1.1-1.1-0.4-2.2,0.7-3.4c0.7-0.7,1.4-1.2,2.1-1.2c0.4,0,0.8,0.1,1.1,0.5 c0.6-0.8,1-1.5,1.4-1.9c2.2-3-1-5.7-1.4-6C15.7,7.8,14.1,6,12.1,6L12.1,6z"/>
<path fill="none" stroke="#000" stroke-linejoin="round" stroke-miterlimit="10" d="M17.5 3.7L20.3 6.5M18.9 2.3L21.7 5.1"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" id="yoDragon" viewBox="0 0 50 50">
<path d="M36.9 33c.6 0 4.3 0 4.3 0s-2.7 1.9-3.7 2.3L36.9 33zM37.1 13c1.4-.4 1.4-3.9 1.4-3.9s3 4.1 3.1 7.4L37.1 13zM41.8 17.1c1.2-.2 1.5-3.1 1.5-3.1s2.1 3.7 1.8 6.4L41.8 17.1zM44.3 28c-.2.5-2 4.6-2 4.6s3-2.3 3.9-3.1L44.3 28z"/>
<path fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M22.4 28.6c3 3.5 5.6 9 5.6 20.4M9 49c0 0 2-3 2-8 0-7.9-5-11-5-18 0-5.6 2.9-10.8 8.2-12.7"/>
<path fill="none" stroke="#000" stroke-linecap="round" stroke-miterlimit="10" stroke-width="2" d="M42.6,22.1c1.7,1.2,2.6,2.4,2.6,2.4"/>
<path d="M32.7,14.4c0,0,4,2.9,5,4.2c0,0,0.2,1-0.2,1.1c-0.6,0.2-3.1,0.5-4-0.2l0.7-2.4l-2.2,2c-1.2-0.4-2.2-1.1-2.2-1.1S29.5,15.4,32.7,14.4z"/>
<path fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M6.5 6.1c1.7 4-1.2 7.3-5.3 7.6 2.3 2.8 3 9.3-.2 12.1 2.9 2.3 4.2 5.3 1 10 4.4 1.1 5.1 6.4 2 9.3 3.9 1.3 5.1 3.5 5.1 3.5M36.1 13.2c0 0-4.1-3.6-4.3-8.9-1.5 1.2-6.4 2.6-7.7-3-1.1 2.7-5.8 6-9.8.5-.2 1.1-1.7 1.6-1.7 1.6M38.1 37c0 0-1.1 1-2.8 1.5-1.7.4-4.5.8-4.5.8l2.5-3.7L26.5 29c-2.1 1.1-12-3.3-7.4-11.5M37 24.2c-.6-.1-1.2 0-1.8.4-.5.3-.8.7-.9 1.2"/>
<path fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M37.2,18.8C28,8.4,7.5,14.8,3.6,1C9.5,10.2,23.8,6.1,37,13.7c10,5.8,13.7,14.9,11.2,16.4"/>
<path d="M36.9 24.6l-.1 1.5 1.6-.9-.1 1.6 1.7-.5L40 27.6l1.2-.6.1 1.5 1.6-.7c0 0-.4 1.4-.2 1.3.2-.1 1.7-.5 1.7-.5l.1 1.3 1.3-.3.1 1.3 1.3-.6v1.2l1.1-.4-.1-2.2c0 0-2.2-1.2-4.2-2.3s-5.2-3.2-6.6-3.4C36.5 23.1 36.9 24.6 36.9 24.6zM38.6 36.3l1-1.1-1.9-.5 1.1-1-1.7-.6.9-1-1.3-.3.9-1.2L36 30.1c0 0 1.1-.9 1-.9-.1-.1-1.6-.6-1.6-.6l.7-1-1.2-.5.7-1.1-1.8-1.4c0 0-.6.8-.6 2.1 0 1.1 1.4 4 1.4 4l2.4 6L38.6 36.3z"/>
<path fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M23.7 2.7c0 0 0 1.6.1 2.9M14.4 3.3L14.3 4.5M31.4 5c0 0-1 1.5-1.4 2.4M2.2 14.3L4.3 15M1.4 25.7l2.1-.3M2.8 35.5c0 0 1.5-.3 3-1.1M4.8 44.9c0 0 1.3-.4 3.1-1.4"/>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" id="yoRabbit" viewBox="0 0 24 24">
<path fill="none" stroke="#000" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M12,22c0-6.4,4.6,0.1,6.3-5.5c0,0,0.7-0.2,0.7-0.8c0-0.4-1.4-4.7-7.6-4.7c0-5.4-3.7-7-4.8-7C6.2,4,6,4.1,6,4.5c0,3.3,1.1,5.4,3,7.1C6.5,12.3,7.5,18,5,18"/>
<path fill="none" stroke="#000" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M9,5.1C9,5.1,8.8,3,9.9,3c0.5,0,4.1,1.9,4.1,8.3"/>
<path d="M14 13.299999999999999A0.8 0.8 0 1 0 14 14.9A0.8 0.8 0 1 0 14 13.299999999999999Z"/>
</svg>

After

Width:  |  Height:  |  Size: 586 B

View File

@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" id="yoTiger" viewBox="0 0 32 32">
<path fill="none" stroke="#000" stroke-linejoin="bevel" stroke-miterlimit="10" stroke-width="2" d="M12,9c2.242,0,4,2,4,2s1.758-2,4-2" id="yoTiger"/>
<path fill="none" stroke="#000" stroke-miterlimit="10" stroke-width="2" d="M4 15c0 0 2.319 2 5 2M3 19c0 0 3.319 2 6 2"/>
<path fill="none" stroke="#000" stroke-miterlimit="10" stroke-width="2" d="M13.871,14.871c0,0-2.871,3.519-2.871,10.129c-4.506,0-8-2-8-2c0-6.665,2.917-11,2.917-11S4,10.443,4,7.927S5.831,4,6.917,4c0.901,1.478,3,3,3,3"/>
<path d="M19 21c0 .552-3 2-3 2s-3-1.448-3-2 1.895-1 3-1S19 20.448 19 21zM10 13l5 2-.725 1.001c0 0-.335-.001-2.275-.001S10 13 10 13z"/>
<path fill="none" stroke="#000" stroke-miterlimit="10" stroke-width="2" d="M25.738 11.508C24.157 6.477 20.797 5 19.083 5s-3 1-3 1h-.167c0 0-1.287-1-3-1s-5.073 1.477-6.655 6.508M28 15c0 0-2.319 2-5 2M29 19c0 0-3.319 2-6 2"/>
<path fill="none" stroke="#000" stroke-miterlimit="10" stroke-width="2" d="M18.129 14.871c0 0 2.871 3.519 2.871 10.129 4.506 0 8-2 8-2 0-6.665-2.917-11-2.917-11S28 10.443 28 7.927 26.169 4 25.083 4c-.901 1.478-3 3-3 3M21 25c0 2.792-5 3-5 3s-5-.208-5-3"/>
<path d="M22,13l-5,2l0.725,1.001c0,0,0.335-0.001,2.275-0.001S22,13,22,13z"/>
<path fill="none" stroke="#000" stroke-miterlimit="10" stroke-width="2" d="M19.207,27.349C17.549,27.549,16.15,25,16.15,25h-0.3c0,0-1.399,2.549-3.057,2.349"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

23
web/icons/Yoda-68107.svg Normal file
View File

@@ -0,0 +1,23 @@
<svg xmlns="http://www.w3.org/2000/svg" id="yoda" viewBox="0 0 48 48">
<path fill="#afb42b" d="M34.829,15.518c2.991-0.001,9.906-5.518,13.171,0C43,16,42,23,38,23L34.829,15.518z"/>
<path fill="#9e9d24" d="M48,15.518C47.161,13.972,45.772,12,44,12c-4,0-5.159,2-7,2c-0.628,0-0.131,1.058-0.147,2.938 C38,17,42.026,13.934,45,14C46.158,14.026,47.086,14.86,48,15.518z"/>
<path fill="#afb42b" d="M13.171,15.518C10.18,15.517,3.265,10,0,15.518C5,16,6,23,10,23L13.171,15.518z"/>
<path fill="#9e9d24" d="M0,15.518C0.839,13.972,2.228,12,4,12c4,0,5.159,2,7,2c0.628,0,0.131,1.058,0.147,2.938 C10,17,5.974,13.934,3,14C1.842,14.026,0.914,14.86,0,15.518z"/>
<path fill="#d84315" d="M15 31l-3 4c-1 4 12 11 12 11V31H15zM33 31l3 4c1 4-12 11-12 11V31H33z"/>
<path fill="#665b0e" d="M24,29H14c0,0,0.684,3.051,1,4c1,3,7,6,9,10c2-4,8-7,9-10c0.316-0.949,1-4,1-4H24z"/>
<path fill="#827717" d="M28,2h-4h-4c-4.125,0-10,8.363-10,11c0,3,0,9,0,9c0,6.083,7,9,7,9c3,1,3,3,7,3s4-2,7-3 c0,0,7-2.917,7-9c0,0,0-6,0-9C38,10.363,32.125,2,28,2z"/>
<path fill="#827717" d="M28,18h-2h-2h-2h-2c0,0-2,4-2,7l6,1l6-1C30,22,28,18,28,18z"/>
<path fill="#665b0e" d="M18 13c-.758.575-1.371 1.193-1.978 1.813-.592.635-1.145 1.291-1.686 1.965-.553.665-1.053 1.379-1.611 2.075-.261.359-.533.718-.823 1.072C11.622 20.287 11.344 20.653 11 21c.038-.479.141-.938.242-1.402.142-.445.282-.898.469-1.33.378-.862.847-1.693 1.456-2.43.6-.739 1.298-1.416 2.119-1.93C16.095 13.401 17.023 12.989 18 13zM30 13c.977-.011 1.905.401 2.714.909.821.513 1.519 1.191 2.119 1.93.609.737 1.077 1.568 1.456 2.43.187.432.327.884.469 1.33C36.859 20.062 36.962 20.521 37 21c-.344-.347-.622-.713-.902-1.074-.29-.355-.562-.713-.823-1.072-.557-.696-1.057-1.41-1.611-2.075-.541-.674-1.095-1.33-1.686-1.965C31.371 14.193 30.758 13.575 30 13z"/>
<path fill="#2d2706" d="M23 18A1 0.5 0 1 0 23 19A1 0.5 0 1 0 23 18Z"/>
<path fill="#2d2706" d="M25 18A1 0.5 0 1 0 25 19A1 0.5 0 1 0 25 18Z"/>
<path fill="#665b0e" d="M30.61,26.536c-0.084-0.412-0.179-0.823-0.283-1.238C30,24,28.976,23.646,28.464,23.508 c-0.252-0.054-0.498-0.122-0.843-0.139c-0.318-0.017-0.61,0.017-0.892,0.058C25.637,23.602,24.615,22.999,24,23 c-0.615-0.001-1.637,0.602-2.729,0.427c-0.282-0.041-0.574-0.075-0.892-0.058c-0.345,0.017-0.591,0.084-0.843,0.139 C19.024,23.646,18,24,17.673,25.298c-0.104,0.415-0.2,0.826-0.283,1.238C17.225,27.36,17.098,29.18,17,30 c0.679-2.036,1.523-4.997,3-4.997c1,0,2-0.003,4-0.003s3,0.003,4,0.003c1.477,0,2.321,2.961,3,4.997 C30.902,29.18,30.775,27.36,30.61,26.536z"/>
<path fill="#423809" d="M26 16c0 0-2-3.001-2-3.001S26.737 12 30 12c3 0 5 2 7 4 0 0-2 1-3 1C31 17 26 16 26 16zM22 16c0 0 2-3.001 2-3.001S21.263 12 18 12c-3 0-5 2-7 4 0 0 2 1 3 1C17 17 22 16 22 16z"/>
<path fill="#dfdfdf" d="M27,17c0,0,1-2,3.5-2c1.5,0,3.5,2,3.5,2s-1.5,1-3.5,1C26,18,27,17,27,17z"/>
<path fill="#2d2706" d="M30.5 15A1.5 1.5 0 1 0 30.5 18A1.5 1.5 0 1 0 30.5 15Z"/>
<path fill="#dfdfdf" d="M21,17c0,0-1.095-2-3.595-2c-1.5,0-3.5,2-3.5,2s1.5,1,3.5,1C21.905,18,21,17,21,17z"/>
<path fill="#2d2706" d="M17.405 15A1.5 1.5 0 1 0 17.405 18A1.5 1.5 0 1 0 17.405 15Z"/>
<path fill="#8c8920" d="M30,9.979c-0.905,0-1.833,0.292-2.784,0.889c-0.064-0.039-0.128-0.085-0.193-0.12 c-0.32-0.177-0.648-0.321-0.986-0.43C25.364,10.105,24.673,10.004,24,10c-0.673,0.004-1.364,0.105-2.038,0.318 c-0.337,0.108-0.666,0.253-0.986,0.43c-0.065,0.035-0.128,0.081-0.193,0.12C19.833,10.27,18.905,9.979,18,9.979 c-5,0-6.815,4.973-7,6.021c0.645-0.842,4-3,6-3c3,0,5,2,5,2c-0.049,0.052,0-1,2-1s2.049,1.052,2,1c0,0,2-2,5-2c2,0,5.355,2.158,6,3 C36.815,14.951,35,9.979,30,9.979z"/>
<path fill="#827717" d="M19.583 8.895c.25.167 1 4 3.417 4.083-1.083-1.833-1-3-1-3L19.583 8.895zM28.583 9.145c-.25.167-1.167 3.75-3.583 3.833 1.083-1.833 1-3 1-3L28.583 9.145z"/>
<path fill="#8c8920" d="M23 7.979v-4c-3 0-4 1-4 1s0 3 1 4C21 7.979 23 7.979 23 7.979zM25 7.979v-4c3 0 4 1 4 1s0 3-1 4C27 7.979 25 7.979 25 7.979z"/>
<path fill="#665b0e" d="M33,28c-0.623-1.917-1.46-3.725-2.505-5.345c-1.055-1.611-2.381-2.994-3.961-3.878l-0.413-0.232 L26.094,18c-0.039-0.797-0.235-1.534-0.611-2.085c-0.19-0.275-0.431-0.5-0.701-0.661c-0.261-0.164-0.59-0.258-0.782-0.254 c-0.191-0.001-0.52,0.091-0.781,0.255c-0.271,0.161-0.511,0.386-0.701,0.661c-0.377,0.551-0.569,1.289-0.61,2.085l-0.028,0.547 l-0.413,0.231c-1.579,0.884-2.905,2.267-3.96,3.878C16.463,24.276,15.624,26.084,15,28c0.069-2.022,0.554-4.052,1.44-5.937 c0.878-1.879,2.226-3.656,4.093-4.841L20.092,18c-0.027-1.037,0.157-2.184,0.818-3.173c0.324-0.491,0.756-0.924,1.27-1.248 c0.52-0.32,1.085-0.557,1.82-0.579c0.734,0.024,1.3,0.259,1.819,0.58c0.513,0.323,0.946,0.757,1.27,1.247 c0.661,0.989,0.842,2.136,0.817,3.173l-0.44-0.776c1.866,1.185,3.214,2.963,4.092,4.841C32.443,23.949,32.93,25.978,33,28z"/>
</svg>

After

Width:  |  Height:  |  Size: 4.6 KiB

3
web/icons/blank.svg Normal file
View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g/>
</svg>

After

Width:  |  Height:  |  Size: 76 B

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

@@ -0,0 +1,17 @@
<html>
<body>
original<br>
<div style="width:100px; height:100px; border: 1px; border-style: solid;">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<path d="M4.96 2.126l.613.416c.301.204.663.321.982.425.158.051.396.128.466.173l.035.023.01.006c.13.084.309.2.535.272C7.748 3.488 7.906 3.513 8.06 3.513c.339 0 .651-.12.881-.339.274.105.601.224.972.25.038.135.068.273.1.423.057.267.122.569.239.889.089.259.208.609.491.91.17.203.355.324.48.407l.031.021c.029.04.076.119.111.179.075.128.166.282.29.437.124.187.296.347.49.456-.088.076-.18.15-.278.222-.365.24-.64.498-.839.788L11.004 8.19l-.02.035-.027.047c-.217.377-.542.941-.423 1.627.034.285.108.529.174.745.017.055.034.11.05.166l-.022.011-.034.02c-.096.056-.938.57-.938 1.369 0 .095.01.182.027.262-.218.2-.351.476-.354.785-.104.14-.181.278-.247.397-.023.042-.046.085-.071.126-.071.037-.216.084-.395.12-.093-.214-.222-.361-.308-.457-.158-.228-.362-.396-.544-.545-.039-.032-.089-.073-.131-.109-.014-.118-.032-.235-.059-.34-.099-.385-.315-.667-.489-.894-.032-.041-.073-.095-.105-.14.02-.049.046-.108.068-.156.099-.221.234-.523.265-.894l.004-.043v-.043c0-.499-.127-.942-.395-1.371.057-.163.105-.375.093-.624C7.139 8.2 7.158 8.088 7.158 7.957c0-.008-.022-.643-.291-1.164C6.855 6.754 6.841 6.716 6.825 6.678 6.626 6.218 6.181 5.985 5.5 5.985c-.04 0-.09 0-.148 0-.586 0-1.988 0-2.326-.001C2.999 5.971 2.972 5.955 2.946 5.94L2.741 5.824C2.63 5.762 2.451 5.662 2.269 5.557c.006-.015.011-.03.016-.045.221-.641.094-1.106-.069-1.397.039-.033.081-.064.126-.094l.06-.034c.237-.13.73-.402.92-1.048.023-.09.036-.175.042-.252.069-.054.145-.12.219-.201.005 0 .01 0 .014 0 .419 0 .775-.15 1.034-.259C4.678 2.209 4.72 2.191 4.761 2.175 4.827 2.156 4.893 2.14 4.96 2.126M5.889 1C5.382 1.035 4.911 1.071 4.441 1.211c-.25.091-.553.26-.841.26-.046 0-.092-.004-.137-.014-.109-.035-.181-.105-.29-.105-.109 0-.217.035-.254.105-.036.071 0 .105 0 .176C2.883 1.913 2.448 1.984 2.376 2.23c-.036.141 0 .316-.036.457C2.268 2.932 2.014 3.038 1.833 3.144c-.326.211-.579.457-.76.808C1.036 4.022 1 4.093 1 4.162c0 .141.217.281.29.386C1.434 4.725 1.398 4.97 1.326 5.181c-.073.211-.217.386-.29.562C1 5.814 1 5.885 1 5.919c.036.035.073.07.109.106.326.246 1.145.686 1.326.792.157.091.341.18.505.182C3 7 5 7 5.5 7c0 0 .5 0 .375.133C6.02 7.238 6.142 7.817 6.142 7.957c0 .14-.073.246-.036.352.036.387-.349.597-.096.913.254.316.398.632.398 1.054-.036.422-.362.773-.362 1.194 0 .457.543.808.652 1.23.036.141.036.316.073.457.073.316.639.597.82.878.073.106.181.175.217.316 0 .071 0 .141 0 .211 0 .175.145.351.326.422C8.166 14.995 8.201 15 8.238 15c.09 0 .192-.025.294-.05.398-.035 1.123-.175 1.376-.527.158-.219.254-.492.434-.668.109-.105.217-.211.181-.351-.036-.071-.073-.106-.073-.141 0-.071.145-.106.217-.106.073 0 .145 0 .217 0 .181-.035.254-.246.217-.386-.036-.175-.217-.281-.29-.457-.036-.035-.036-.07-.036-.105 0-.141.254-.387.434-.492.217-.105.471-.211.579-.422.073-.175.073-.351 0-.527-.073-.352-.217-.668-.254-1.019-.073-.351.145-.703.326-1.019.145-.211.362-.387.579-.527C13.167 7.677 13.891 6.878 14 6c-.254.211-.688.272-1.05.306-.109 0-.217 0-.29-.035-.073-.035-.145-.106-.181-.175C12.262 5.85 12.153 5.498 11.9 5.288c-.145-.106-.29-.175-.398-.317-.145-.141-.217-.352-.29-.563-.217-.598-.217-1.09-.471-1.687-.073-.14-.145-.316-.29-.316-.073 0-.145 0-.254 0-.045.007-.091.009-.136.009-.456 0-.912-.296-1.373-.391C8.645 2.009 8.594 2 8.544 2c-.07 0-.138.017-.18.058C8.256 2.164 8.348 2.335 8.24 2.44 8.197 2.481 8.13 2.498 8.06 2.498c-.049 0-.101-.008-.146-.023C7.805 2.44 7.701 2.369 7.592 2.299 7.23 2.053 6.506 1.948 6.144 1.702c.073-.105.109-.211.145-.317.036-.105-.038-.28-.146-.35C6.07 1 5.961 1 5.889 1L5.889 1zM13.875 10c-.099.215-.232.465-.398.571-.132.071-.299.143-.365.285-.099.179 0 .393-.033.571 0 .071-.033.143-.066.215-.033.179 0 .393.066.571.033.107.099.25.199.285.066 0 .166-.035.232-.107.066-.071.132-.215.166-.321s.033-.215.033-.321c.033-.321.199-.643.266-.965.033-.215.033-.429 0-.643C13.942 10.071 13.909 10 13.875 10l.017.035c.009.009.017.018.017.035l-.017-.035C13.884 10.027 13.875 10.018 13.875 10L13.875 10z"/>
</svg>
</div>
original<br>
<div style="width:100px; height:100px; border: 1px; border-style: solid;">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<path d="M4.96 2.126l.613.416c.301.204.663.321.982.425.158.051.396.128.466.173l.035.023.01.006c.13.084.309.2.535.272C7.748 3.488 7.906 3.513 8.06 3.513c.339 0 .651-.12.881-.339.274.105.601.224.972.25.038.135.068.273.1.423.057.267.122.569.239.889.089.259.208.609.491.91.17.203.355.324.48.407l.031.021c.029.04.076.119.111.179.075.128.166.282.29.437.124.187.296.347.49.456-.088.076-.18.15-.278.222-.365.24-.64.498-.839.788L11.004 8.19l-.02.035-.027.047c-.217.377-.542.941-.423 1.627.034.285.108.529.174.745.017.055.034.11.05.166l-.022.011-.034.02c-.096.056-.938.57-.938 1.369 0 .095.01.182.027.262-.218.2-.351.476-.354.785-.104.14-.181.278-.247.397-.023.042-.046.085-.071.126-.071.037-.216.084-.395.12-.093-.214-.222-.361-.308-.457-.158-.228-.362-.396-.544-.545-.039-.032-.089-.073-.131-.109-.014-.118-.032-.235-.059-.34-.099-.385-.315-.667-.489-.894-.032-.041-.073-.095-.105-.14.02-.049.046-.108.068-.156.099-.221.234-.523.265-.894l.004-.043v-.043c0-.499-.127-.942-.395-1.371.057-.163.105-.375.093-.624C7.139 8.2 7.158 8.088 7.158 7.957c0-.008-.022-.643-.291-1.164C6.855 6.754 6.841 6.716 6.825 6.678 6.626 6.218 6.181 5.985 5.5 5.985c-.04 0-.09 0-.148 0-.586 0-1.988 0-2.326-.001C2.999 5.971 2.972 5.955 2.946 5.94L2.741 5.824C2.63 5.762 2.451 5.662 2.269 5.557c.006-.015.011-.03.016-.045.221-.641.094-1.106-.069-1.397.039-.033.081-.064.126-.094l.06-.034c.237-.13.73-.402.92-1.048.023-.09.036-.175.042-.252.069-.054.145-.12.219-.201.005 0 .01 0 .014 0 .419 0 .775-.15 1.034-.259C4.678 2.209 4.72 2.191 4.761 2.175 4.827 2.156 4.893 2.14 4.96 2.126M5.889 1C5.382 1.035 4.911 1.071 4.441 1.211c-.25.091-.553.26-.841.26-.046 0-.092-.004-.137-.014-.109-.035-.181-.105-.29-.105-.109 0-.217.035-.254.105-.036.071 0 .105 0 .176C2.883 1.913 2.448 1.984 2.376 2.23c-.036.141 0 .316-.036.457C2.268 2.932 2.014 3.038 1.833 3.144c-.326.211-.579.457-.76.808C1.036 4.022 1 4.093 1 4.162c0 .141.217.281.29.386C1.434 4.725 1.398 4.97 1.326 5.181c-.073.211-.217.386-.29.562C1 5.814 1 5.885 1 5.919c.036.035.073.07.109.106.326.246 1.145.686 1.326.792.157.091.341.18.505.182C3 7 5 7 5.5 7c0 0 .5 0 .375.133C6.02 7.238 6.142 7.817 6.142 7.957c0 .14-.073.246-.036.352.036.387-.349.597-.096.913.254.316.398.632.398 1.054-.036.422-.362.773-.362 1.194 0 .457.543.808.652 1.23.036.141.036.316.073.457.073.316.639.597.82.878.073.106.181.175.217.316 0 .071 0 .141 0 .211 0 .175.145.351.326.422C8.166 14.995 8.201 15 8.238 15c.09 0 .192-.025.294-.05.398-.035 1.123-.175 1.376-.527.158-.219.254-.492.434-.668.109-.105.217-.211.181-.351-.036-.071-.073-.106-.073-.141 0-.071.145-.106.217-.106.073 0 .145 0 .217 0 .181-.035.254-.246.217-.386-.036-.175-.217-.281-.29-.457-.036-.035-.036-.07-.036-.105 0-.141.254-.387.434-.492.217-.105.471-.211.579-.422.073-.175.073-.351 0-.527-.073-.352-.217-.668-.254-1.019-.073-.351.145-.703.326-1.019.145-.211.362-.387.579-.527C13.167 7.677 13.891 6.878 14 6c-.254.211-.688.272-1.05.306-.109 0-.217 0-.29-.035-.073-.035-.145-.106-.181-.175C12.262 5.85 12.153 5.498 11.9 5.288c-.145-.106-.29-.175-.398-.317-.145-.141-.217-.352-.29-.563-.217-.598-.217-1.09-.471-1.687-.073-.14-.145-.316-.29-.316-.073 0-.145 0-.254 0-.045.007-.091.009-.136.009-.456 0-.912-.296-1.373-.391C8.645 2.009 8.594 2 8.544 2c-.07 0-.138.017-.18.058C8.256 2.164 8.348 2.335 8.24 2.44 8.197 2.481 8.13 2.498 8.06 2.498c-.049 0-.101-.008-.146-.023C7.805 2.44 7.701 2.369 7.592 2.299 7.23 2.053 6.506 1.948 6.144 1.702c.073-.105.109-.211.145-.317.036-.105-.038-.28-.146-.35C6.07 1 5.961 1 5.889 1L5.889 1zM13.875 10c-.099.215-.232.465-.398.571-.132.071-.299.143-.365.285-.099.179 0 .393-.033.571 0 .071-.033.143-.066.215-.033.179 0 .393.066.571.033.107.099.25.199.285.066 0 .166-.035.232-.107.066-.071.132-.215.166-.321s.033-.215.033-.321c.033-.321.199-.643.266-.965.033-.215.033-.429 0-.643C13.942 10.071 13.909 10 13.875 10l.017.035c.009.009.017.018.017.035l-.017-.035C13.884 10.027 13.875 10.018 13.875 10L13.875 10z"/>
</svg>
</div>
</body>
</html>

View File

@@ -0,0 +1,3 @@
<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>

After

Width:  |  Height:  |  Size: 350 B

Some files were not shown because too many files have changed in this diff Show More