Skip to content

Commit ef7fbf8

Browse files
Merge remote-tracking branch 'origin/main' into codex/twitch-panel-extension
# Conflicts: # src/components/playlist-management-surface.tsx # src/lib/db/repositories.ts # src/lib/i18n/resources/en/bot.json # src/lib/i18n/resources/es/bot.json # src/lib/i18n/resources/fr/bot.json # src/lib/i18n/resources/pt-br/bot.json # src/lib/server/dashboard-settings.ts # src/lib/server/extension-panel.ts # src/lib/server/playlist-management.ts # src/routes/$slug/index.tsx # tests/eventsub.chat-message.test.ts # tests/extension-panel.test.ts
2 parents 4b65f44 + fb4f599 commit ef7fbf8

35 files changed

+738
-246
lines changed

src/components/playlist-management-surface.tsx

Lines changed: 154 additions & 143 deletions
Large diffs are not rendered by default.

src/components/song-search-panel.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ export interface SearchSong {
8888
downloads?: number;
8989
sourceUpdatedAt?: number;
9090
sourceId?: number;
91+
hasLyrics?: boolean;
9192
source: string;
9293
}
9394

@@ -1191,6 +1192,7 @@ export function SongSearchPanel(props: {
11911192
const compactTuning = formatCompactTuningSummary([
11921193
song.tuning,
11931194
]);
1195+
const displaySongPaths = normalizePathOptions(song.parts);
11941196
const tuningTitle =
11951197
compactTuning && song.tuning
11961198
? (() => {
@@ -1202,7 +1204,6 @@ export function SongSearchPanel(props: {
12021204
: undefined;
12031205
})()
12041206
: undefined;
1205-
const displaySongPaths = normalizePathOptions(song.parts);
12061207

12071208
return (
12081209
<div

src/extension/panel/app.tsx

Lines changed: 102 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,11 @@ type PanelViewerRequestSubmitInput =
305305
requestKind: "regular" | "vip";
306306
requestMode: "random" | "choice";
307307
vipTokenCost?: number;
308+
}
309+
| {
310+
requestKind: "regular" | "vip";
311+
requestMode: "favorite";
312+
vipTokenCost?: number;
308313
};
309314

310315
type PanelDropTargetState = {
@@ -1145,10 +1150,14 @@ function ExtensionPanelAppContent(props: {
11451150
requestMode: "catalog",
11461151
requestedPath: input.requestedPath,
11471152
}
1148-
: {
1149-
query: input.query.trim(),
1150-
requestMode: input.requestMode,
1151-
}),
1153+
: "query" in input
1154+
? {
1155+
query: input.query.trim(),
1156+
requestMode: input.requestMode,
1157+
}
1158+
: {
1159+
requestMode: input.requestMode,
1160+
}),
11521161
requestKind: input.requestKind,
11531162
vipTokenCost: input.vipTokenCost,
11541163
itemId: editingRequestItemId ?? undefined,
@@ -2472,7 +2481,10 @@ export function ExtensionPanelModeratorPreview() {
24722481
}
24732482

24742483
async function handleSubmitRequest(input: PanelViewerRequestSubmitInput) {
2475-
const normalizedQuery = "query" in input ? input.query.trim() : null;
2484+
const normalizedQuery =
2485+
"query" in input && typeof input.query === "string"
2486+
? input.query.trim()
2487+
: null;
24762488
const song =
24772489
"songId" in input
24782490
? (searchResults?.items.find(
@@ -2521,10 +2533,14 @@ export function ExtensionPanelModeratorPreview() {
25212533
requestedPath:
25222534
"songId" in input ? input.requestedPath : undefined,
25232535
}
2524-
: {
2525-
query: normalizedQuery ?? "",
2526-
requestMode: input.requestMode,
2527-
}),
2536+
: "query" in input
2537+
? {
2538+
query: normalizedQuery ?? "",
2539+
requestMode: input.requestMode,
2540+
}
2541+
: {
2542+
requestMode: input.requestMode,
2543+
}),
25282544
requestKind: input.requestKind,
25292545
vipTokenCost: input.vipTokenCost,
25302546
replaceExisting: false,
@@ -3039,18 +3055,21 @@ function PanelPlaylistRow(props: {
30393055
(!props.canManagePlaylist && canOpenViewerActions);
30403056
const isActionTrayOpen = props.expandedActionItemId === props.itemId;
30413057
const confirmingRemove = props.confirmingRemoveItemId === props.itemId;
3042-
const itemHasLyrics =
3043-
props.canManagePlaylist &&
3044-
playlistDisplayItemHasLyrics({
3045-
songHasLyrics: props.item.songHasLyrics === true,
3046-
songPartsJson: getString(props.item, "songPartsJson") ?? undefined,
3047-
});
30483058
const canReorder = props.canReorderPlaylist && !isCurrent;
30493059
const isDragging = props.draggingItemId === props.itemId;
30503060
const dropEdge =
30513061
props.dropTargetState?.itemId === props.itemId
30523062
? props.dropTargetState.edge
30533063
: null;
3064+
const itemHasLyrics =
3065+
props.canManagePlaylist &&
3066+
playlistDisplayItemHasLyrics({
3067+
songHasLyrics:
3068+
typeof props.item.songHasLyrics === "boolean"
3069+
? props.item.songHasLyrics
3070+
: null,
3071+
songPartsJson: getString(props.item, "songPartsJson") ?? undefined,
3072+
});
30543073

30553074
useEffect(() => {
30563075
const element = itemRef.current;
@@ -3303,6 +3322,11 @@ function PanelPlaylistRow(props: {
33033322
{getPanelRequestedPathLabel(props.item)}
33043323
</span>
33053324
) : null}
3325+
{itemHasLyrics ? (
3326+
<span className="inline-flex h-5 items-center border border-(--border-strong) bg-(--panel-soft) px-1.5 text-[9px] leading-none font-medium uppercase tracking-[0.12em] text-(--muted)">
3327+
{t("queue.lyrics")}
3328+
</span>
3329+
) : null}
33063330
{(isVipRequest &&
33073331
getPanelStoredVipTokenCost(props.item) > 1) ||
33083332
(!isVipRequest &&
@@ -3626,19 +3650,47 @@ function PanelSpecialRequestControls(props: {
36263650
pendingAction: string | null;
36273651
isEditingRequest: boolean;
36283652
onSubmit: (
3629-
requestMode: "random" | "choice",
3653+
requestMode: "random" | "favorite" | "choice",
36303654
requestKind: "regular" | "vip"
36313655
) => void;
36323656
}) {
36333657
const { t } = useLocaleTranslation("extension");
36343658
const normalizedQuery = props.query.trim();
3635-
const regularDisabledReason = getPanelSpecialRequestDisabledReason({
3659+
const randomDisabledReason = getPanelSpecialRequestDisabledReason({
3660+
query: normalizedQuery,
3661+
requestMode: "random",
3662+
canRequest: props.canRequest,
3663+
t,
3664+
});
3665+
const randomVipDisabledReason = getPanelSpecialRequestDisabledReason({
3666+
query: normalizedQuery,
3667+
requestMode: "random",
3668+
canRequest: props.canVipRequest,
3669+
fallbackReason: props.vipDisabledReason ?? t("vip.insufficient"),
3670+
t,
3671+
});
3672+
const choiceDisabledReason = getPanelSpecialRequestDisabledReason({
3673+
query: normalizedQuery,
3674+
requestMode: "choice",
3675+
canRequest: props.canRequest,
3676+
t,
3677+
});
3678+
const choiceVipDisabledReason = getPanelSpecialRequestDisabledReason({
3679+
query: normalizedQuery,
3680+
requestMode: "choice",
3681+
canRequest: props.canVipRequest,
3682+
fallbackReason: props.vipDisabledReason ?? t("vip.insufficient"),
3683+
t,
3684+
});
3685+
const favoriteDisabledReason = getPanelSpecialRequestDisabledReason({
36363686
query: normalizedQuery,
3687+
requestMode: "favorite",
36373688
canRequest: props.canRequest,
36383689
t,
36393690
});
3640-
const vipDisabledReason = getPanelSpecialRequestDisabledReason({
3691+
const favoriteVipDisabledReason = getPanelSpecialRequestDisabledReason({
36413692
query: normalizedQuery,
3693+
requestMode: "favorite",
36423694
canRequest: props.canVipRequest,
36433695
fallbackReason: props.vipDisabledReason ?? t("vip.insufficient"),
36443696
t,
@@ -3652,8 +3704,8 @@ function PanelSpecialRequestControls(props: {
36523704
<div className="grid gap-2">
36533705
<PanelSpecialRequestRow
36543706
label={t("requests.randomSong")}
3655-
disabledReason={regularDisabledReason}
3656-
vipDisabledReason={vipDisabledReason}
3707+
disabledReason={randomDisabledReason}
3708+
vipDisabledReason={randomVipDisabledReason}
36573709
busy={props.pendingAction != null}
36583710
regularPending={
36593711
props.pendingAction ===
@@ -3675,10 +3727,33 @@ function PanelSpecialRequestControls(props: {
36753727
onRegularClick={() => props.onSubmit("random", "regular")}
36763728
onVipClick={() => props.onSubmit("random", "vip")}
36773729
/>
3730+
<PanelSpecialRequestRow
3731+
label={t("requests.randomFavorite")}
3732+
disabledReason={favoriteDisabledReason}
3733+
vipDisabledReason={favoriteVipDisabledReason}
3734+
busy={props.pendingAction != null}
3735+
regularPending={
3736+
props.pendingAction ===
3737+
getPanelViewerRequestActionKey({
3738+
requestMode: "favorite",
3739+
requestKind: "regular",
3740+
})
3741+
}
3742+
vipPending={
3743+
props.pendingAction ===
3744+
getPanelViewerRequestActionKey({
3745+
requestMode: "favorite",
3746+
requestKind: "vip",
3747+
})
3748+
}
3749+
isEditingRequest={props.isEditingRequest}
3750+
onRegularClick={() => props.onSubmit("favorite", "regular")}
3751+
onVipClick={() => props.onSubmit("favorite", "vip")}
3752+
/>
36783753
<PanelSpecialRequestRow
36793754
label={t("requests.streamerChoice")}
3680-
disabledReason={regularDisabledReason}
3681-
vipDisabledReason={vipDisabledReason}
3755+
disabledReason={choiceDisabledReason}
3756+
vipDisabledReason={choiceVipDisabledReason}
36823757
busy={props.pendingAction != null}
36833758
regularPending={
36843759
props.pendingAction ===
@@ -4892,16 +4967,19 @@ function getPanelViewerRequestActionKey(input: PanelViewerRequestSubmitInput) {
48924967
return `${input.songId}:${input.requestKind}:${input.requestedPath ?? "none"}`;
48934968
}
48944969

4895-
return `special:${input.requestMode}:${input.requestKind}:${input.query.trim().toLowerCase()}`;
4970+
return "query" in input
4971+
? `special:${input.requestMode}:${input.requestKind}:${input.query.trim().toLowerCase()}`
4972+
: `special:${input.requestMode}:${input.requestKind}`;
48964973
}
48974974

48984975
function getPanelSpecialRequestDisabledReason(input: {
48994976
query: string;
4977+
requestMode: "random" | "favorite" | "choice";
49004978
canRequest: boolean;
49014979
fallbackReason?: string;
49024980
t: TFunction;
49034981
}) {
4904-
if (input.query.length < 2) {
4982+
if (input.requestMode !== "favorite" && input.query.length < 2) {
49054983
return input.t("search.typeAtLeastTwo");
49064984
}
49074985

src/extension/panel/demo.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ export function applyDemoViewerRequestMutation(input: {
107107
viewerProfile: PanelDemoViewerProfile;
108108
song?: Record<string, unknown>;
109109
query?: string;
110-
requestMode?: "catalog" | "random" | "choice";
110+
requestMode?: "catalog" | "random" | "favorite" | "choice";
111111
requestKind: "regular" | "vip";
112112
requestedPath?: string;
113113
vipTokenCost?: number;
@@ -122,7 +122,9 @@ export function applyDemoViewerRequestMutation(input: {
122122
const songId =
123123
requestMode === "choice"
124124
? `preview-choice-${now}`
125-
: (getString(songRecord, "id") ?? `preview-song-${now}`);
125+
: requestMode === "favorite" && !getString(songRecord, "id")
126+
? `preview-favorite-${now}`
127+
: (getString(songRecord, "id") ?? `preview-song-${now}`);
126128
const nextId = input.nextId ?? `preview-request-${songId}-${now}`;
127129
const replaceTarget =
128130
input.replaceItemId != null
@@ -154,7 +156,10 @@ export function applyDemoViewerRequestMutation(input: {
154156
songTitle:
155157
requestMode === "choice"
156158
? "Streamer choice"
157-
: (getString(songRecord, "title") ?? "Unknown song"),
159+
: requestMode === "favorite" &&
160+
!getString(songRecord, "title")
161+
? "Random favorite"
162+
: (getString(songRecord, "title") ?? "Unknown song"),
158163
songArtist:
159164
requestMode === "choice"
160165
? null
@@ -230,7 +235,9 @@ export function applyDemoViewerRequestMutation(input: {
230235
songTitle:
231236
requestMode === "choice"
232237
? "Streamer choice"
233-
: (getString(songRecord, "title") ?? "Unknown song"),
238+
: requestMode === "favorite" && !getString(songRecord, "title")
239+
? "Random favorite"
240+
: (getString(songRecord, "title") ?? "Unknown song"),
234241
songArtist:
235242
requestMode === "choice" ? null : getString(songRecord, "artist"),
236243
songAlbum: requestMode === "choice" ? null : getString(songRecord, "album"),

0 commit comments

Comments
 (0)