From cebc3799dd2f2f8835f9bb7f7c81ef5387d48d26 Mon Sep 17 00:00:00 2001 From: Mralimoh Date: Sat, 3 Oct 2026 15:08:17 +0330 Subject: [PATCH 1/8] Add trashed songs list with live search --- modules/trashbin/logic.ts | 7 ++-- modules/trashbin/mod.tsx | 83 +++++++++++++++++++++++++++++++++++---- 2 files changed, 79 insertions(+), 11 deletions(-) diff --git a/modules/trashbin/logic.ts b/modules/trashbin/logic.ts index e8d44c79..9fc0f0f3 100644 --- a/modules/trashbin/logic.ts +++ b/modules/trashbin/logic.ts @@ -39,11 +39,12 @@ export function targetMatchesCurrent( } export function toggleEntry( - list: Record, + list: Record, uri: string, -): { next: Record; added: boolean } { + displayName: string = "Unknown", +): { next: Record; added: boolean } { if (!list[uri]) { - return { next: { ...list, [uri]: true }, added: true }; + return { next: { ...list, [uri]: displayName }, added: true }; } const next = { ...list }; delete next[uri]; diff --git a/modules/trashbin/mod.tsx b/modules/trashbin/mod.tsx index 3e36110d..4468550b 100644 --- a/modules/trashbin/mod.tsx +++ b/modules/trashbin/mod.tsx @@ -37,11 +37,11 @@ const findSkipBackButton = (): HTMLElement | null => document.querySelector(".player-controls__left > button[data-encore-id='buttonTertiary']"); export default async function (ctx: ModuleRuntimeContext) { - const { useState } = React; + const { useState, useEffect } = React; const registrar = createRegistrar(ctx); - let trashSongList: Record = initValue("TrashSongList", {}); - let trashArtistList: Record = initValue("TrashArtistList", {}); + let trashSongList: Record = initValue("TrashSongList", {}); + let trashArtistList: Record = initValue("TrashArtistList", {}); let trashbinStatus: boolean = initValue("trashbin-enabled", true); let enableWidget: boolean = initValue("TrashbinWidgetIcon", true); let userHitBack = false; @@ -102,9 +102,14 @@ export default async function (ctx: ModuleRuntimeContext) { }; const toggleCurrent = () => { - const uri = client.player.data?.item?.uri; - if (!uri) return; - const { next, added } = toggleEntry(trashSongList, uri); + const item = client.player.data?.item; + if (!item?.uri) return; + + const trackName = item.name || item.metadata?.title || "Unknown Track"; + const artistName = item.metadata?.artist_name || ""; + const displayName = artistName ? `${trackName} - ${artistName}` : trackName; + + const { next, added } = toggleEntry(trashSongList, item.uri, displayName); trashSongList = next; if (added) { client.player.next(); @@ -119,7 +124,7 @@ export default async function (ctx: ModuleRuntimeContext) { // ----- playbar button (v3 register) ----- const TrashButton = () => { const [, force] = React.useReducer((n: number) => n + 1, 0); - React.useEffect(() => { + useEffect(() => { const on = () => force(); refreshers.add(force); client.player.addEventListener("songchange", on); @@ -183,6 +188,31 @@ export default async function (ctx: ModuleRuntimeContext) { function Settings() { const [enabled, setEnabled] = useState(trashbinStatus); const [widget, setWidget] = useState(enableWidget); + const [songs, setSongs] = useState(trashSongList); + const [searchQuery, setSearchQuery] = useState(""); + + useEffect(() => { + const sync = () => setSongs(trashSongList); + refreshers.add(sync); + return () => { + refreshers.delete(sync); + }; + }, []); + + const filteredSongs = Object.entries(songs).filter(([uri, name]) => { + const displayName = typeof name === "string" ? name : uri; + return displayName.toLowerCase().includes(searchQuery.toLowerCase()); + }); + + const removeSong = (uri: string) => { + const next = { ...trashSongList }; + delete next[uri]; + trashSongList = next; + setSongs(next); + putDataLocal(); + refreshButtons(); + client.notify("Song removed from trashbin"); + }; return ( @@ -238,6 +268,7 @@ export default async function (ctx: ModuleRuntimeContext) { onClick={() => { trashSongList = {}; trashArtistList = {}; + setSongs({}); putDataLocal(); refreshButtons(); client.notify("Trashbin cleared!"); @@ -246,6 +277,42 @@ export default async function (ctx: ModuleRuntimeContext) { Clear + +
+ setSearchQuery(e.target.value)} + style={{ + width: "100%", + padding: "8px 12px", + marginBottom: "10px", + borderRadius: "4px", + border: "1px solid rgba(255,255,255,0.2)", + background: "rgba(255,255,255,0.05)", + color: "var(--spice-text, #fff)", + boxSizing: "border-box", + outline: "none", + }} + /> + +
+ {filteredSongs.length === 0 ? ( +
+ {searchQuery ? "No matching songs found" : "Trashbin is empty"} +
+ ) : ( + filteredSongs.map(([uri, name]) => ( + + + + )) + )} +
+
); } @@ -304,4 +371,4 @@ export default async function (ctx: ModuleRuntimeContext) { client.player.removeEventListener("songchange", watchChange); cntxMenu.deregister(); }); -} +} \ No newline at end of file From c997b074c73c4ae03c28fee59c03ad43e5833c14 Mon Sep 17 00:00:00 2001 From: Mralimoh Date: Sat, 3 Oct 2026 15:12:09 +0330 Subject: [PATCH 2/8] style: format code --- modules/trashbin/mod.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/trashbin/mod.tsx b/modules/trashbin/mod.tsx index 4468550b..212bbb6e 100644 --- a/modules/trashbin/mod.tsx +++ b/modules/trashbin/mod.tsx @@ -371,4 +371,4 @@ export default async function (ctx: ModuleRuntimeContext) { client.player.removeEventListener("songchange", watchChange); cntxMenu.deregister(); }); -} \ No newline at end of file +} From 93f4759cbef7a0dce2fc3a4288fc8920f77573fe Mon Sep 17 00:00:00 2001 From: Mralimoh Date: Sat, 3 Oct 2026 15:15:36 +0330 Subject: [PATCH 3/8] fix: default entry value to true and format code --- modules/trashbin/logic.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/trashbin/logic.ts b/modules/trashbin/logic.ts index 9fc0f0f3..8112925e 100644 --- a/modules/trashbin/logic.ts +++ b/modules/trashbin/logic.ts @@ -41,7 +41,7 @@ export function targetMatchesCurrent( export function toggleEntry( list: Record, uri: string, - displayName: string = "Unknown", + displayName: any = true, ): { next: Record; added: boolean } { if (!list[uri]) { return { next: { ...list, [uri]: displayName }, added: true }; @@ -49,4 +49,4 @@ export function toggleEntry( const next = { ...list }; delete next[uri]; return { next, added: false }; -} +} \ No newline at end of file From c3287db2619ce674ebf45eaf2d5445c15a1103d2 Mon Sep 17 00:00:00 2001 From: Mralimoh Date: Sat, 3 Oct 2026 15:20:06 +0330 Subject: [PATCH 4/8] fix: format logic.ts --- modules/trashbin/logic.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/trashbin/logic.ts b/modules/trashbin/logic.ts index 8112925e..a8dc2481 100644 --- a/modules/trashbin/logic.ts +++ b/modules/trashbin/logic.ts @@ -49,4 +49,4 @@ export function toggleEntry( const next = { ...list }; delete next[uri]; return { next, added: false }; -} \ No newline at end of file +} From 606c13723c7f99bb6b426e85353c668cee4e4022 Mon Sep 17 00:00:00 2001 From: Mralimoh Date: Sat, 3 Oct 2026 15:22:26 +0330 Subject: [PATCH 5/8] chore: bump trashbin version to 0.2.7 --- modules/trashbin/metadata.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/trashbin/metadata.json b/modules/trashbin/metadata.json index c0e43fd2..06804015 100644 --- a/modules/trashbin/metadata.json +++ b/modules/trashbin/metadata.json @@ -1,7 +1,7 @@ { "name": "trashbin", "kind": "extension", - "version": "0.2.6", + "version": "0.2.7", "authors": ["spicetify"], "description": "Throw songs and artists in the trashbin and never hear them again.", "entries": { From 221013e543e610711836a94d0fd48246f7de15a6 Mon Sep 17 00:00:00 2001 From: Mralimoh Date: Sat, 3 Oct 2026 15:47:24 +0330 Subject: [PATCH 6/8] feat(trashbin): format song as Artist - Title and tune scroll height --- modules/trashbin/mod.tsx | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/modules/trashbin/mod.tsx b/modules/trashbin/mod.tsx index 212bbb6e..819bbe36 100644 --- a/modules/trashbin/mod.tsx +++ b/modules/trashbin/mod.tsx @@ -107,7 +107,7 @@ export default async function (ctx: ModuleRuntimeContext) { const trackName = item.name || item.metadata?.title || "Unknown Track"; const artistName = item.metadata?.artist_name || ""; - const displayName = artistName ? `${trackName} - ${artistName}` : trackName; + const displayName = artistName ? `${artistName} - ${trackName}` : trackName; const { next, added } = toggleEntry(trashSongList, item.uri, displayName); trashSongList = next; @@ -152,7 +152,19 @@ export default async function (ctx: ModuleRuntimeContext) { const uri = uris[0]; const type = client.uri.fromString(uri).type; const isTrack = type === client.uri.Type.TRACK; - const { next, added } = toggleEntry(isTrack ? trashSongList : trashArtistList, uri); + + const curItem = client.player.data?.item; + let displayName = isTrack ? "Track" : "Artist"; + + if (curItem && curItem.uri === uri) { + const trackName = curItem.name || curItem.metadata?.title || "Unknown Track"; + const artistName = curItem.metadata?.artist_name || ""; + displayName = isTrack ? (artistName ? `${artistName} - ${trackName}` : trackName) : artistName; + } else { + displayName = `${isTrack ? "Track" : "Artist"} (${uri.split(":").pop()})`; + } + + const { next, added } = toggleEntry(isTrack ? trashSongList : trashArtistList, uri, displayName); if (isTrack) trashSongList = next; else trashArtistList = next; if (added) { @@ -297,7 +309,15 @@ export default async function (ctx: ModuleRuntimeContext) { }} /> -
+
5 ? "auto" : "hidden", + overflowX: "hidden", + scrollbarGutter: "stable", + paddingRight: "6px", + }} + > {filteredSongs.length === 0 ? (
{searchQuery ? "No matching songs found" : "Trashbin is empty"} From 85ed32bf24e4ac5d22b2cdf7cc18b9e8ca83ca97 Mon Sep 17 00:00:00 2001 From: Mralimoh Date: Sat, 3 Oct 2026 16:00:15 +0330 Subject: [PATCH 7/8] fix(trashbin): show newest trashed songs first --- modules/trashbin/mod.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/modules/trashbin/mod.tsx b/modules/trashbin/mod.tsx index 819bbe36..ce9852d4 100644 --- a/modules/trashbin/mod.tsx +++ b/modules/trashbin/mod.tsx @@ -211,10 +211,12 @@ export default async function (ctx: ModuleRuntimeContext) { }; }, []); - const filteredSongs = Object.entries(songs).filter(([uri, name]) => { - const displayName = typeof name === "string" ? name : uri; - return displayName.toLowerCase().includes(searchQuery.toLowerCase()); - }); + const filteredSongs = Object.entries(songs) + .reverse() + .filter(([uri, name]) => { + const displayName = typeof name === "string" ? name : uri; + return displayName.toLowerCase().includes(searchQuery.toLowerCase()); + }); const removeSong = (uri: string) => { const next = { ...trashSongList }; From 5b3f212c107a7bc9a2c22ea9647e6d3413f3ace9 Mon Sep 17 00:00:00 2001 From: Mralimoh Date: Sat, 3 Oct 2026 16:37:25 +0330 Subject: [PATCH 8/8] fix(trashbin): hide search bar when trashbin is empty --- modules/trashbin/mod.tsx | 90 ++++++++++++++++++++++------------------ 1 file changed, 50 insertions(+), 40 deletions(-) diff --git a/modules/trashbin/mod.tsx b/modules/trashbin/mod.tsx index ce9852d4..0df84e2e 100644 --- a/modules/trashbin/mod.tsx +++ b/modules/trashbin/mod.tsx @@ -293,47 +293,57 @@ export default async function (ctx: ModuleRuntimeContext) {
- setSearchQuery(e.target.value)} - style={{ - width: "100%", - padding: "8px 12px", - marginBottom: "10px", - borderRadius: "4px", - border: "1px solid rgba(255,255,255,0.2)", - background: "rgba(255,255,255,0.05)", - color: "var(--spice-text, #fff)", - boxSizing: "border-box", - outline: "none", - }} - /> - -
5 ? "auto" : "hidden", - overflowX: "hidden", - scrollbarGutter: "stable", - paddingRight: "6px", - }} - > - {filteredSongs.length === 0 ? ( -
- {searchQuery ? "No matching songs found" : "Trashbin is empty"} + {Object.keys(songs).length === 0 ? ( +
+ Trashbin is empty +
+ ) : ( + <> + setSearchQuery(e.target.value)} + style={{ + width: "100%", + padding: "8px 12px", + marginBottom: "10px", + borderRadius: "4px", + border: "1px solid rgba(255,255,255,0.2)", + background: "rgba(255,255,255,0.05)", + color: "var(--spice-text, #fff)", + boxSizing: "border-box", + outline: "none", + }} + /> + +
5 ? "auto" : "hidden", + overflowX: "hidden", + scrollbarGutter: "stable", + paddingRight: "6px", + }} + > + {filteredSongs.length === 0 ? ( +
+ No matching songs found +
+ ) : ( + filteredSongs.map(([uri, name]) => ( + + + + )) + )}
- ) : ( - filteredSongs.map(([uri, name]) => ( - - - - )) - )} -
+ + )}
);