Skip to content

Commit 2298d76

Browse files
author
NellowTCS
committed
bunch of changes
1 parent 0902010 commit 2298d76

34 files changed

+1931
-1393
lines changed

Build/package-lock.json

Lines changed: 114 additions & 114 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Build/src/platform/library/library.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,9 @@ export class LibraryManager implements LibraryActions {
276276
}
277277
}
278278

279-
export function flattenPlaylists(items: (Playlist | PlaylistFolder)[]): Playlist[] {
279+
export function flattenPlaylists(
280+
items: (Playlist | PlaylistFolder)[],
281+
): Playlist[] {
280282
const result: Playlist[] = [];
281283
for (const item of items) {
282284
if ("songs" in item) {

Build/src/platform/library/types.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
import type { Track, Playlist, PlaylistItem, PlaylistFolder } from "../../core/engine/types";
1+
import type {
2+
Track,
3+
Playlist,
4+
PlaylistItem,
5+
PlaylistFolder,
6+
} from "../../core/engine/types";
27

38
export interface LibraryState {
49
songs: Track[];

Build/src/platform/lyrics/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { cleanMetadata, createMasterFilter } from "./metadataFilter";
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import {
2+
MetadataFilter,
3+
createYouTubeFilter,
4+
createSpotifyFilter,
5+
createAmazonFilter,
6+
createTidalFilter,
7+
createRemasteredFilter,
8+
} from "@web-scrobbler/metadata-filter";
9+
10+
const removeExtraSuffixes = (text: string) =>
11+
text
12+
.replace(/ - Topic$/i, "")
13+
.replace(/\s*\[.*?\]$/i, "")
14+
.trim();
15+
16+
const removeArtistFromTitle = (artistName: string) => (text: string) => {
17+
if (!artistName) return text;
18+
const escapedArtist = artistName.replace(/[.*+?^${}()|[\]\\]/g, "");
19+
const regex = new RegExp(`^${escapedArtist}\\s*[-:|]?\\s*`, "i");
20+
return text.replace(regex, "").trim();
21+
};
22+
23+
export const createMasterFilter = (artistName: string) =>
24+
new MetadataFilter({})
25+
.extend(createYouTubeFilter())
26+
.extend(createSpotifyFilter())
27+
.extend(createAmazonFilter())
28+
.extend(createTidalFilter())
29+
.extend(createRemasteredFilter())
30+
.extend(
31+
new MetadataFilter({
32+
artist: removeExtraSuffixes,
33+
track: [removeExtraSuffixes, removeArtistFromTitle(artistName)],
34+
}),
35+
);
36+
37+
export const cleanMetadata = (artist: string, title: string) => {
38+
const masterFilter = createMasterFilter(artist);
39+
40+
const cleanedArtist = masterFilter.canFilterField("artist")
41+
? masterFilter.filterField("artist", artist)
42+
: artist;
43+
44+
const cleanedTitle = masterFilter.canFilterField("track")
45+
? masterFilter.filterField("track", title)
46+
: title;
47+
48+
return { artist: cleanedArtist, title: cleanedTitle };
49+
};

Build/src/platform/metadata/musicMetadata.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import { BaseMetadataExtractor } from "./base";
22
import type { ExtractedMetadata, MetadataExtractor } from "./base";
3-
import type { EmbeddedLyrics, EncodingDetails, GaplessInfo } from "../../core/engine/types";
3+
import type {
4+
EmbeddedLyrics,
5+
EncodingDetails,
6+
GaplessInfo,
7+
} from "../../core/engine/types";
48

59
export class MusicMetadataExtractor extends BaseMetadataExtractor {
610
async extractMetadata(file: File | Blob): Promise<ExtractedMetadata> {
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { getCurrentThemeCSS } from "../../ui/theming";
2+
3+
const themeChannel = new BroadcastChannel("theme-updates");
4+
5+
const themeVars = [
6+
"--themecolor", "--themecolor2", "--themecolor3", "--themecolor4",
7+
"--themegradient", "--themecolor-transparent", "--themecolor2-transparent",
8+
"--themecolor3-transparent", "--foreground", "--foreground-strong",
9+
"--foreground-stronger", "--foreground-muted", "--foreground-subtle",
10+
"--background", "--surface", "--surface-foreground",
11+
"--surface-transparent-05", "--surface-transparent-1", "--surface-transparent-2",
12+
"--primary", "--primary-foreground", "--primary-transparent",
13+
"--primary-border", "--primary-border-strong", "--secondary",
14+
"--secondary-foreground", "--menu-background", "--spacing-1",
15+
"--spacing-2", "--spacing-3", "--spacing-4", "--radius", "--radius-lg",
16+
];
17+
18+
export function broadcastThemeCSS() {
19+
const themeCSS = getCurrentThemeCSS();
20+
const isDarkMode = document.documentElement.classList.contains("dark");
21+
22+
if (themeCSS && themeCSS.trim() !== ":root {\n \n}") {
23+
themeChannel.postMessage({
24+
type: "theme-css",
25+
css: themeCSS,
26+
darkMode: isDarkMode,
27+
timestamp: Date.now(),
28+
});
29+
} else {
30+
const rootStyle = getComputedStyle(document.documentElement);
31+
const fallbackVariables: string[] = [];
32+
33+
themeVars.forEach((varName) => {
34+
const value = rootStyle.getPropertyValue(varName).trim();
35+
if (value) {
36+
fallbackVariables.push(`${varName}: ${value};`);
37+
}
38+
});
39+
40+
if (fallbackVariables.length > 0) {
41+
const fallbackCSS = `:root {\n ${fallbackVariables.join("\n ")}\n}`;
42+
themeChannel.postMessage({
43+
type: "theme-css",
44+
css: fallbackCSS,
45+
darkMode: isDarkMode,
46+
timestamp: Date.now(),
47+
fallback: true,
48+
});
49+
}
50+
}
51+
}
52+
53+
export function listenForThemeUpdates() {
54+
themeChannel.onmessage = () => {};
55+
}

Build/src/platform/pip/index.tsx

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
import { createRoot } from "react-dom/client";
2+
import { getCurrentThemeCSS } from "../../ui/theming";
3+
4+
let pipWindow: Window | null = null;
5+
6+
export function copyAllStyles(pipWindow: Window) {
7+
document.querySelectorAll('link[rel="stylesheet"]').forEach((link) => {
8+
const newLink = pipWindow.document.createElement("link");
9+
newLink.rel = "stylesheet";
10+
newLink.href = (link as HTMLLinkElement).href;
11+
newLink.type = "text/css";
12+
pipWindow.document.head.appendChild(newLink);
13+
});
14+
15+
document.querySelectorAll("style").forEach((style) => {
16+
const newStyle = pipWindow.document.createElement("style");
17+
newStyle.textContent = style.textContent;
18+
19+
Array.from(style.attributes).forEach((attr) => {
20+
newStyle.setAttribute(attr.name, attr.value);
21+
});
22+
23+
pipWindow.document.head.appendChild(newStyle);
24+
});
25+
26+
let copiedRulesCount = 0;
27+
try {
28+
[...document.styleSheets].forEach((styleSheet) => {
29+
try {
30+
const cssRules = [...styleSheet.cssRules]
31+
.map((rule) => rule.cssText)
32+
.join("");
33+
if (cssRules) {
34+
const style = pipWindow.document.createElement("style");
35+
style.textContent = cssRules;
36+
pipWindow.document.head.appendChild(style);
37+
copiedRulesCount += styleSheet.cssRules.length;
38+
}
39+
} catch (e) {
40+
console.warn("Could not access stylesheet:", styleSheet.href, e);
41+
}
42+
});
43+
} catch (e) {
44+
console.warn("Could not copy some stylesheets:", e);
45+
}
46+
47+
console.log(`Copied ${copiedRulesCount} CSS rules to PiP window`);
48+
}
49+
50+
export const isMiniplayerSupported = (): boolean => {
51+
return (
52+
"documentPictureInPicture" in window && !!window.documentPictureInPicture
53+
);
54+
};
55+
56+
export interface MiniplayerControls {
57+
togglePlayPause: () => void;
58+
next: () => void;
59+
previous: () => void;
60+
playerState: {
61+
currentSong: any;
62+
isPlaying: boolean;
63+
};
64+
}
65+
66+
declare global {
67+
interface Window {
68+
documentPictureInPicture?: {
69+
requestWindow: (options: {
70+
width: number;
71+
height: number;
72+
}) => Promise<Window & { close: () => void }>;
73+
};
74+
}
75+
}
76+
77+
export async function toggleMiniplayer(
78+
controls: MiniplayerControls,
79+
MiniplayerContent: React.ComponentType<{ controls: MiniplayerControls }>,
80+
) {
81+
if (!controls.playerState.currentSong) {
82+
console.error("No song is currently playing");
83+
return;
84+
}
85+
86+
try {
87+
if (pipWindow) {
88+
pipWindow.close();
89+
pipWindow = null;
90+
return;
91+
}
92+
93+
if (
94+
!("documentPictureInPicture" in window) ||
95+
!window.documentPictureInPicture
96+
) {
97+
console.error("Document Picture-in-Picture not supported");
98+
return;
99+
}
100+
101+
const newPipWindow = await window.documentPictureInPicture.requestWindow({
102+
width: 400,
103+
height: 70,
104+
});
105+
pipWindow = newPipWindow;
106+
107+
if (document.documentElement.classList.contains("dark")) {
108+
newPipWindow.document.documentElement.classList.add("dark");
109+
}
110+
111+
copyAllStyles(newPipWindow);
112+
113+
await new Promise((resolve) => setTimeout(resolve, 300));
114+
115+
const themeCSS = getCurrentThemeCSS();
116+
if (themeCSS && themeCSS.trim() !== ":root {\n \n}") {
117+
const themeStyle = newPipWindow.document.createElement("style");
118+
themeStyle.textContent = themeCSS;
119+
themeStyle.setAttribute("data-theme-variables", "true");
120+
newPipWindow.document.head.appendChild(themeStyle);
121+
} else {
122+
applyFallbackThemeVariables(newPipWindow);
123+
}
124+
125+
newPipWindow.document.body.style.margin = "0";
126+
newPipWindow.document.body.style.padding = "0";
127+
newPipWindow.document.body.style.overflow = "hidden";
128+
129+
const rootElement = createRoot(newPipWindow.document.body);
130+
rootElement.render(<MiniplayerContent controls={controls} />);
131+
132+
newPipWindow.addEventListener("pagehide", () => {
133+
rootElement.unmount();
134+
pipThemeChannel.close();
135+
pipWindow = null;
136+
});
137+
138+
const pipThemeChannel = new BroadcastChannel("theme-updates");
139+
140+
pipThemeChannel.onmessage = (event) => {
141+
if (event.data.type === "theme-css") {
142+
if (event.data.darkMode) {
143+
newPipWindow.document.documentElement.classList.add("dark");
144+
} else {
145+
newPipWindow.document.documentElement.classList.remove("dark");
146+
}
147+
148+
const existingThemeStyles = newPipWindow.document.querySelectorAll(
149+
'style[data-theme-variables], style[data-fallback-theme-variables]',
150+
);
151+
existingThemeStyles.forEach((style) => style.remove());
152+
153+
const styleElement = newPipWindow.document.createElement("style");
154+
styleElement.textContent = event.data.css;
155+
styleElement.setAttribute("data-theme-variables", "true");
156+
if (event.data.fallback) {
157+
styleElement.setAttribute("data-fallback-theme-variables", "true");
158+
}
159+
newPipWindow.document.head.appendChild(styleElement);
160+
newPipWindow.document.body.offsetHeight;
161+
}
162+
};
163+
} catch (err) {
164+
console.error("PiP failed:", err);
165+
pipWindow = null;
166+
}
167+
}
168+
169+
function applyFallbackThemeVariables(pipWindow: Window) {
170+
const rootStyle = getComputedStyle(document.documentElement);
171+
const fallbackVariables: string[] = [];
172+
const themeVars = [
173+
"--themecolor", "--themecolor2", "--themecolor3", "--themecolor4",
174+
"--themegradient", "--themecolor-transparent", "--themecolor2-transparent",
175+
"--themecolor3-transparent", "--foreground", "--foreground-strong",
176+
"--foreground-stronger", "--foreground-muted", "--foreground-subtle",
177+
"--background", "--surface", "--surface-foreground",
178+
"--surface-transparent-05", "--surface-transparent-1", "--surface-transparent-2",
179+
"--primary", "--primary-foreground", "--primary-transparent",
180+
"--primary-border", "--primary-border-strong", "--secondary",
181+
"--secondary-foreground", "--menu-background", "--spacing-1",
182+
"--spacing-2", "--spacing-3", "--spacing-4", "--radius", "--radius-lg",
183+
];
184+
185+
themeVars.forEach((varName) => {
186+
const value = rootStyle.getPropertyValue(varName).trim();
187+
if (value) {
188+
fallbackVariables.push(`${varName}: ${value};`);
189+
}
190+
});
191+
192+
if (fallbackVariables.length > 0) {
193+
const fallbackStyle = pipWindow.document.createElement("style");
194+
fallbackStyle.textContent = `:root {\n ${fallbackVariables.join("\n ")}\n}`;
195+
fallbackStyle.setAttribute("data-fallback-theme-variables", "true");
196+
pipWindow.document.head.appendChild(fallbackStyle);
197+
}
198+
}

Build/src/platform/storage/albumArt.ts

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@ export const albumArtStorage = {
2121
const tx = db.transaction(STORES.ALBUM_ART, "readonly");
2222
const store = tx.objectStore(STORES.ALBUM_ART);
2323

24-
const result = await new Promise<{ songId: string; albumArt: string } | undefined>((resolve, reject) => {
24+
const result = await new Promise<
25+
{ songId: string; albumArt: string } | undefined
26+
>((resolve, reject) => {
2527
const req = store.get(songId);
2628
req.onsuccess = () => resolve(req.result);
2729
req.onerror = () => reject(req.error);
@@ -60,13 +62,16 @@ export const albumArtStorage = {
6062
const store = tx.objectStore(STORES.ALBUM_ART);
6163

6264
const loaded = await Promise.all(
63-
toLoad.map((songId) =>
64-
new Promise<{ songId: string; albumArt: string } | null>((resolve, reject) => {
65-
const req = store.get(songId);
66-
req.onsuccess = () => resolve(req.result || null);
67-
req.onerror = () => reject(req.error);
68-
})
69-
)
65+
toLoad.map(
66+
(songId) =>
67+
new Promise<{ songId: string; albumArt: string } | null>(
68+
(resolve, reject) => {
69+
const req = store.get(songId);
70+
req.onsuccess = () => resolve(req.result || null);
71+
req.onerror = () => reject(req.error);
72+
},
73+
),
74+
),
7075
);
7176

7277
for (const art of loaded) {

Build/src/platform/storage/audio.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@ export const audioStorage = {
2222
store.put({ ...result, lastAccessed: Date.now() });
2323
}
2424

25-
return result ? { fileData: result.fileData, mimeType: result.mimeType } : null;
25+
return result
26+
? { fileData: result.fileData, mimeType: result.mimeType }
27+
: null;
2628
} catch (error) {
2729
console.error(`Failed to load audio data for song ${songId}:`, error);
2830
return null;

0 commit comments

Comments
 (0)