diff --git a/CHANGELOG.md b/CHANGELOG.md index a1b160c..bbfcd63 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,8 @@ Fixed: - Checking for update every 10 Minutes not working - Background album art is cut off (#116) -- Tall context menus not scrollable (#114) \ No newline at end of file +- Tall context menus not scrollable (#114) +- Default folder images not working + +Improved: +- Moved `Hide Ads` into `Main Settings` \ No newline at end of file diff --git a/package.json b/package.json index 8781836..5b66433 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "markdown-it-attrs": "^4.1.0", "markdown-it-bracketed-spans": "^1.0.1", "moment": "^2.29.1", - "node-vibrant": "^3.1.6" + "node-vibrant": "^3.1.6", + "svgson": "^5.2.1" } } diff --git a/src/assets/glue-resources/fonts/GoogleSans-Black.ttf b/src/assets/fonts/GoogleSans-Black.ttf similarity index 100% rename from src/assets/glue-resources/fonts/GoogleSans-Black.ttf rename to src/assets/fonts/GoogleSans-Black.ttf diff --git a/src/assets/glue-resources/fonts/GoogleSans-Bold.ttf b/src/assets/fonts/GoogleSans-Bold.ttf similarity index 100% rename from src/assets/glue-resources/fonts/GoogleSans-Bold.ttf rename to src/assets/fonts/GoogleSans-Bold.ttf diff --git a/src/assets/glue-resources/fonts/GoogleSans-Light.ttf b/src/assets/fonts/GoogleSans-Light.ttf similarity index 100% rename from src/assets/glue-resources/fonts/GoogleSans-Light.ttf rename to src/assets/fonts/GoogleSans-Light.ttf diff --git a/src/assets/glue-resources/fonts/GoogleSans-Medium.ttf b/src/assets/fonts/GoogleSans-Medium.ttf similarity index 100% rename from src/assets/glue-resources/fonts/GoogleSans-Medium.ttf rename to src/assets/fonts/GoogleSans-Medium.ttf diff --git a/src/assets/glue-resources/fonts/GoogleSans-Regular.ttf b/src/assets/fonts/GoogleSans-Regular.ttf similarity index 100% rename from src/assets/glue-resources/fonts/GoogleSans-Regular.ttf rename to src/assets/fonts/GoogleSans-Regular.ttf diff --git a/src/assets/glue-resources/fonts/GoogleSans-Thin.ttf b/src/assets/fonts/GoogleSans-Thin.ttf similarity index 100% rename from src/assets/glue-resources/fonts/GoogleSans-Thin.ttf rename to src/assets/fonts/GoogleSans-Thin.ttf diff --git a/src/assets/glue-resources/fonts/Roboto-Black.ttf b/src/assets/fonts/Roboto-Black.ttf similarity index 100% rename from src/assets/glue-resources/fonts/Roboto-Black.ttf rename to src/assets/fonts/Roboto-Black.ttf diff --git a/src/assets/glue-resources/fonts/Roboto-Bold.ttf b/src/assets/fonts/Roboto-Bold.ttf similarity index 100% rename from src/assets/glue-resources/fonts/Roboto-Bold.ttf rename to src/assets/fonts/Roboto-Bold.ttf diff --git a/src/assets/glue-resources/fonts/Roboto-Light.ttf b/src/assets/fonts/Roboto-Light.ttf similarity index 100% rename from src/assets/glue-resources/fonts/Roboto-Light.ttf rename to src/assets/fonts/Roboto-Light.ttf diff --git a/src/assets/glue-resources/fonts/Roboto-Medium.ttf b/src/assets/fonts/Roboto-Medium.ttf similarity index 100% rename from src/assets/glue-resources/fonts/Roboto-Medium.ttf rename to src/assets/fonts/Roboto-Medium.ttf diff --git a/src/assets/glue-resources/fonts/Roboto-Regular.ttf b/src/assets/fonts/Roboto-Regular.ttf similarity index 100% rename from src/assets/glue-resources/fonts/Roboto-Regular.ttf rename to src/assets/fonts/Roboto-Regular.ttf diff --git a/src/assets/glue-resources/fonts/Roboto-Thin.ttf b/src/assets/fonts/Roboto-Thin.ttf similarity index 100% rename from src/assets/glue-resources/fonts/Roboto-Thin.ttf rename to src/assets/fonts/Roboto-Thin.ttf diff --git a/src/svg/arrow-down.svg b/src/icons/arrow-down.svg similarity index 77% rename from src/svg/arrow-down.svg rename to src/icons/arrow-down.svg index 2de2e8f..68e8fda 100644 --- a/src/svg/arrow-down.svg +++ b/src/icons/arrow-down.svg @@ -1,3 +1,3 @@ - + \ No newline at end of file diff --git a/src/svg/code.svg b/src/icons/code.svg similarity index 87% rename from src/svg/code.svg rename to src/icons/code.svg index 4fbffd1..1892947 100644 --- a/src/svg/code.svg +++ b/src/icons/code.svg @@ -1,3 +1,3 @@ - + \ No newline at end of file diff --git a/src/svg/cog.svg b/src/icons/cog.svg similarity index 90% rename from src/svg/cog.svg rename to src/icons/cog.svg index c003872..1d22a59 100644 --- a/src/svg/cog.svg +++ b/src/icons/cog.svg @@ -1,3 +1,3 @@ - + \ No newline at end of file diff --git a/src/icons/folder-open.light.svg b/src/icons/folder-open.light.svg new file mode 100644 index 0000000..9dccaa7 --- /dev/null +++ b/src/icons/folder-open.light.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/icons/folder-open.svg b/src/icons/folder-open.svg new file mode 100644 index 0000000..58884f9 --- /dev/null +++ b/src/icons/folder-open.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/icons/folder.light.svg b/src/icons/folder.light.svg new file mode 100644 index 0000000..79f5c39 --- /dev/null +++ b/src/icons/folder.light.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/icons/folder.svg b/src/icons/folder.svg new file mode 100644 index 0000000..f918b31 --- /dev/null +++ b/src/icons/folder.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/icons/note.svg b/src/icons/note.svg new file mode 100644 index 0000000..74aaa85 --- /dev/null +++ b/src/icons/note.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/svg/undo.svg b/src/icons/undo.svg similarity index 87% rename from src/svg/undo.svg rename to src/icons/undo.svg index 7276eb1..7397f55 100644 --- a/src/svg/undo.svg +++ b/src/icons/undo.svg @@ -1,3 +1,3 @@ - + \ No newline at end of file diff --git a/src/svg/wifi-slash.svg b/src/icons/wifi-slash.svg similarity index 91% rename from src/svg/wifi-slash.svg rename to src/icons/wifi-slash.svg index 3fb33e2..e02f87a 100644 --- a/src/svg/wifi-slash.svg +++ b/src/icons/wifi-slash.svg @@ -1,3 +1,3 @@ - + \ No newline at end of file diff --git a/src/js/ConfigMenu.js b/src/js/ConfigMenu.js index 529ff81..5149d88 100644 --- a/src/js/ConfigMenu.js +++ b/src/js/ConfigMenu.js @@ -1,9 +1,8 @@ import $ from "jquery"; -import MarkdownIt from "markdown-it"; -import MarkdownItAttrs from "markdown-it-attrs"; -import MarkdownItBracketedSpans from "markdown-it-bracketed-spans"; -import svgUndo from "svg/undo"; +import { renderMD } from "./Util"; + +import iconUndo from "icon/undo"; export default class ConfigMenu { /** @@ -69,21 +68,10 @@ export default class ConfigMenu { /** @type {Spicetify.Menu.Item} */ #configButton; - /** @type {MarkdownIt} */ - #md; - constructor() { this.#config = {}; this.#configButton = new Spicetify.Menu.Item("Dribbblish Settings", false, () => this.open()); this.#configButton.register(); - this.#md = MarkdownIt({ - html: true, - breaks: true, - linkify: true, - typographer: true - }); - this.#md.use(MarkdownItBracketedSpans); - this.#md.use(MarkdownItAttrs); const container = document.createElement("div"); container.id = "dribbblish-config"; @@ -134,9 +122,9 @@ export default class ConfigMenu {

${options.name} - ${options.resetButton ? /* html */ `` : ""} + ${options.resetButton ? /* html */ `` : ""}

- +
` : "" diff --git a/src/js/Folders.js b/src/js/Folders.js new file mode 100644 index 0000000..909522a --- /dev/null +++ b/src/js/Folders.js @@ -0,0 +1,116 @@ +import { waitForElement, htmlToNode } from "./Util"; + +import iconFolder from "icon/folder.light"; +import iconFolderOpen from "icon/folder-open.light"; +import iconNote from "icon/note"; + +waitForElement([`.main-rootlist-rootlistPlaylistsScrollNode ul[tabindex="0"]`, `.main-rootlist-rootlistPlaylistsScrollNode ul[tabindex="0"] li`], ([root, firstItem]) => { + const listElem = firstItem.parentElement; + root.classList.add("dribs-playlist-list"); + + /** Replace Playlist name with their pictures */ + function loadPlaylistImage() { + for (const item of listElem.children) { + let link = item.querySelector("a"); + if (!link) continue; + + let [_, app, uid] = link.pathname.split("/"); + let uri; + if (app === "playlist") { + uri = Spicetify.URI.playlistV2URI(uid); + } else if (app === "folder") { + const base64 = localStorage.getItem("dribbblish:folder-image:" + uid); + if (base64 != null) { + if (link.querySelector("svg")) link.querySelector("svg").remove(); + let img = link.querySelector("img"); + if (!img) { + img = document.createElement("img"); + img.classList.add("playlist-picture"); + link.prepend(img); + } + img.src = base64; + } else { + if (link.querySelector("img")) link.querySelector("img").remove(); + let svg = link.querySelector("svg"); + if (!svg) { + svg = htmlToNode(iconFolder); + svg.classList.add("playlist-picture"); + link.prepend(svg); + } + } + continue; + } + + Spicetify.CosmosAsync.get(`sp://core-playlist/v1/playlist/${uri.toURI()}/metadata`, { policy: { picture: true } }).then((res) => { + const { picture } = res.metadata; + if (picture != null && picture.trim() != "") { + if (link.querySelector("svg")) link.querySelector("svg").remove(); + let img = link.querySelector("img"); + if (!img) { + img = document.createElement("img"); + img.classList.add("playlist-picture"); + link.prepend(img); + } + img.src = picture; + } else { + if (link.querySelector("img")) link.querySelector("img").remove(); + let svg = link.querySelector("svg"); + if (!svg) { + svg = htmlToNode(iconNote); + svg.classList.add("playlist-picture"); + link.prepend(svg); + } + } + }); + } + } + loadPlaylistImage(); + new MutationObserver(loadPlaylistImage).observe(listElem, { childList: true }); + + const filePickerForm = document.createElement("form"); + filePickerForm.setAttribute("aria-hidden", true); + filePickerForm.innerHTML = ''; + document.body.appendChild(filePickerForm); + /** @type {HTMLInputElement} */ + const filePickerInput = filePickerForm.childNodes[0]; + filePickerInput.accept = ["image/jpeg", "image/apng", "image/avif", "image/gif", "image/png", "image/svg+xml", "image/webp"].join(","); + + filePickerInput.onchange = () => { + if (!filePickerInput.files.length) return; + + const file = filePickerInput.files[0]; + const reader = new FileReader(); + reader.onload = (event) => { + const result = event.target.result; + const id = Spicetify.URI.from(filePickerInput.uri).id; + try { + localStorage.setItem("dribbblish:folder-image:" + id, result); + } catch { + Spicetify.showNotification("File too large"); + } + loadPlaylistImage(); + }; + reader.readAsDataURL(file); + }; + + new Spicetify.ContextMenu.Item( + "Remove folder image", + ([uri]) => { + const id = Spicetify.URI.from(uri).id; + localStorage.removeItem("dribbblish:folder-image:" + id); + loadPlaylistImage(); + }, + ([uri]) => Spicetify.URI.isFolder(uri), + "x" + ).register(); + new Spicetify.ContextMenu.Item( + "Choose folder image", + ([uri]) => { + filePickerInput.uri = uri; + filePickerForm.reset(); + filePickerInput.click(); + }, + ([uri]) => Spicetify.URI.isFolder(uri), + "edit" + ).register(); +}); diff --git a/src/js/Util.js b/src/js/Util.js index 248a6f5..f25cb0e 100644 --- a/src/js/Util.js +++ b/src/js/Util.js @@ -1,3 +1,7 @@ +import MarkdownIt from "markdown-it"; +import MarkdownItAttrs from "markdown-it-attrs"; +import MarkdownItBracketedSpans from "markdown-it-bracketed-spans"; + /** * @callback waitForElCb * @param {HTMLElement[]} queries @@ -37,3 +41,22 @@ export function capitalizeFirstLetter(string) { export function getClosestToNum(arr, num) { return arr.reduce((prev, curr) => (Math.abs(curr - num) < Math.abs(prev - num) ? curr : prev)); } + +export function renderMD(src, env) { + const md = MarkdownIt({ + html: true, + breaks: true, + linkify: true, + typographer: true + }); + md.use(MarkdownItBracketedSpans); + md.use(MarkdownItAttrs); + + return md.render(src, env); +} + +export function htmlToNode(htmlStr) { + var div = document.createElement("div"); + div.innerHTML = htmlStr.trim(); + return div.firstChild; +} diff --git a/src/js/main.js b/src/js/main.js index ed34c28..b6ef2d3 100644 --- a/src/js/main.js +++ b/src/js/main.js @@ -7,11 +7,12 @@ import moment from "moment"; import { waitForElement, copyToClipboard, capitalizeFirstLetter, getClosestToNum } from "./Util"; import ConfigMenu from "./ConfigMenu"; import Info from "./Info"; +import "./Folders"; -import svgArrowDown from "svg/arrow-down"; -import svgCode from "svg/code"; -import svgWifiSlash from "svg/wifi-slash"; -import svgCog from "svg/cog"; +import iconArrowDown from "icon/arrow-down"; +import iconCode from "icon/code"; +import iconWifiSlash from "icon/wifi-slash"; +import iconCog from "icon/cog"; const Dribbblish = { config: new ConfigMenu(), @@ -29,7 +30,7 @@ Dribbblish.config.register({ defaultValue: true, onChange: (val) => Dribbblish.info[val ? "set" : "remove"]("settings", { - icon: svgCog, + icon: iconCog, color: { fg: "var(--spice-subtext)", bg: "rgba(var(--spice-rgb-subtext), calc(0.1 + var(--is_light) * 0.05))" @@ -153,49 +154,6 @@ waitForElement(["#main"], () => { }); }); -waitForElement([`.main-rootlist-rootlistPlaylistsScrollNode ul[tabindex="0"]`, `.main-rootlist-rootlistPlaylistsScrollNode ul[tabindex="0"] li`], ([root, firstItem]) => { - const listElem = firstItem.parentElement; - root.classList.add("dribs-playlist-list"); - - /** Replace Playlist name with their pictures */ - function loadPlaylistImage() { - for (const item of listElem.children) { - let link = item.querySelector("a"); - if (!link) continue; - - let [_, app, uid] = link.pathname.split("/"); - let uri; - if (app === "playlist") { - uri = Spicetify.URI.playlistV2URI(uid); - } else if (app === "folder") { - const base64 = localStorage.getItem("dribbblish:folder-image:" + uid); - let img = link.querySelector("img"); - if (!img) { - img = document.createElement("img"); - img.classList.add("playlist-picture"); - link.prepend(img); - } - img.src = base64 || "/images/tracklist-row-song-fallback.svg"; - continue; - } - - Spicetify.CosmosAsync.get(`sp://core-playlist/v1/playlist/${uri.toURI()}/metadata`, { policy: { picture: true } }).then((res) => { - const meta = res.metadata; - let img = link.querySelector("img"); - if (!img) { - img = document.createElement("img"); - img.classList.add("playlist-picture"); - link.prepend(img); - } - img.src = meta.picture || "/images/tracklist-row-song-fallback.svg"; - }); - } - } - - loadPlaylistImage(); - new MutationObserver(loadPlaylistImage).observe(listElem, { childList: true }); -}); - waitForElement([".main-rootlist-rootlist", ".main-rootlist-wrapper > :nth-child(2) > :first-child", "#spicetify-show-list"], ([rootlist]) => { function checkSidebarPlaylistScroll() { const topDist = rootlist.getBoundingClientRect().top - document.querySelector("#spicetify-show-list:not(:empty), .main-rootlist-wrapper > :nth-child(2) > :first-child").getBoundingClientRect().top; @@ -295,53 +253,6 @@ waitForElement([".Root__main-view .os-resize-observer-host"], ([resizeHost]) => root.classList.remove("is-connectBarVisible"); } }); - - const filePickerForm = document.createElement("form"); - filePickerForm.setAttribute("aria-hidden", true); - filePickerForm.innerHTML = ''; - document.body.appendChild(filePickerForm); - /** @type {HTMLInputElement} */ - const filePickerInput = filePickerForm.childNodes[0]; - filePickerInput.accept = ["image/jpeg", "image/apng", "image/avif", "image/gif", "image/png", "image/svg+xml", "image/webp"].join(","); - - filePickerInput.onchange = () => { - if (!filePickerInput.files.length) return; - - const file = filePickerInput.files[0]; - const reader = new FileReader(); - reader.onload = (event) => { - const result = event.target.result; - const id = Spicetify.URI.from(filePickerInput.uri).id; - try { - localStorage.setItem("dribbblish:folder-image:" + id, result); - } catch { - Spicetify.showNotification("File too large"); - } - loadPlaylistImage(); - }; - reader.readAsDataURL(file); - }; - - new Spicetify.ContextMenu.Item( - "Remove folder image", - ([uri]) => { - const id = Spicetify.URI.from(uri).id; - localStorage.removeItem("dribbblish:folder-image:" + id); - loadPlaylistImage(); - }, - ([uri]) => Spicetify.URI.isFolder(uri), - "x" - ).register(); - new Spicetify.ContextMenu.Item( - "Choose folder image", - ([uri]) => { - filePickerInput.uri = uri; - filePickerForm.reset(); - filePickerInput.click(); - }, - ([uri]) => Spicetify.URI.isFolder(uri), - "edit" - ).register(); })(); /* Config settings */ @@ -839,8 +750,8 @@ function checkForUpdate() { .then((response) => response.json()) .then((data) => { const isDev = process.env.DRIBBBLISH_VERSION == "Dev"; - Dribbblish.info.set("update", isDev || data.tag_name > process.env.DRIBBBLISH_VERSION ? { text: `v${data.tag_name}`, tooltip: "Open Release page to download", icon: svgArrowDown, onClick: () => window.open("https://github.com/JulienMaille/dribbblish-dynamic-theme/releases/latest", "_blank") } : null); - Dribbblish.info.set("dev", isDev ? { tooltip: "Dev build", icon: svgCode } : null); + Dribbblish.info.set("update", isDev || data.tag_name > process.env.DRIBBBLISH_VERSION ? { text: `v${data.tag_name}`, tooltip: "Open Release page to download", icon: iconArrowDown, onClick: () => window.open("https://github.com/JulienMaille/dribbblish-dynamic-theme/releases/latest", "_blank") } : null); + Dribbblish.info.set("dev", isDev ? { tooltip: "Dev build", icon: iconCode } : null); }) .catch(console.error); } @@ -852,7 +763,7 @@ checkForUpdate(); window.addEventListener("offline", () => Dribbblish.info.set("offline", { tooltip: "Offline", - icon: svgWifiSlash, + icon: iconWifiSlash, order: 998, color: { fg: "#ffffff", diff --git a/src/loaders/icon-loader.js b/src/loaders/icon-loader.js new file mode 100644 index 0000000..b506183 --- /dev/null +++ b/src/loaders/icon-loader.js @@ -0,0 +1,22 @@ +const { parseSync: parseSVG, stringify: stringifySVG } = require("svgson"); + +module.exports = function (content, map, meta) { + const query = new URLSearchParams(this.resourceQuery); + const svg = parseSVG(content); + + svg.attributes.type = "icon"; + svg.attributes.width = query.get("width") ?? 16; + svg.attributes.height = query.get("height") ?? 16; + + svg.children = svg.children.map((c) => { + if (c.attributes.fill != null && query.has("fill")) c.attributes.fill = query.get("fill"); + return c; + }); + + const svgStr = stringifySVG(svg); + if (query.has("base64")) { + return `data:image/svg+xml;base64,${Buffer.from(svgStr).toString("base64")}`; + } else { + return svgStr; + } +}; diff --git a/src/styles/Fonts.scss b/src/styles/Fonts.scss index 7733b08..666de08 100644 --- a/src/styles/Fonts.scss +++ b/src/styles/Fonts.scss @@ -20,7 +20,7 @@ $font-weights: ( font-family: $font; font-weight: $weight; font-style: normal; - src: url("glue-resources/fonts/#{$font}-#{$style}.ttf") format("truetype"); + src: url("fonts/#{$font}-#{$style}.ttf") format("truetype"); } } } diff --git a/src/styles/Info.scss b/src/styles/Info.scss index 2c7859e..16a5a68 100644 --- a/src/styles/Info.scss +++ b/src/styles/Info.scss @@ -16,7 +16,7 @@ height: 100%; padding: 0px 8px; color: spiceColor("sidebar-text"); - background-color: spiceColor("button"); + background-color: spiceColor("sidebar"); border-radius: var(--sidebar-icons-border-radius); line-height: 13px; @include spiceFont("glue", 14px, "Medium"); diff --git a/src/styles/main.scss b/src/styles/main.scss index 8cfd197..6085dd5 100644 --- a/src/styles/main.scss +++ b/src/styles/main.scss @@ -638,7 +638,7 @@ html.sidebar-hide-text .GlueDropTarget span { padding: 0 12px; } -img.playlist-picture { +.playlist-picture { width: 32px; height: 32px; flex: 0 0 32px; @@ -646,6 +646,10 @@ img.playlist-picture { background-size: cover; background-position: center; border-radius: var(--sidebar-icons-border-radius); + + &[type="icon"] { + padding: 2px; + } } .main-rootlist-rootlistItem a span { @@ -737,12 +741,9 @@ li.GlueDropTarget { padding-top: 32px; } -#main[top-bar="none-padding"] .spotify__container--is-desktop.spotify__os--is-windows[dir="ltr"] .Root__top-bar + .main-buddyFeed-buddyFeedRoot .main-topBar-container { - padding-right: 167px; -} - .main-topBar-container { max-width: unset; + padding: 16px 32px !important; } /** Custom elements */ @@ -969,7 +970,8 @@ span.main-userWidget-displayName, .main-rootlist-wrapper > div:nth-child(2) > li img, .main-navBar-navBarLink > svg, -.main-navBar-navBarLink > .icon { +.main-navBar-navBarLink > .icon, +.main-rootlist-rootlistItemLink > svg { z-index: 1; } @@ -1051,7 +1053,7 @@ div.GlueDropTarget.personal-library > *.active { } html.right-expanded-cover .main-coverSlotExpanded-container { - right: var(--main-gap); + right: calc(var(--main-gap) * 2); left: unset; } diff --git a/webpack.config.js b/webpack.config.js index 3f6514f..d645468 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -14,7 +14,7 @@ module.exports = { resolve: { extensions: [".js", ".svg"], alias: { - svg: path.resolve(__dirname, "./src/svg") + icon: path.resolve(__dirname, "./src/icons") } }, module: { @@ -49,23 +49,24 @@ module.exports = { }, { test: /\.svg/, - exclude: /node_modules/, - type: "asset/source" + type: "asset/source", + resourceQuery: /.?/, + use: [path.resolve(__dirname, "./src/loaders/icon-loader.js")] } ] }, devtool: "inline-source-map", plugins: [ - new CleanWebpackPlugin({ - protectWebpackAssets: false, - cleanAfterEveryBuildPatterns: ["*.LICENSE.txt"] + new CopyPlugin({ + patterns: [{ from: "src/assets", to: "assets" }] }), new webpack.DefinePlugin({ "process.env.DRIBBBLISH_VERSION": JSON.stringify(process.env.DRIBBBLISH_VERSION || "Dev"), "process.env.COMMIT_HASH": JSON.stringify(process.env.COMMIT_HASH || "local") }), - new CopyPlugin({ - patterns: [{ from: "src/assets", to: "assets" }] + new CleanWebpackPlugin({ + protectWebpackAssets: false, + cleanAfterEveryBuildPatterns: ["*.LICENSE.txt"] }) ] };