Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 2 additions & 6 deletions plugins/DiscordRPC/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,10 @@ export const { trace, errSignal } = Tracer("[DiscordRPC]");
export { Settings } from "./Settings";

redux.intercept(["playbackControls/SEEK", "playbackControls/SET_PLAYBACK_STATE"], unloads, () => {
updateActivity()
.then(() => (errSignal!._ = undefined))
.catch(trace.err.withContext("Failed to set activity"));
updateActivity();
});
MediaItem.onMediaTransition(unloads, (mediaItem) => {
updateActivity(mediaItem)
.then(() => (errSignal!._ = undefined))
.catch(trace.err.withContext("Failed to set activity"));
updateActivity(mediaItem);
});
unloads.add(cleanupRPC.bind(cleanupRPC));

Expand Down
54 changes: 53 additions & 1 deletion plugins/DiscordRPC/src/updateActivity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,63 @@ import type { SetActivity } from "@xhayper/discord-rpc";
import { fmtStr, getStatusText } from "./activityTextHelpers";
import { setActivity, StatusDisplayTypeEnum } from "./discord.native";
import { settings } from "./Settings";
import { trace, errSignal } from "./index";

// Proxy this so we dont try import a node native module
const StatusDisplayType = await StatusDisplayTypeEnum();

export const updateActivity = async (mediaItem?: MediaItem) => {
// Debounce state
const DEBOUNCE_MS = 300;
let debounceTimer: ReturnType<typeof setTimeout> | null = null;
let isUpdating = false;
let pendingUpdate: MediaItem | undefined | null = null; // null = no pending, undefined = pending with no mediaItem

/**
* Debounced wrapper for _updateActivity
* - Waits DEBOUNCE_MS after last call before executing
* - If already updating, queues the next update
*/
export const updateActivity = (mediaItem?: MediaItem) => {
// If currently updating, mark that we need another update after
if (isUpdating) {
pendingUpdate = mediaItem;
return;
}

// Clear existing timer
if (debounceTimer) clearTimeout(debounceTimer);

// Set new debounce timer
debounceTimer = setTimeout(async () => {
debounceTimer = null;
await executeUpdate(mediaItem);
}, DEBOUNCE_MS);
};

/**
* Execute the actual update with mutex protection
*/
const executeUpdate = async (mediaItem?: MediaItem) => {
isUpdating = true;
try {
await _updateActivity(mediaItem);
errSignal!._ = undefined;
} catch (e) {
trace.err.withContext("Failed to set activity")(e);
} finally {
isUpdating = false;
if (pendingUpdate !== null) {
const pending = pendingUpdate;
pendingUpdate = null;
updateActivity(pending);
}
}
};

/**
* Internal update implementation (no debounce/mutex)
*/
const _updateActivity = async (mediaItem?: MediaItem) => {
if (!PlayState.playing && !settings.displayOnPause) return await setActivity();

mediaItem ??= await MediaItem.fromPlaybackContext();
Expand Down
2 changes: 1 addition & 1 deletion plugins/ListenBrainz/src/Settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export const Settings = () => {
desc={
<>
User token from{" "}
<LunaLink fontWeight="bold" href={`${domain}/settings`}>
<LunaLink fontWeight="bold" href={`${domain?.replace("api.", "")}/settings`}>
listenbrainz.org/settings
</LunaLink>
</>
Expand Down
11 changes: 8 additions & 3 deletions plugins/NativeFullscreen/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { LunaUnload } from "@luna/core";
import { redux } from "@luna/lib";
import { observePromise, redux } from "@luna/lib";
import { storage } from "./Settings";
export { Settings } from "./Settings";

Expand All @@ -16,7 +16,12 @@ export const setTopBarVisibility = (visible: boolean) => {
const bar = document.querySelector<HTMLElement>("div[class^='_bar']");
if (bar) bar.style.display = visible ? "" : "none";
};
if (storage.hideTopBar) setTopBarVisibility(false);
// Apply hideTopBar setting on load
if (storage.hideTopBar) {
observePromise<HTMLElement>(unloads, "div[class^='_bar']").then((bar) => {
if (bar) setTopBarVisibility(false);
});
}

const onKeyDown = (event: KeyboardEvent) => {
if (event.key === "F11") {
Expand All @@ -27,7 +32,7 @@ const onKeyDown = (event: KeyboardEvent) => {

if (document.fullscreenElement || wimp?.classList.contains("is-fullscreen")) {
// Exiting fullscreen
document.exitFullscreen();
if (document.fullscreenElement) document.exitFullscreen();
if (wimp) wimp.classList.remove("is-fullscreen");
if (!storage.hideTopBar) setTopBarVisibility(true);
if (contentContainer) contentContainer.style.maxHeight = "";
Expand Down
89 changes: 60 additions & 29 deletions plugins/RealMax/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,45 +7,41 @@ import { settings } from "./Settings";

export { errSignal, unloads } from "./index.safe";

const getFormatString = async (mediaItem: MediaItem): Promise<string> => {
const quality = mediaItem.bestQuality;
try {
const format = await mediaItem.updateFormat(quality.audioQuality);
if (format?.bitDepth && format?.sampleRate) return `${format.bitDepth}bit/${format.sampleRate / 1000}kHz`;
} catch {}
return quality.name;
};

const getMaxItem = async (mediaItem?: MediaItem) => {
const maxItem = await mediaItem?.max();
if (maxItem === undefined) return;
if (settings.displayInfoPopups) trace.msg.log(`Found replacement for ${mediaItem!.tidalItem.title}`);
if (settings.displayInfoPopups) {
const [fromFmt, toFmt] = await Promise.all([getFormatString(mediaItem!), getFormatString(maxItem)]);
trace.msg.log(`Found replacement for ${mediaItem!.tidalItem.title} (${fromFmt} -> ${toFmt})`);
}
return maxItem;
};

const playMaxItem = async (elements: redux.PlayQueueElement[], index: number) => {
const newElements = [...elements];
if (newElements[index]?.mediaItemId === undefined) return false;

const mediaItem = await MediaItem.fromId(newElements[index].mediaItemId);
const maxItem = await getMaxItem(mediaItem);
if (maxItem === undefined) return false;

newElements[index] = { ...newElements[index], mediaItemId: maxItem.id };
PlayState.updatePlayQueue({
elements: newElements,
currentIndex: index,
});
return true;
};

export { Settings } from "./Settings";

// Prefetch max on preload
MediaItem.onPreload(unloads, (mediaItem) => mediaItem.max().catch(trace.err.withContext("onPreload.max")));

MediaItem.onPreMediaTransition(unloads, async (mediaItem) => {
const maxItem = await getMaxItem(mediaItem);
if (!PlayState.playing) return;

PlayState.pause();
try {
if (maxItem !== undefined) PlayState.playNext(maxItem.id);
} catch (err) {
trace.msg.err.withContext("addNext")(err);
if (maxItem !== undefined && PlayState.playing) {
PlayState.pause();
try {
PlayState.playNext(maxItem.id);
} catch (err) {
trace.msg.err.withContext("addNext")(err);
}
PlayState.play();
}
PlayState.play();

// Preload next item
const nextItem = await PlayState.nextMediaItem();
Expand All @@ -70,18 +66,53 @@ redux.intercept("playQueue/ADD_NOW", unloads, (payload) => {
redux.intercept(["playQueue/MOVE_TO", "playQueue/MOVE_NEXT", "playQueue/MOVE_PREVIOUS"], unloads, (payload, action) => {
(async () => {
const { elements, currentIndex } = PlayState.playQueue;
let targetIndex: number;
switch (action) {
case "playQueue/MOVE_NEXT":
if (!(await playMaxItem(elements, currentIndex + 1))) PlayState.next();
targetIndex = currentIndex + 1;
break;
case "playQueue/MOVE_PREVIOUS":
if (!(await playMaxItem(elements, currentIndex - 1))) PlayState.previous();
targetIndex = currentIndex - 1;
break;
case "playQueue/MOVE_TO":
if (!(await playMaxItem(elements, payload ?? currentIndex))) PlayState.moveTo(payload ?? currentIndex);
targetIndex = payload ?? currentIndex;
break;
default:
return;
}

// Pre-swap the target track with its max version before transitioning
const element = elements[targetIndex];
if (element?.mediaItemId !== undefined) {
try {
const mediaItem = await MediaItem.fromId(element.mediaItemId);
const maxItem = await getMaxItem(mediaItem);
if (maxItem !== undefined) {
const newElements = [...elements];
newElements[targetIndex] = { ...newElements[targetIndex], mediaItemId: maxItem.id };
// Update queue but keep currentIndex unchanged — only swap the future track
PlayState.updatePlayQueue({
elements: newElements,
currentIndex,
});
}
} catch (err) {
trace.err.withContext(action)(err);
}
}

// Transition using normal PlayState methods (works with Tidal Connect)
switch (action) {
case "playQueue/MOVE_NEXT":
PlayState.next();
break;
case "playQueue/MOVE_PREVIOUS":
PlayState.previous();
break;
case "playQueue/MOVE_TO":
PlayState.moveTo(payload ?? currentIndex);
break;
}
PlayState.play();
})();
return true;
});
18 changes: 8 additions & 10 deletions plugins/Themer/src/editor.html
Original file line number Diff line number Diff line change
Expand Up @@ -40,17 +40,15 @@
});

require(["vs/editor/editor.main"], () => {
const editor = window.editor = monaco.editor.create(
document.getElementById("container"),
{
value: window.themerCSS,
language: "css",
theme: "vs-dark",
smoothScrolling: true,
}
);
const container = document.getElementById("container");
const editor = window.editor = monaco.editor.create(container, {
value: window.themerCSS,
language: "css",
theme: "vs-dark",
smoothScrolling: true,
});
editor.onDidChangeModelContent(() => ipcRenderer.setCSS(editor.getValue()));
window.addEventListener("resize", editor.layout.bind(editor));
new ResizeObserver(() => editor.layout()).observe(container);
});
</script>
</body>
Expand Down
10 changes: 5 additions & 5 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading