@lumiastream/ui 0.2.8-alpha.1 → 0.2.8-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +3 -6
- package/dist/index.js +916 -136
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -485,7 +485,7 @@ declare const resolvePlatformChatterProfileUrl: ({ platform, username, displayna
|
|
|
485
485
|
declare const normalizeHttpUrl: (value: unknown) => string | null;
|
|
486
486
|
declare const resolveChatterProfileUrlWithResolvers: ({ platform, username, displayname, userId, extraSettings, resolvers, }: ResolveChatterProfileUrlWithResolversInput) => Promise<string | null>;
|
|
487
487
|
|
|
488
|
-
type ModuleType = 'alert' | 'emotebox' | 'emotealert' | 'text' | 'image' | 'audio' | 'video' | 'camera' | 'slideshow' | 'shape' | 'svg' | 'colorbox' | 'goal' | 'timer' | 'gradientlight' | 'browsersource' | 'chatbox' | 'credits' | 'eventlist' | 'eventrotate' | 'hfx' | 'hfxalert' | 'clip' | 'youtube' | 'spinwheel' | 'raffle' | 'viewerqueue' | 'loyaltyleaderboard' | 'viewerprofiles' | 'achievements' | 'poll' | 'tournaments' | 'spotify' | 'nowplaying' | 'youtubemusic' | 'brbscreen' | 'vlc' | 'tts' | 'livecaption' | 'custom' | 'group';
|
|
488
|
+
type ModuleType = 'alert' | 'emotebox' | 'emotealert' | 'text' | 'image' | 'audio' | 'video' | 'camera' | 'slideshow' | 'shape' | 'svg' | 'colorbox' | 'goal' | 'timer' | 'gradientlight' | 'browsersource' | 'chatbox' | 'credits' | 'eventlist' | 'eventrotate' | 'hfx' | 'hfxalert' | 'clip' | 'youtube' | 'spinwheel' | 'raffle' | 'viewerqueue' | 'loyaltyleaderboard' | 'viewerprofiles' | 'achievements' | 'poll' | 'tournaments' | 'spotify' | 'nowplaying' | 'youtubemusic' | 'brbscreen' | 'vlc' | 'tts' | 'livecaption' | 'custom' | 'tipjar' | 'hypetrain' | 'streamboss' | 'songrequest' | 'group';
|
|
489
489
|
type OverlayLayerType = 'layer' | 'group';
|
|
490
490
|
type OverlayLayerBounds = {
|
|
491
491
|
height: number;
|
|
@@ -689,11 +689,8 @@ declare const SE_WIDGET_TO_MARKETPLACE_CANDIDATES: Record<string, number[]>;
|
|
|
689
689
|
declare function getMarketplaceCandidates(seWidgetType: string): number[];
|
|
690
690
|
declare function hasMarketplaceCandidates(seWidgetType: string): boolean;
|
|
691
691
|
|
|
692
|
-
declare const SE_IMPORT_FLAGS: {
|
|
693
|
-
|
|
694
|
-
readonly IMPORT_SEASONAL: false;
|
|
695
|
-
};
|
|
696
|
-
declare function isWidgetFlaggedOff(seType: string): boolean;
|
|
692
|
+
declare const SE_IMPORT_FLAGS: {};
|
|
693
|
+
declare function isWidgetFlaggedOff(_seType: string): boolean;
|
|
697
694
|
declare const FLAG_OFF_REASONS: Record<string, string>;
|
|
698
695
|
|
|
699
696
|
interface SEAccountClaims {
|
package/dist/index.js
CHANGED
|
@@ -3126,7 +3126,11 @@ var SE_WIDGET_TYPE_LABELS = {
|
|
|
3126
3126
|
"se-widget-hype-cup": "Hype Cup",
|
|
3127
3127
|
"se-widget-custom-event-list": "Custom widget",
|
|
3128
3128
|
"se-widget-credit-roll": "Credits",
|
|
3129
|
-
"se-widget-media-share": "Media share"
|
|
3129
|
+
"se-widget-media-share": "Media share",
|
|
3130
|
+
// Imports as an empty slideshow — Lumia has no Fourthwall catalog API, so
|
|
3131
|
+
// the streamer has to add product images by hand. The friendly title makes
|
|
3132
|
+
// that obvious in the layers panel; see REVIEW_REASONS for the full prompt.
|
|
3133
|
+
"se-widget-merch-products-rotator": "Merch products (add images manually)"
|
|
3130
3134
|
};
|
|
3131
3135
|
function defaultLabelForSEWidget(widget) {
|
|
3132
3136
|
const trimmed = typeof widget.name === "string" ? widget.name.trim() : "";
|
|
@@ -3273,7 +3277,22 @@ var LUMIA_DEFAULT_SIZES = {
|
|
|
3273
3277
|
emotebox: { width: 400, height: 400 },
|
|
3274
3278
|
nowplaying: { width: 600, height: 250 },
|
|
3275
3279
|
spotify: { width: 600, height: 250 },
|
|
3276
|
-
youtubemusic: { width: 600, height: 250 }
|
|
3280
|
+
youtubemusic: { width: 600, height: 250 },
|
|
3281
|
+
tipjar: { width: 600, height: 700 },
|
|
3282
|
+
// Hype Train: wide-and-short by default to fit a horizontal progress bar.
|
|
3283
|
+
// All four built-in themes (subtle / train / rocket / inferno) render
|
|
3284
|
+
// fine at this aspect — rocket internally uses a vertical gauge but
|
|
3285
|
+
// still works at this footprint. Users can resize per their overlay.
|
|
3286
|
+
hypetrain: { width: 740, height: 140 },
|
|
3287
|
+
// Stream Boss: matches SE's bit-boss default (440×86) for a 1:1 import
|
|
3288
|
+
// experience, lightly padded so Lumia's Card-theme avatar fits without
|
|
3289
|
+
// resizing. Users can swap to Bar-Only and shrink height further.
|
|
3290
|
+
streamboss: { width: 480, height: 100 },
|
|
3291
|
+
// Song Request: SE's media-share default is 480×292. Lumia's now-playing
|
|
3292
|
+
// card + 5-item queue list needs a bit more vertical room, so we add ~80px
|
|
3293
|
+
// for the queue list. Users can hide the queue list and shrink back to
|
|
3294
|
+
// just the card if they want SE's exact footprint.
|
|
3295
|
+
songrequest: { width: 480, height: 380 }
|
|
3277
3296
|
};
|
|
3278
3297
|
function translateVariationCondition(seType, seCondition, requirement) {
|
|
3279
3298
|
if (seType === "gift") return { conditionType: "IS_GIFT", condition: 1 };
|
|
@@ -3370,6 +3389,17 @@ function mapVideo(widget) {
|
|
|
3370
3389
|
}
|
|
3371
3390
|
});
|
|
3372
3391
|
}
|
|
3392
|
+
var LUMIA_SNOW_WEBM = "https://storage.lumiastream.com/overlays/global/seasonal/snow.webm";
|
|
3393
|
+
function mapSnow(widget) {
|
|
3394
|
+
return buildUnit(widget, "video", {
|
|
3395
|
+
content: {
|
|
3396
|
+
src: LUMIA_SNOW_WEBM,
|
|
3397
|
+
volume: 0,
|
|
3398
|
+
loop: true,
|
|
3399
|
+
muted: true
|
|
3400
|
+
}
|
|
3401
|
+
});
|
|
3402
|
+
}
|
|
3373
3403
|
function mapReadout(widget, fallbackVar) {
|
|
3374
3404
|
const explicitListener = widget.variables?.listener;
|
|
3375
3405
|
const explicit = typeof explicitListener === "string" ? explicitListener : null;
|
|
@@ -3539,6 +3569,9 @@ function currentForListener(listener) {
|
|
|
3539
3569
|
if (listener.startsWith("subscriber-")) return "{{twitch_total_subscriber_count}}";
|
|
3540
3570
|
if (listener.startsWith("cheer-")) return "{{twitch_total_bits_count}}";
|
|
3541
3571
|
if (listener.startsWith("tip-")) return "{{total_donation_amount}}";
|
|
3572
|
+
if (listener === "merch-goal-orders") return "{{fourthwall_total_order_count}}";
|
|
3573
|
+
if (listener === "merch-goal-items") return "{{fourthwall_total_items_count}}";
|
|
3574
|
+
if (listener === "merch-goal-total") return "{{fourthwall_total_order_amount}}";
|
|
3542
3575
|
if (listener.startsWith("merch-")) return "{{fourthwall_total_order_count}}";
|
|
3543
3576
|
return "0";
|
|
3544
3577
|
}
|
|
@@ -3639,77 +3672,6 @@ function mapCustom(widget) {
|
|
|
3639
3672
|
}
|
|
3640
3673
|
});
|
|
3641
3674
|
}
|
|
3642
|
-
var SEASONAL_TEMPLATE_HTML = '<canvas id="particles"></canvas>';
|
|
3643
|
-
var SEASONAL_TEMPLATE_CSS = `html, body { margin: 0; padding: 0; width: 100%; height: 100%; background: transparent; overflow: hidden; }
|
|
3644
|
-
#particles { position: fixed; inset: 0; width: 100%; height: 100%; pointer-events: none; }`;
|
|
3645
|
-
var SEASONAL_TEMPLATE_JS = `const SEASONS = {
|
|
3646
|
-
snow: { emoji: '\\u2744', count: 80, sizeMin: 14, sizeMax: 28, gravity: 1.0, drift: 0.6, sway: 1.2 },
|
|
3647
|
-
halloween: { emoji: '\\ud83d\\udc7b', count: 25, sizeMin: 28, sizeMax: 44, gravity: 0.6, drift: 0.4, sway: 0.8 },
|
|
3648
|
-
xmas: { emoji: '\\u2744', count: 120, sizeMin: 16, sizeMax: 32, gravity: 1.1, drift: 0.7, sway: 1.3 },
|
|
3649
|
-
valentine: { emoji: '\\u2764\\ufe0f', count: 50, sizeMin: 18, sizeMax: 34, gravity: 0.8, drift: 0.5, sway: 1.0 },
|
|
3650
|
-
easter: { emoji: '\\ud83e\\udd5a', count: 30, sizeMin: 22, sizeMax: 38, gravity: 0.9, drift: 0.3, sway: 0.4 },
|
|
3651
|
-
worldcup: { emoji: '\\u26bd', count: 25, sizeMin: 24, sizeMax: 40, gravity: 1.1, drift: 0.4, sway: 0.5 },
|
|
3652
|
-
};
|
|
3653
|
-
const cfg = SEASONS[(window.DATA && window.DATA.season) || 'snow'] || SEASONS.snow;
|
|
3654
|
-
const canvas = document.getElementById('particles');
|
|
3655
|
-
const ctx = canvas.getContext('2d');
|
|
3656
|
-
const dpr = window.devicePixelRatio || 1;
|
|
3657
|
-
function resize() {
|
|
3658
|
-
canvas.width = window.innerWidth * dpr;
|
|
3659
|
-
canvas.height = window.innerHeight * dpr;
|
|
3660
|
-
canvas.style.width = window.innerWidth + 'px';
|
|
3661
|
-
canvas.style.height = window.innerHeight + 'px';
|
|
3662
|
-
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
3663
|
-
}
|
|
3664
|
-
resize();
|
|
3665
|
-
window.addEventListener('resize', resize);
|
|
3666
|
-
const particles = [];
|
|
3667
|
-
for (let i = 0; i < cfg.count; i++) {
|
|
3668
|
-
particles.push({
|
|
3669
|
-
x: Math.random() * window.innerWidth,
|
|
3670
|
-
y: Math.random() * window.innerHeight - window.innerHeight,
|
|
3671
|
-
size: cfg.sizeMin + Math.random() * (cfg.sizeMax - cfg.sizeMin),
|
|
3672
|
-
speed: 0.5 + Math.random() * 1.5,
|
|
3673
|
-
phase: Math.random() * Math.PI * 2,
|
|
3674
|
-
});
|
|
3675
|
-
}
|
|
3676
|
-
function tick(now) {
|
|
3677
|
-
ctx.clearRect(0, 0, window.innerWidth, window.innerHeight);
|
|
3678
|
-
const t = now / 1000;
|
|
3679
|
-
for (const p of particles) {
|
|
3680
|
-
p.y += p.speed * cfg.gravity;
|
|
3681
|
-
const sway = Math.sin(t + p.phase) * cfg.sway;
|
|
3682
|
-
if (p.y > window.innerHeight + 40) {
|
|
3683
|
-
p.y = -40;
|
|
3684
|
-
p.x = Math.random() * window.innerWidth;
|
|
3685
|
-
}
|
|
3686
|
-
ctx.font = p.size + 'px serif';
|
|
3687
|
-
ctx.fillText(cfg.emoji, p.x + sway * 20, p.y);
|
|
3688
|
-
}
|
|
3689
|
-
requestAnimationFrame(tick);
|
|
3690
|
-
}
|
|
3691
|
-
requestAnimationFrame(tick);`;
|
|
3692
|
-
function mapSeasonal(widget, season) {
|
|
3693
|
-
return buildUnit(widget, "custom", {
|
|
3694
|
-
content: {
|
|
3695
|
-
codeId: `seasonal-${season}-${widget.id}`,
|
|
3696
|
-
html: SEASONAL_TEMPLATE_HTML,
|
|
3697
|
-
css: SEASONAL_TEMPLATE_CSS,
|
|
3698
|
-
js: SEASONAL_TEMPLATE_JS,
|
|
3699
|
-
configs: [
|
|
3700
|
-
{
|
|
3701
|
-
key: "season",
|
|
3702
|
-
label: "Season",
|
|
3703
|
-
type: "dropdown",
|
|
3704
|
-
value: season,
|
|
3705
|
-
options: { snow: "Snow", halloween: "Halloween", xmas: "Christmas", valentine: "Valentine's", easter: "Easter", worldcup: "World Cup" }
|
|
3706
|
-
}
|
|
3707
|
-
],
|
|
3708
|
-
data: { season },
|
|
3709
|
-
flavor: "lumia-seasonal"
|
|
3710
|
-
}
|
|
3711
|
-
});
|
|
3712
|
-
}
|
|
3713
3675
|
|
|
3714
3676
|
// src/se-import/mappers/misc.ts
|
|
3715
3677
|
function mapChatbox(widget) {
|
|
@@ -4012,6 +3974,49 @@ function mapNowPlaying(widget) {
|
|
|
4012
3974
|
}
|
|
4013
3975
|
});
|
|
4014
3976
|
}
|
|
3977
|
+
function mapMediaShare(widget) {
|
|
3978
|
+
const v = widget.variables ?? {};
|
|
3979
|
+
return buildUnit(widget, "songrequest", {
|
|
3980
|
+
content: {
|
|
3981
|
+
version: 1,
|
|
3982
|
+
// SE always shows the now-playing card; honour its cosmetic toggles
|
|
3983
|
+
// for the rest. `showTitle` defaults true; the others default to
|
|
3984
|
+
// SE's documented behavior (tipper visible, stats hidden).
|
|
3985
|
+
showNowPlayingCard: true,
|
|
3986
|
+
showQueueList: true,
|
|
3987
|
+
showSkipButton: true,
|
|
3988
|
+
showPauseButton: true,
|
|
3989
|
+
showTitle: v.showTitle !== false,
|
|
3990
|
+
showTipper: v.showTipper !== false,
|
|
3991
|
+
showAmount: v.showAmount === true,
|
|
3992
|
+
showStats: v.showStats === true,
|
|
3993
|
+
// SE doesn't have a queue-size knob — pick a sensible default
|
|
3994
|
+
// matching what fits in the SE widget's ~292px tall footprint.
|
|
3995
|
+
maxQueueItemsVisible: 5,
|
|
3996
|
+
// SE color knobs translate directly. Fall back to Lumia's defaults
|
|
3997
|
+
// (purple accent, near-black card background) if SE values are
|
|
3998
|
+
// missing — matches what the module ships with for native creates.
|
|
3999
|
+
accentColor: v.accentColor ?? "#9146ff",
|
|
4000
|
+
primaryColor: v.primaryColor ?? "#1a1a1a",
|
|
4001
|
+
// SE's widget hides the YT player by default; mirror that for
|
|
4002
|
+
// audio-only overlay use.
|
|
4003
|
+
playerVisible: false,
|
|
4004
|
+
defaultVolume: 70,
|
|
4005
|
+
autoAdvanceOnEnd: true,
|
|
4006
|
+
// SE-only knob preserved for round-trip provenance; Lumia ignores
|
|
4007
|
+
// it (LumiaStream auto-advance fires on `durationSeconds + grace`).
|
|
4008
|
+
se_timeLeft: v.timeLeft === true
|
|
4009
|
+
},
|
|
4010
|
+
css: {
|
|
4011
|
+
fontFamily: "Roboto",
|
|
4012
|
+
fontWeight: "bold",
|
|
4013
|
+
color: "#ffffff",
|
|
4014
|
+
textShadow: "rgb(0, 0, 0) 1px 1px 1px",
|
|
4015
|
+
background: "transparent",
|
|
4016
|
+
fontSize: 18
|
|
4017
|
+
}
|
|
4018
|
+
});
|
|
4019
|
+
}
|
|
4015
4020
|
var SE_KAPPAGEN_EVENT_TO_LUMIA_ALERT = {
|
|
4016
4021
|
follower: "twitch-follower",
|
|
4017
4022
|
subscriber: "twitch-subscriber",
|
|
@@ -4155,6 +4160,75 @@ function mapGiveaway(widget) {
|
|
|
4155
4160
|
}
|
|
4156
4161
|
});
|
|
4157
4162
|
}
|
|
4163
|
+
var HYPETRAIN_LISTENER_KEYS = /* @__PURE__ */ new Set([
|
|
4164
|
+
"hypetrain-latest",
|
|
4165
|
+
"hypetrain-level-progress",
|
|
4166
|
+
"hypetrain-level-goal",
|
|
4167
|
+
"hypetrain-total",
|
|
4168
|
+
"hypetrain-latest-top-contributors",
|
|
4169
|
+
// SE alert-name variants some community widgets subscribe to as well.
|
|
4170
|
+
"hypetrain-start",
|
|
4171
|
+
"hypetrain-end",
|
|
4172
|
+
"hypetrain-cooldown-start",
|
|
4173
|
+
"hypetrain-cooldown-end"
|
|
4174
|
+
]);
|
|
4175
|
+
function isHypetrainCustomWidget(widget) {
|
|
4176
|
+
const enabled = [];
|
|
4177
|
+
const listeners = widget.listeners;
|
|
4178
|
+
if (listeners && typeof listeners === "object" && !Array.isArray(listeners)) {
|
|
4179
|
+
for (const [k, v] of Object.entries(listeners)) {
|
|
4180
|
+
if (v) enabled.push(k);
|
|
4181
|
+
}
|
|
4182
|
+
} else if (Array.isArray(listeners)) {
|
|
4183
|
+
enabled.push(...listeners);
|
|
4184
|
+
}
|
|
4185
|
+
const explicit = widget.listener;
|
|
4186
|
+
if (typeof explicit === "string" && explicit && !enabled.includes(explicit)) {
|
|
4187
|
+
enabled.push(explicit);
|
|
4188
|
+
}
|
|
4189
|
+
if (enabled.length === 0) return false;
|
|
4190
|
+
return enabled.every((listener) => HYPETRAIN_LISTENER_KEYS.has(listener));
|
|
4191
|
+
}
|
|
4192
|
+
function mapHypetrain(widget) {
|
|
4193
|
+
const v = widget.variables ?? {};
|
|
4194
|
+
return buildUnit(widget, "hypetrain", {
|
|
4195
|
+
content: {
|
|
4196
|
+
// Twitch-native is the right default — SE hype-train widgets always
|
|
4197
|
+
// reflected the Twitch native train. Users can flip to 'synthetic'
|
|
4198
|
+
// or 'hybrid' from Settings if they want cross-platform pooling.
|
|
4199
|
+
sourceMode: "twitch-native",
|
|
4200
|
+
sources: null,
|
|
4201
|
+
// default money-only whitelist; ignored in twitch-native anyway
|
|
4202
|
+
customValues: {},
|
|
4203
|
+
pretrainThreshold: 5,
|
|
4204
|
+
pretrainDurationMs: 5 * 60 * 1e3,
|
|
4205
|
+
levelDurationMs: 5 * 60 * 1e3,
|
|
4206
|
+
levelStep: 5,
|
|
4207
|
+
levelThresholds: [],
|
|
4208
|
+
// Pick a moderately energetic default theme — SE hype-train widgets
|
|
4209
|
+
// were never subtle. Users can switch in Settings.
|
|
4210
|
+
theme: "train",
|
|
4211
|
+
primaryColor: typeof v.primaryColor === "string" ? v.primaryColor : "#ff6b00",
|
|
4212
|
+
accentColor: typeof v.accentColor === "string" ? v.accentColor : "#ffd84d",
|
|
4213
|
+
textColor: typeof v.textColor === "string" ? v.textColor : "#ffffff",
|
|
4214
|
+
showLevel: true,
|
|
4215
|
+
showTotal: true,
|
|
4216
|
+
showTimer: true,
|
|
4217
|
+
showTopContributor: true,
|
|
4218
|
+
showLeaderboard: true,
|
|
4219
|
+
leaderboardLimit: 5,
|
|
4220
|
+
leaderboardDurationMs: 30 * 1e3,
|
|
4221
|
+
levelUpSoundUrl: "https://storage.lumiastream.com/overlays/lumia/audio/crowdClap.mp3",
|
|
4222
|
+
trainEndSoundUrl: "https://storage.lumiastream.com/overlays/lumia/audio/crowdClap.mp3",
|
|
4223
|
+
audioVolume: 0.5,
|
|
4224
|
+
// SE config preserved verbatim as provenance — no Lumia equivalent for
|
|
4225
|
+
// most of these knobs (per-widget animation tunables, sound URLs the
|
|
4226
|
+
// streamer might want to migrate manually, etc.). Users can inspect
|
|
4227
|
+
// these via the overlay JSON if they want to recreate a specific look.
|
|
4228
|
+
se_variables: v
|
|
4229
|
+
}
|
|
4230
|
+
});
|
|
4231
|
+
}
|
|
4158
4232
|
function mapUnsupportedAsText(widget) {
|
|
4159
4233
|
return buildUnit(widget, "text", {
|
|
4160
4234
|
content: {
|
|
@@ -4172,44 +4246,258 @@ function mapUnsupportedAsText(widget) {
|
|
|
4172
4246
|
});
|
|
4173
4247
|
}
|
|
4174
4248
|
|
|
4249
|
+
// src/se-import/mappers/hypecup.ts
|
|
4250
|
+
var SE_TYPE_TO_CATEGORY = {
|
|
4251
|
+
cheer: "bits",
|
|
4252
|
+
sub: "subs",
|
|
4253
|
+
follower: "follows",
|
|
4254
|
+
raid: "raids",
|
|
4255
|
+
tip: "tips"
|
|
4256
|
+
};
|
|
4257
|
+
function liftSESpritesIntoTiers(types) {
|
|
4258
|
+
const out = {};
|
|
4259
|
+
if (!types) return out;
|
|
4260
|
+
for (const [seKey, block] of Object.entries(types)) {
|
|
4261
|
+
const cat = SE_TYPE_TO_CATEGORY[seKey];
|
|
4262
|
+
if (!cat) continue;
|
|
4263
|
+
const variations = block?.variations ?? [];
|
|
4264
|
+
const tiers = variations.filter((v) => typeof v?.src === "string" && v.src.length > 0).map((v) => ({
|
|
4265
|
+
minAmount: typeof v.amount === "number" && v.amount > 0 ? v.amount : 1,
|
|
4266
|
+
imageUrl: v.src,
|
|
4267
|
+
scale: typeof v.cheerSize === "number" && v.cheerSize > 0 ? v.cheerSize : 1
|
|
4268
|
+
}));
|
|
4269
|
+
if (tiers.length > 0) out[cat] = tiers;
|
|
4270
|
+
}
|
|
4271
|
+
return out;
|
|
4272
|
+
}
|
|
4273
|
+
var LISTENER_TO_CATEGORY = {
|
|
4274
|
+
"follower-latest": "follows",
|
|
4275
|
+
"subscriber-latest": "subs",
|
|
4276
|
+
"tip-latest": "tips",
|
|
4277
|
+
"cheer-latest": "bits",
|
|
4278
|
+
"raid-latest": "raids"
|
|
4279
|
+
};
|
|
4280
|
+
function categoriesFromListeners(listeners) {
|
|
4281
|
+
const events = {
|
|
4282
|
+
tips: false,
|
|
4283
|
+
bits: false,
|
|
4284
|
+
subs: false,
|
|
4285
|
+
resubs: false,
|
|
4286
|
+
gifts: false,
|
|
4287
|
+
superchats: false,
|
|
4288
|
+
superstickers: false,
|
|
4289
|
+
kicks: false,
|
|
4290
|
+
charity: false,
|
|
4291
|
+
follows: false,
|
|
4292
|
+
raids: false,
|
|
4293
|
+
redemptions: false,
|
|
4294
|
+
chat: false
|
|
4295
|
+
};
|
|
4296
|
+
if (!listeners) return events;
|
|
4297
|
+
const onListeners = Array.isArray(listeners) ? listeners : Object.entries(listeners).filter(([, on]) => on === true).map(([k]) => k);
|
|
4298
|
+
for (const l of onListeners) {
|
|
4299
|
+
const cat = LISTENER_TO_CATEGORY[l];
|
|
4300
|
+
if (cat) events[cat] = true;
|
|
4301
|
+
}
|
|
4302
|
+
if (events.subs) events.resubs = true;
|
|
4303
|
+
return events;
|
|
4304
|
+
}
|
|
4305
|
+
function mapHypeCup(widget) {
|
|
4306
|
+
const v = widget.variables ?? {};
|
|
4307
|
+
const cupX = typeof v.cupPosition?.x === "number" ? clamp01(v.cupPosition.x) : 0.5;
|
|
4308
|
+
const cupY = typeof v.cupPosition?.y === "number" ? clamp01(v.cupPosition.y) : 0.7;
|
|
4309
|
+
const bounce = typeof v.bounce === "number" ? Math.min(1, Math.max(0, v.bounce)) : 0.3;
|
|
4310
|
+
const maxBodies = typeof v.recentEvents === "number" && v.recentEvents > 0 ? Math.min(300, Math.max(10, v.recentEvents)) : 80;
|
|
4311
|
+
const duration = typeof v.duration === "number" && v.duration > 0 ? Math.min(15, Math.max(1, v.duration)) : 3;
|
|
4312
|
+
const sprites = liftSESpritesIntoTiers(v.types);
|
|
4313
|
+
return buildUnit(widget, "tipjar", {
|
|
4314
|
+
content: {
|
|
4315
|
+
version: 1,
|
|
4316
|
+
// Phase 1 ships a single preset; SE's per-cup polygon set isn't lifted
|
|
4317
|
+
// yet (it'd require multi-cup support which we deliberately deferred).
|
|
4318
|
+
// The streamer can swap a custom cup image + wireframe via Settings.
|
|
4319
|
+
preset: "glass-mug",
|
|
4320
|
+
customCup: null,
|
|
4321
|
+
cup: {
|
|
4322
|
+
x: cupX,
|
|
4323
|
+
y: cupY,
|
|
4324
|
+
scale: 1,
|
|
4325
|
+
opacity: 1
|
|
4326
|
+
},
|
|
4327
|
+
physics: {
|
|
4328
|
+
gravity: 1,
|
|
4329
|
+
bounce,
|
|
4330
|
+
friction: 0.06,
|
|
4331
|
+
maxBodies
|
|
4332
|
+
},
|
|
4333
|
+
tokens: {
|
|
4334
|
+
defaultRadius: 26
|
|
4335
|
+
},
|
|
4336
|
+
events: categoriesFromListeners(widget.listeners),
|
|
4337
|
+
chat: {
|
|
4338
|
+
useViewerAvatars: true
|
|
4339
|
+
},
|
|
4340
|
+
display: {
|
|
4341
|
+
showWireframes: !!v.showWireframes,
|
|
4342
|
+
showEventMessages: !!v.textEnabled,
|
|
4343
|
+
messageDuration: duration
|
|
4344
|
+
},
|
|
4345
|
+
sprites,
|
|
4346
|
+
// Phase 2 defaults for goal/break — disabled by import. Streamer
|
|
4347
|
+
// can enable in Settings; SE has no equivalent so nothing to lift.
|
|
4348
|
+
goal: { active: false, currency: "tips", target: 100, current: 0, readoutTemplate: "{{current}} / {{target}}", textColor: "#ffffff", hitAt: null },
|
|
4349
|
+
break: { enabled: false, command: "!breakjar", rebuildDelay: 5e3 },
|
|
4350
|
+
// SE-only knobs that don't have a Lumia equivalent — preserved as
|
|
4351
|
+
// provenance for future phases (per-cup polygons + enter/movement
|
|
4352
|
+
// physics tweaks). `se_types` removed — now consumed into sprites above.
|
|
4353
|
+
se_enter_from: v.enterFrom ?? null,
|
|
4354
|
+
se_prevent_movement: !!v.preventMovement,
|
|
4355
|
+
se_fullscreen: !!v.fullscreen,
|
|
4356
|
+
se_cups: v.cups ?? null
|
|
4357
|
+
},
|
|
4358
|
+
css: {
|
|
4359
|
+
background: "transparent"
|
|
4360
|
+
}
|
|
4361
|
+
});
|
|
4362
|
+
}
|
|
4363
|
+
function clamp01(n) {
|
|
4364
|
+
if (!Number.isFinite(n)) return 0.5;
|
|
4365
|
+
return Math.min(1, Math.max(0, n));
|
|
4366
|
+
}
|
|
4367
|
+
|
|
4368
|
+
// src/se-import/mappers/streamboss.ts
|
|
4369
|
+
var SE_LISTENER_TO_ALERT = {
|
|
4370
|
+
"follower-latest": "twitch-follower",
|
|
4371
|
+
"subscriber-latest": "twitch-subscriber",
|
|
4372
|
+
"tip-latest": "lumiastream-donation",
|
|
4373
|
+
"cheer-latest": "twitch-bits",
|
|
4374
|
+
"raid-latest": "twitch-raid"
|
|
4375
|
+
// `purchase-latest` and `subscriber-gifted-latest` are intentionally absent.
|
|
4376
|
+
// SE rolled gifted subs into `subscriber-latest`; Lumia splits them via
|
|
4377
|
+
// extraSettings.isGift, which the runtime already handles consistently.
|
|
4378
|
+
};
|
|
4379
|
+
var SE_LISTENER_MODE = {
|
|
4380
|
+
"follower-latest": "flat",
|
|
4381
|
+
"subscriber-latest": "flat",
|
|
4382
|
+
"tip-latest": "multiplier",
|
|
4383
|
+
// 100 means "100 dmg per $"
|
|
4384
|
+
"cheer-latest": "multiplier",
|
|
4385
|
+
// 1 means "1 dmg per bit"
|
|
4386
|
+
"raid-latest": "multiplier"
|
|
4387
|
+
// 10 means "10 dmg per viewer"
|
|
4388
|
+
};
|
|
4389
|
+
function listenerIsOn(listeners, key) {
|
|
4390
|
+
if (!listeners) return false;
|
|
4391
|
+
if (Array.isArray(listeners)) return listeners.includes(key);
|
|
4392
|
+
return listeners[key] === true;
|
|
4393
|
+
}
|
|
4394
|
+
function mapStreamBoss(widget) {
|
|
4395
|
+
const v = widget.variables ?? {};
|
|
4396
|
+
const baseHp = typeof v.baseHP === "number" && v.baseHP > 0 ? Math.round(v.baseHP) : 1e3;
|
|
4397
|
+
const seDamage = v.damage ?? {};
|
|
4398
|
+
const damageMap = {};
|
|
4399
|
+
const selectedEvents = [];
|
|
4400
|
+
for (const [seListener, alertKey] of Object.entries(SE_LISTENER_TO_ALERT)) {
|
|
4401
|
+
if (!listenerIsOn(widget.listeners, seListener)) continue;
|
|
4402
|
+
const raw = seDamage[seListener];
|
|
4403
|
+
const value = typeof raw === "number" && Number.isFinite(raw) && raw > 0 ? raw : null;
|
|
4404
|
+
if (value === null) continue;
|
|
4405
|
+
damageMap[alertKey] = { value, mode: SE_LISTENER_MODE[seListener] };
|
|
4406
|
+
selectedEvents.push(alertKey);
|
|
4407
|
+
}
|
|
4408
|
+
const barColor = typeof v.colors?.frame === "string" && v.colors.frame ? v.colors.frame : "#2bff00";
|
|
4409
|
+
return buildUnit(widget, "streamboss", {
|
|
4410
|
+
content: {
|
|
4411
|
+
version: 1,
|
|
4412
|
+
// Theme: SE bit-boss with `showImage: true` reads as the Card layout
|
|
4413
|
+
// (avatar + name + bar). When images are hidden it's effectively
|
|
4414
|
+
// our Bar-Only theme.
|
|
4415
|
+
theme: v.showImage === false ? "bar-only" : "card",
|
|
4416
|
+
initialBossName: "Stream Boss",
|
|
4417
|
+
// SE has no stage cycling — every takeover resets to baseHP. We
|
|
4418
|
+
// honor that by shipping a single-element healthStages array; the
|
|
4419
|
+
// runtime cycles modulo length so a one-element array stays put.
|
|
4420
|
+
healthStages: [baseHp],
|
|
4421
|
+
currentBoss: "Stream Boss",
|
|
4422
|
+
currentBossAvatar: "",
|
|
4423
|
+
currentHP: baseHp,
|
|
4424
|
+
maxHP: baseHp,
|
|
4425
|
+
currentStageIndex: 0,
|
|
4426
|
+
damageMap,
|
|
4427
|
+
// Concrete selectedEvents means "user has opted in to exactly these
|
|
4428
|
+
// events". Empty arrays would disable everything — only happens if
|
|
4429
|
+
// SE had all listeners off, which mirrors what the imported overlay
|
|
4430
|
+
// should do.
|
|
4431
|
+
selectedEvents,
|
|
4432
|
+
bossHealsOnEventTrigger: v.selfHeal !== false,
|
|
4433
|
+
newBossCommand: "!newboss",
|
|
4434
|
+
eventQueueDelayMs: 3e3,
|
|
4435
|
+
defeatedDelayMs: 3e3,
|
|
4436
|
+
showAvatar: v.showImage !== false,
|
|
4437
|
+
showFloatingNumbers: true,
|
|
4438
|
+
showEventMessages: v.showText !== false,
|
|
4439
|
+
barColor,
|
|
4440
|
+
barBackgroundColor: "#333333",
|
|
4441
|
+
attackColor: "#ff007f",
|
|
4442
|
+
healColor: "#00ff00",
|
|
4443
|
+
// Phase 2 defaults — off by default so imports stay close to
|
|
4444
|
+
// SE's behaviour and don't surprise the streamer with new effects.
|
|
4445
|
+
lastResetAt: 0,
|
|
4446
|
+
playSounds: true,
|
|
4447
|
+
audioVolume: 50,
|
|
4448
|
+
damageSounds: [],
|
|
4449
|
+
healSound: "",
|
|
4450
|
+
newBossSound: "",
|
|
4451
|
+
defeatSound: "",
|
|
4452
|
+
megaBossChance: 0,
|
|
4453
|
+
particleScatter: true,
|
|
4454
|
+
showCrown: true,
|
|
4455
|
+
matchBossChatColor: true,
|
|
4456
|
+
bossNameColor: "",
|
|
4457
|
+
botChat: "off",
|
|
4458
|
+
chatAsSelf: false,
|
|
4459
|
+
// SE-only knobs preserved as provenance for future passes. None
|
|
4460
|
+
// have direct Lumia analogs today:
|
|
4461
|
+
// - `right` anchors the boss to the right edge of its bounds;
|
|
4462
|
+
// Lumia uses layer position instead, so this is informational.
|
|
4463
|
+
// - `bossName` controls vertical name position in SE; our themes
|
|
4464
|
+
// don't expose that level of fine-grained tweaking yet.
|
|
4465
|
+
// - `mode` SE's gameplay mode hint ('overkill', etc.).
|
|
4466
|
+
se_right: !!v.right,
|
|
4467
|
+
se_bossNamePosition: typeof v.bossName === "string" ? v.bossName : null,
|
|
4468
|
+
se_mode: typeof v.mode === "string" ? v.mode : null,
|
|
4469
|
+
se_showName: v.showName !== false
|
|
4470
|
+
},
|
|
4471
|
+
css: {
|
|
4472
|
+
background: "transparent"
|
|
4473
|
+
}
|
|
4474
|
+
});
|
|
4475
|
+
}
|
|
4476
|
+
|
|
4175
4477
|
// src/se-import/feature-flags.ts
|
|
4176
4478
|
var SE_IMPORT_FLAGS = {
|
|
4177
4479
|
// (Removed) IMPORT_CUSTOM_EMBED: custom-event-list now routes to native eventlist
|
|
4178
4480
|
// by default with a one-click "switch to custom HTML" option in the review step.
|
|
4179
4481
|
// (Removed) IMPORT_MERCH_GOAL: Fourthwall integration provides session/total order
|
|
4180
4482
|
// counters now (see fourthwall.manager.ts ORDER_PLACED handler); merch goal maps
|
|
4181
|
-
// to {{
|
|
4182
|
-
//
|
|
4183
|
-
//
|
|
4184
|
-
//
|
|
4185
|
-
//
|
|
4186
|
-
//
|
|
4187
|
-
//
|
|
4188
|
-
|
|
4189
|
-
//
|
|
4190
|
-
//
|
|
4191
|
-
//
|
|
4192
|
-
//
|
|
4193
|
-
//
|
|
4194
|
-
IMPORT_SEASONAL: false
|
|
4483
|
+
// to {{fourthwall_total_*}} via the goal mapper (per-listener: orders → order_count,
|
|
4484
|
+
// items → items_count, total → order_amount).
|
|
4485
|
+
// (Removed) IMPORT_SEASONAL: seasonal widgets now route per-widget into existing
|
|
4486
|
+
// modules (image / slideshow / video) rather than a single approximate canvas-
|
|
4487
|
+
// particle template. See dispatcher.ts SEASONAL_IMAGE_TYPES comment for the
|
|
4488
|
+
// rationale and /SE/05-widget-to-module-mapping.md §D for the current mapping.
|
|
4489
|
+
// (Removed) IMPORT_MERCH_PRODUCTS: the merch-products-rotator now imports as an
|
|
4490
|
+
// empty slideshow with a friendly layer title prompting the user to add product
|
|
4491
|
+
// images manually. This is a degraded but honest UX — better than a placeholder
|
|
4492
|
+
// text layer for the (rare) streamer who uses SE's merch rotator. A Fourthwall
|
|
4493
|
+
// catalog fetcher would unlock fully-automatic import, but that's a multi-day
|
|
4494
|
+
// project gated on a new Fourthwall API auth scope, an endpoint, and a refresh
|
|
4495
|
+
// cadence.
|
|
4195
4496
|
};
|
|
4196
|
-
function isWidgetFlaggedOff(
|
|
4197
|
-
if (seType === "se-widget-merch-products-rotator") return !SE_IMPORT_FLAGS.IMPORT_MERCH_PRODUCTS;
|
|
4198
|
-
if (seType === "se-widget-snow" || seType === "se-widget-halloween" || seType === "se-widget-halloween-2019" || seType === "se-widget-xmas" || seType === "se-widget-valentine" || seType === "se-widget-easter" || seType === "se-widget-world-cup") {
|
|
4199
|
-
return !SE_IMPORT_FLAGS.IMPORT_SEASONAL;
|
|
4200
|
-
}
|
|
4497
|
+
function isWidgetFlaggedOff(_seType) {
|
|
4201
4498
|
return false;
|
|
4202
4499
|
}
|
|
4203
|
-
var FLAG_OFF_REASONS = {
|
|
4204
|
-
"se-widget-merch-products-rotator": "Merch products rotator \u2014 Lumia tracks Fourthwall orders but doesn't fetch the full product catalog yet. Generate a replacement with AI, skip it, or use Marketplace when a curated replacement is configured.",
|
|
4205
|
-
"se-widget-snow": "Seasonal effect \u2014 the generic auto-import template is approximate. Generate a replacement with AI, skip it, or use Marketplace when a curated version is configured.",
|
|
4206
|
-
"se-widget-halloween": "Seasonal effect \u2014 the generic auto-import template is approximate. Generate a replacement with AI, skip it, or use Marketplace when a curated version is configured.",
|
|
4207
|
-
"se-widget-halloween-2019": "Seasonal effect \u2014 the generic auto-import template is approximate. Generate a replacement with AI, skip it, or use Marketplace when a curated version is configured.",
|
|
4208
|
-
"se-widget-xmas": "Seasonal effect \u2014 the generic auto-import template is approximate. Generate a replacement with AI, skip it, or use Marketplace when a curated version is configured.",
|
|
4209
|
-
"se-widget-valentine": "Seasonal effect \u2014 the generic auto-import template is approximate. Generate a replacement with AI, skip it, or use Marketplace when a curated version is configured.",
|
|
4210
|
-
"se-widget-easter": "Seasonal effect \u2014 the generic auto-import template is approximate. Generate a replacement with AI, skip it, or use Marketplace when a curated version is configured.",
|
|
4211
|
-
"se-widget-world-cup": "Seasonal effect \u2014 the generic auto-import template is approximate. Generate a replacement with AI, skip it, or use Marketplace when a curated version is configured."
|
|
4212
|
-
};
|
|
4500
|
+
var FLAG_OFF_REASONS = {};
|
|
4213
4501
|
|
|
4214
4502
|
// src/se-import/dispatcher.ts
|
|
4215
4503
|
var READOUT_TYPES = {
|
|
@@ -4238,15 +4526,7 @@ var READOUT_TYPES = {
|
|
|
4238
4526
|
// twitch_session_chat_count, so we render it as a text layer with the variable inline.
|
|
4239
4527
|
"se-widget-botcounter": "twitch_session_chat_count"
|
|
4240
4528
|
};
|
|
4241
|
-
var
|
|
4242
|
-
"se-widget-snow": "snow",
|
|
4243
|
-
"se-widget-halloween": "halloween",
|
|
4244
|
-
"se-widget-halloween-2019": "halloween",
|
|
4245
|
-
"se-widget-xmas": "xmas",
|
|
4246
|
-
"se-widget-valentine": "valentine",
|
|
4247
|
-
"se-widget-easter": "easter",
|
|
4248
|
-
"se-widget-world-cup": "worldcup"
|
|
4249
|
-
};
|
|
4529
|
+
var SEASONAL_IMAGE_TYPES = /* @__PURE__ */ new Set(["se-widget-halloween", "se-widget-xmas", "se-widget-valentine", "se-widget-easter", "se-widget-world-cup"]);
|
|
4250
4530
|
var GOAL_TYPES = /* @__PURE__ */ new Set([
|
|
4251
4531
|
"se-widget-follower-goal",
|
|
4252
4532
|
"se-widget-subscriber-goal",
|
|
@@ -4287,10 +4567,17 @@ function dispatch(widget) {
|
|
|
4287
4567
|
if (t === "se-widget-current-song") return { unit: mapNowPlaying(widget), status: "partial", lumiaType: "nowplaying" };
|
|
4288
4568
|
if (t === "se-widget-kappagen") return { unit: mapKappagen(widget), status: "partial", lumiaType: "emotealert" };
|
|
4289
4569
|
if (t === "se-widget-giveaway") return { unit: mapGiveaway(widget), status: "direct", lumiaType: "raffle" };
|
|
4290
|
-
if (t === "se-widget-
|
|
4291
|
-
if (t
|
|
4292
|
-
|
|
4570
|
+
if (t === "se-widget-media-share") return { unit: mapMediaShare(widget), status: "partial", lumiaType: "songrequest" };
|
|
4571
|
+
if (t === "se-widget-hype-cup") return { unit: mapHypeCup(widget), status: "partial", lumiaType: "tipjar" };
|
|
4572
|
+
if (t === "se-widget-bit-boss") return { unit: mapStreamBoss(widget), status: "partial", lumiaType: "streamboss" };
|
|
4573
|
+
if (t === "se-widget-hype-train") return { unit: mapHypetrain(widget), status: "partial", lumiaType: "hypetrain" };
|
|
4574
|
+
if (t === "se-widget-custom-event-list" && isHypetrainCustomWidget(widget)) {
|
|
4575
|
+
return { unit: mapHypetrain(widget), status: "partial", lumiaType: "hypetrain" };
|
|
4293
4576
|
}
|
|
4577
|
+
if (t === "se-widget-custom-event-list") return { unit: mapCustom(widget), status: "partial", lumiaType: "custom" };
|
|
4578
|
+
if (t === "se-widget-snow") return { unit: mapSnow(widget), status: "direct", lumiaType: "video" };
|
|
4579
|
+
if (t === "se-widget-halloween-2019") return { unit: mapSlideshow(widget), status: "direct", lumiaType: "slideshow" };
|
|
4580
|
+
if (SEASONAL_IMAGE_TYPES.has(t)) return { unit: mapImage(widget), status: "direct", lumiaType: "image" };
|
|
4294
4581
|
return { unit: mapUnsupportedAsText(widget), status: "placeholder", lumiaType: "text" };
|
|
4295
4582
|
}
|
|
4296
4583
|
function recordCoverage(coverage, result, seType) {
|
|
@@ -4477,7 +4764,10 @@ var SE_WIDGET_TO_MARKETPLACE_CANDIDATES = {
|
|
|
4477
4764
|
// value for marketplace replacements since AI generation rarely produces
|
|
4478
4765
|
// satisfying results for these.
|
|
4479
4766
|
"se-widget-hype-cup": [],
|
|
4480
|
-
|
|
4767
|
+
// Note: `se-widget-bit-boss` is auto-mapped onto the native `streamboss`
|
|
4768
|
+
// module by the dispatcher and no longer needs a marketplace fallback.
|
|
4769
|
+
// Re-add a key here if a community curated boss overlay later supersedes
|
|
4770
|
+
// the native module for specific use cases.
|
|
4481
4771
|
"se-widget-train": [],
|
|
4482
4772
|
// Medium-difficulty widgets — possible to AI-generate but a curated overlay
|
|
4483
4773
|
// will usually look better.
|
|
@@ -4487,6 +4777,15 @@ var SE_WIDGET_TO_MARKETPLACE_CANDIDATES = {
|
|
|
4487
4777
|
// have curated replacements for.
|
|
4488
4778
|
"se-widget-custom-event-list": [],
|
|
4489
4779
|
"se-widget-botcounter": [],
|
|
4780
|
+
// Hype Train — the dispatcher already auto-routes `se-widget-hype-train` and
|
|
4781
|
+
// hypetrain-only custom-event-lists onto the native `hypetrain` module. This
|
|
4782
|
+
// entry stays here for two cases the auto-router can't catch:
|
|
4783
|
+
// - Heavily customized hype-train custom widgets where the native module
|
|
4784
|
+
// would lose too much visual fidelity (custom locomotive art, themed
|
|
4785
|
+
// particles, etc.) and the streamer would prefer a curated alternative.
|
|
4786
|
+
// - Community curated overlays that pair the native hypetrain with extra
|
|
4787
|
+
// decorative layers (e.g. a "depot" background, station signage).
|
|
4788
|
+
"se-widget-hype-train": [],
|
|
4490
4789
|
// Flagged-off in feature-flags.ts — the auto-import is disabled because
|
|
4491
4790
|
// Lumia has no merch data source. Marketplace overlays here would be
|
|
4492
4791
|
// curated static merch-style goals/sliders that don't depend on a feed.
|
|
@@ -4619,6 +4918,13 @@ async function fetchUsableChannels(client) {
|
|
|
4619
4918
|
const me = await client.get("/kappa/v2/users/current");
|
|
4620
4919
|
return (me.channels ?? []).filter((c) => c.authorized !== false && c.inactive !== true);
|
|
4621
4920
|
}
|
|
4921
|
+
async function fetchSEOverlays(client, count = 200) {
|
|
4922
|
+
const res = await client.get(`/kappa/v2/overlays/${client.channelId}?count=${count}`);
|
|
4923
|
+
return res.docs ?? [];
|
|
4924
|
+
}
|
|
4925
|
+
async function fetchSEBootstrapForOverlay(client, overlayId) {
|
|
4926
|
+
return client.get(`/kappa/v2/overlays/${overlayId}/bootstrap?isEditor=false&isMobile=false&isObs=false&isObsLive=false&isXsplit=false`);
|
|
4927
|
+
}
|
|
4622
4928
|
|
|
4623
4929
|
// src/se-import/ui/SEImportWizard.tsx
|
|
4624
4930
|
import { useCallback as useCallback3, useEffect as useEffect6, useMemo as useMemo5, useRef as useRef4, useState as useState6 } from "react";
|
|
@@ -4780,6 +5086,7 @@ var imageIcon = new URL("../../assets/img.svg", import.meta.url).href;
|
|
|
4780
5086
|
var videoIcon = new URL("../../assets/video.svg", import.meta.url).href;
|
|
4781
5087
|
var CORE_STEPS = [
|
|
4782
5088
|
{ key: "connect", label: "Connect" },
|
|
5089
|
+
{ key: "pick", label: "Overlays" },
|
|
4783
5090
|
{ key: "url", label: "Load" },
|
|
4784
5091
|
{ key: "discovery", label: "Discovery" },
|
|
4785
5092
|
{ key: "options", label: "Options" },
|
|
@@ -4932,6 +5239,136 @@ function StepHeader({
|
|
|
4932
5239
|
] })
|
|
4933
5240
|
] });
|
|
4934
5241
|
}
|
|
5242
|
+
var JWT_FEATURES = [
|
|
5243
|
+
"All your overlays \u2014 pick one, many, or all",
|
|
5244
|
+
"Live counters (follower / sub / tip / bits goals)",
|
|
5245
|
+
"Chat bot commands",
|
|
5246
|
+
"Loyalty points + watch time",
|
|
5247
|
+
"Recent activity replay",
|
|
5248
|
+
"Bits leaderboard"
|
|
5249
|
+
];
|
|
5250
|
+
function ModeCard({
|
|
5251
|
+
primary,
|
|
5252
|
+
title,
|
|
5253
|
+
body,
|
|
5254
|
+
caveat,
|
|
5255
|
+
onClick
|
|
5256
|
+
}) {
|
|
5257
|
+
return /* @__PURE__ */ jsxs7(
|
|
5258
|
+
"button",
|
|
5259
|
+
{
|
|
5260
|
+
type: "button",
|
|
5261
|
+
onClick,
|
|
5262
|
+
style: {
|
|
5263
|
+
display: "flex",
|
|
5264
|
+
flexDirection: "column",
|
|
5265
|
+
alignItems: "flex-start",
|
|
5266
|
+
gap: 14,
|
|
5267
|
+
padding: 24,
|
|
5268
|
+
minHeight: 280,
|
|
5269
|
+
borderRadius: 14,
|
|
5270
|
+
border: primary ? "2px solid var(--se-primary)" : "1px solid var(--se-border)",
|
|
5271
|
+
background: primary ? "rgba(255, 64, 118, 0.08)" : "transparent",
|
|
5272
|
+
color: "var(--se-text)",
|
|
5273
|
+
textAlign: "left",
|
|
5274
|
+
cursor: "pointer",
|
|
5275
|
+
transition: "background 120ms, border-color 120ms, transform 120ms"
|
|
5276
|
+
},
|
|
5277
|
+
onMouseEnter: (e) => {
|
|
5278
|
+
e.currentTarget.style.background = primary ? "rgba(255, 64, 118, 0.14)" : "rgba(255, 255, 255, 0.04)";
|
|
5279
|
+
if (!primary)
|
|
5280
|
+
e.currentTarget.style.borderColor = "var(--se-border-strong)";
|
|
5281
|
+
e.currentTarget.style.transform = "translateY(-2px)";
|
|
5282
|
+
},
|
|
5283
|
+
onMouseLeave: (e) => {
|
|
5284
|
+
e.currentTarget.style.background = primary ? "rgba(255, 64, 118, 0.08)" : "transparent";
|
|
5285
|
+
if (!primary)
|
|
5286
|
+
e.currentTarget.style.borderColor = "var(--se-border)";
|
|
5287
|
+
e.currentTarget.style.transform = "translateY(0)";
|
|
5288
|
+
},
|
|
5289
|
+
children: [
|
|
5290
|
+
/* @__PURE__ */ jsx13(
|
|
5291
|
+
"div",
|
|
5292
|
+
{
|
|
5293
|
+
style: {
|
|
5294
|
+
fontSize: 18,
|
|
5295
|
+
fontWeight: 800,
|
|
5296
|
+
color: primary ? "var(--se-primary)" : "var(--se-text)"
|
|
5297
|
+
},
|
|
5298
|
+
children: title
|
|
5299
|
+
}
|
|
5300
|
+
),
|
|
5301
|
+
/* @__PURE__ */ jsx13("div", { style: { fontSize: 13.5, lineHeight: 1.55, color: "var(--se-text)", width: "100%" }, children: body }),
|
|
5302
|
+
/* @__PURE__ */ jsx13("div", { style: { fontSize: 12, color: "var(--se-muted)", marginTop: "auto" }, children: caveat })
|
|
5303
|
+
]
|
|
5304
|
+
}
|
|
5305
|
+
);
|
|
5306
|
+
}
|
|
5307
|
+
function StepModePicker({
|
|
5308
|
+
onChooseJwt,
|
|
5309
|
+
onChooseUrl
|
|
5310
|
+
}) {
|
|
5311
|
+
return /* @__PURE__ */ jsxs7("div", { className: panelClass("se-import-panel--wide"), children: [
|
|
5312
|
+
/* @__PURE__ */ jsx13(
|
|
5313
|
+
StepHeader,
|
|
5314
|
+
{
|
|
5315
|
+
number: 1,
|
|
5316
|
+
title: "What would you like to import?",
|
|
5317
|
+
subtitle: "Pick a starting point. You can change your mind from the Back button on the next step."
|
|
5318
|
+
}
|
|
5319
|
+
),
|
|
5320
|
+
/* @__PURE__ */ jsxs7(
|
|
5321
|
+
"div",
|
|
5322
|
+
{
|
|
5323
|
+
style: {
|
|
5324
|
+
display: "grid",
|
|
5325
|
+
gridTemplateColumns: "repeat(auto-fit, minmax(300px, 1fr))",
|
|
5326
|
+
gap: 18,
|
|
5327
|
+
marginTop: 8
|
|
5328
|
+
},
|
|
5329
|
+
children: [
|
|
5330
|
+
/* @__PURE__ */ jsx13(
|
|
5331
|
+
ModeCard,
|
|
5332
|
+
{
|
|
5333
|
+
primary: true,
|
|
5334
|
+
title: "Import everything from StreamElements",
|
|
5335
|
+
body: /* @__PURE__ */ jsxs7(Fragment3, { children: [
|
|
5336
|
+
/* @__PURE__ */ jsx13("div", { style: { marginBottom: 10 }, children: "Connect once and bring over your entire SE setup:" }),
|
|
5337
|
+
/* @__PURE__ */ jsx13(
|
|
5338
|
+
"ul",
|
|
5339
|
+
{
|
|
5340
|
+
style: {
|
|
5341
|
+
margin: 0,
|
|
5342
|
+
paddingLeft: 18,
|
|
5343
|
+
display: "grid",
|
|
5344
|
+
gap: 6,
|
|
5345
|
+
fontSize: 13
|
|
5346
|
+
},
|
|
5347
|
+
children: JWT_FEATURES.map((feat) => /* @__PURE__ */ jsx13("li", { children: feat }, feat))
|
|
5348
|
+
}
|
|
5349
|
+
)
|
|
5350
|
+
] }),
|
|
5351
|
+
caveat: "Requires pasting your StreamElements JWT. Stored only in memory \u2014 never sent to Lumia.",
|
|
5352
|
+
onClick: onChooseJwt
|
|
5353
|
+
}
|
|
5354
|
+
),
|
|
5355
|
+
/* @__PURE__ */ jsx13(
|
|
5356
|
+
ModeCard,
|
|
5357
|
+
{
|
|
5358
|
+
title: "Import a single overlay",
|
|
5359
|
+
body: /* @__PURE__ */ jsxs7(Fragment3, { children: [
|
|
5360
|
+
"Paste a single overlay's public preview URL to bring just that overlay over.",
|
|
5361
|
+
/* @__PURE__ */ jsx13("div", { style: { marginTop: 10, color: "var(--se-muted)" }, children: "No login required \u2014 perfect for trying one overlay before deciding whether to migrate your whole account." })
|
|
5362
|
+
] }),
|
|
5363
|
+
caveat: "The URL contains the access token; treat it like a password.",
|
|
5364
|
+
onClick: onChooseUrl
|
|
5365
|
+
}
|
|
5366
|
+
)
|
|
5367
|
+
]
|
|
5368
|
+
}
|
|
5369
|
+
)
|
|
5370
|
+
] });
|
|
5371
|
+
}
|
|
4935
5372
|
function StepConnect({
|
|
4936
5373
|
jwt,
|
|
4937
5374
|
setJwt,
|
|
@@ -5057,6 +5494,140 @@ function StepConnect({
|
|
|
5057
5494
|
error && /* @__PURE__ */ jsx13(ErrorPanel, { message: error.message, hint: error.hint })
|
|
5058
5495
|
] });
|
|
5059
5496
|
}
|
|
5497
|
+
function StepPickOverlay({
|
|
5498
|
+
overlays,
|
|
5499
|
+
loading,
|
|
5500
|
+
error,
|
|
5501
|
+
selectedIds,
|
|
5502
|
+
toggleId,
|
|
5503
|
+
selectAll,
|
|
5504
|
+
clearAll,
|
|
5505
|
+
onRetry
|
|
5506
|
+
}) {
|
|
5507
|
+
const selectedCount = selectedIds.size;
|
|
5508
|
+
const total = overlays?.length ?? 0;
|
|
5509
|
+
const allSelected = total > 0 && selectedCount === total;
|
|
5510
|
+
const hasAnySelected = selectedCount > 0;
|
|
5511
|
+
return /* @__PURE__ */ jsxs7("div", { className: panelClass("se-import-panel--wide"), children: [
|
|
5512
|
+
/* @__PURE__ */ jsx13(
|
|
5513
|
+
StepHeader,
|
|
5514
|
+
{
|
|
5515
|
+
number: 2,
|
|
5516
|
+
title: "Pick the overlays to import",
|
|
5517
|
+
subtitle: `Select one or more StreamElements overlays from your account. We'll run each one through the discovery \u2192 review \u2192 import pipeline in sequence. ${total > 0 ? `${total} overlay${total === 1 ? "" : "s"} on this channel.` : ""}`
|
|
5518
|
+
}
|
|
5519
|
+
),
|
|
5520
|
+
loading && /* @__PURE__ */ jsxs7("div", { className: "se-import-help se-import-help--info", children: [
|
|
5521
|
+
/* @__PURE__ */ jsx13("div", { className: "se-import-help__icon", children: "\u2026" }),
|
|
5522
|
+
/* @__PURE__ */ jsx13("div", { children: "Fetching your StreamElements overlays." })
|
|
5523
|
+
] }),
|
|
5524
|
+
error && /* @__PURE__ */ jsxs7(Fragment3, { children: [
|
|
5525
|
+
/* @__PURE__ */ jsx13(ErrorPanel, { message: error.message, hint: error.hint }),
|
|
5526
|
+
/* @__PURE__ */ jsx13("div", { style: { marginTop: 12 }, children: /* @__PURE__ */ jsx13(LSButton, { type: "button", color: "secondary", onClick: onRetry, label: "Retry" }) })
|
|
5527
|
+
] }),
|
|
5528
|
+
!loading && !error && overlays && overlays.length === 0 && /* @__PURE__ */ jsxs7("div", { className: "se-import-help se-import-help--info", children: [
|
|
5529
|
+
/* @__PURE__ */ jsx13("div", { className: "se-import-help__icon", children: "i" }),
|
|
5530
|
+
/* @__PURE__ */ jsxs7("div", { children: [
|
|
5531
|
+
/* @__PURE__ */ jsx13("div", { className: "se-import-help__title", children: "No overlays found" }),
|
|
5532
|
+
/* @__PURE__ */ jsx13("p", { children: "This StreamElements account doesn't have any overlays yet. Create one in the SE dashboard, or use the legacy preview URL flow from the Connect step." })
|
|
5533
|
+
] })
|
|
5534
|
+
] }),
|
|
5535
|
+
!loading && !error && overlays && overlays.length > 0 && /* @__PURE__ */ jsxs7(Fragment3, { children: [
|
|
5536
|
+
/* @__PURE__ */ jsxs7("div", { className: "se-import-actions se-import-actions--inline", style: { marginTop: 24, marginBottom: 16, justifyContent: "space-between", alignItems: "center", display: "flex" }, children: [
|
|
5537
|
+
/* @__PURE__ */ jsxs7("label", { style: { display: "inline-flex", alignItems: "center", gap: 8, cursor: "pointer", fontSize: 14, fontWeight: 600 }, children: [
|
|
5538
|
+
/* @__PURE__ */ jsx13(
|
|
5539
|
+
"input",
|
|
5540
|
+
{
|
|
5541
|
+
type: "checkbox",
|
|
5542
|
+
checked: allSelected,
|
|
5543
|
+
ref: (el) => {
|
|
5544
|
+
if (el) el.indeterminate = !allSelected && hasAnySelected;
|
|
5545
|
+
},
|
|
5546
|
+
onChange: () => allSelected ? clearAll() : selectAll()
|
|
5547
|
+
}
|
|
5548
|
+
),
|
|
5549
|
+
/* @__PURE__ */ jsxs7("span", { children: [
|
|
5550
|
+
allSelected ? "Deselect all" : "Select all",
|
|
5551
|
+
" (",
|
|
5552
|
+
selectedCount,
|
|
5553
|
+
"/",
|
|
5554
|
+
total,
|
|
5555
|
+
")"
|
|
5556
|
+
] })
|
|
5557
|
+
] }),
|
|
5558
|
+
/* @__PURE__ */ jsx13("span", { style: { fontSize: 13, color: "var(--se-muted)" }, children: hasAnySelected ? `${selectedCount} overlay${selectedCount === 1 ? "" : "s"} queued. Each runs through the full import flow in sequence.` : "Click overlays below to select them." })
|
|
5559
|
+
] }),
|
|
5560
|
+
/* @__PURE__ */ jsx13(
|
|
5561
|
+
"div",
|
|
5562
|
+
{
|
|
5563
|
+
className: "se-import-grid",
|
|
5564
|
+
style: {
|
|
5565
|
+
gridTemplateColumns: "repeat(auto-fill, minmax(220px, 1fr))",
|
|
5566
|
+
gap: 12,
|
|
5567
|
+
maxHeight: 480,
|
|
5568
|
+
overflowY: "auto",
|
|
5569
|
+
padding: 4
|
|
5570
|
+
},
|
|
5571
|
+
children: overlays.map((o) => {
|
|
5572
|
+
const selected = selectedIds.has(o._id);
|
|
5573
|
+
return /* @__PURE__ */ jsxs7(
|
|
5574
|
+
"label",
|
|
5575
|
+
{
|
|
5576
|
+
style: {
|
|
5577
|
+
position: "relative",
|
|
5578
|
+
display: "flex",
|
|
5579
|
+
flexDirection: "column",
|
|
5580
|
+
border: selected ? "2px solid var(--se-primary)" : "1px solid var(--se-border)",
|
|
5581
|
+
borderRadius: 10,
|
|
5582
|
+
padding: 10,
|
|
5583
|
+
background: selected ? "rgba(255, 64, 118, 0.08)" : "rgba(28, 32, 66, 0.45)",
|
|
5584
|
+
cursor: "pointer",
|
|
5585
|
+
transition: "border-color 120ms, background 120ms"
|
|
5586
|
+
},
|
|
5587
|
+
children: [
|
|
5588
|
+
/* @__PURE__ */ jsx13(
|
|
5589
|
+
"input",
|
|
5590
|
+
{
|
|
5591
|
+
type: "checkbox",
|
|
5592
|
+
checked: selected,
|
|
5593
|
+
onChange: () => toggleId(o._id),
|
|
5594
|
+
style: { position: "absolute", top: 10, right: 10, accentColor: "var(--se-primary)", zIndex: 1 }
|
|
5595
|
+
}
|
|
5596
|
+
),
|
|
5597
|
+
o.preview ? /* @__PURE__ */ jsx13(
|
|
5598
|
+
"img",
|
|
5599
|
+
{
|
|
5600
|
+
src: o.preview,
|
|
5601
|
+
alt: "",
|
|
5602
|
+
style: {
|
|
5603
|
+
width: "100%",
|
|
5604
|
+
height: 110,
|
|
5605
|
+
objectFit: "cover",
|
|
5606
|
+
borderRadius: 6,
|
|
5607
|
+
background: "var(--se-panel-2)"
|
|
5608
|
+
},
|
|
5609
|
+
loading: "lazy"
|
|
5610
|
+
}
|
|
5611
|
+
) : /* @__PURE__ */ jsx13("div", { style: { width: "100%", height: 110, borderRadius: 6, background: "var(--se-panel-2)" } }),
|
|
5612
|
+
/* @__PURE__ */ jsx13("div", { style: { marginTop: 8, fontSize: 14, fontWeight: 700, color: "var(--se-text)", paddingRight: 28 }, children: o.name ?? "Unnamed overlay" }),
|
|
5613
|
+
/* @__PURE__ */ jsxs7("div", { style: { marginTop: 4, fontSize: 11, color: "var(--se-muted)" }, children: [
|
|
5614
|
+
o.type ?? "regular",
|
|
5615
|
+
o.settings?.width && o.settings?.height ? ` \xB7 ${o.settings.width}\xD7${o.settings.height}` : ""
|
|
5616
|
+
] }),
|
|
5617
|
+
o.updatedAt && /* @__PURE__ */ jsxs7("div", { style: { marginTop: 2, fontSize: 11, color: "var(--se-muted)" }, children: [
|
|
5618
|
+
"Updated ",
|
|
5619
|
+
new Date(o.updatedAt).toLocaleDateString()
|
|
5620
|
+
] })
|
|
5621
|
+
]
|
|
5622
|
+
},
|
|
5623
|
+
o._id
|
|
5624
|
+
);
|
|
5625
|
+
})
|
|
5626
|
+
}
|
|
5627
|
+
)
|
|
5628
|
+
] })
|
|
5629
|
+
] });
|
|
5630
|
+
}
|
|
5060
5631
|
function StepURL({
|
|
5061
5632
|
url,
|
|
5062
5633
|
setUrl,
|
|
@@ -5077,7 +5648,7 @@ function StepURL({
|
|
|
5077
5648
|
{
|
|
5078
5649
|
label: "Overlay URL",
|
|
5079
5650
|
value: url,
|
|
5080
|
-
onChange: setUrl,
|
|
5651
|
+
onChange: (_, next) => setUrl(String(next ?? "")),
|
|
5081
5652
|
placeholder: "https://streamelements.com/overlay/<id>/<token>",
|
|
5082
5653
|
disabled: busy
|
|
5083
5654
|
}
|
|
@@ -5554,13 +6125,23 @@ function SEImportWizard({
|
|
|
5554
6125
|
t
|
|
5555
6126
|
} = bindings;
|
|
5556
6127
|
const translate = (key, fallback) => t ? t(key) ?? fallback : fallback;
|
|
5557
|
-
const [step, setStep] = useState6("
|
|
6128
|
+
const [step, setStep] = useState6(initialUrl ? "url" : "mode");
|
|
6129
|
+
const [entryMode, setEntryMode] = useState6(
|
|
6130
|
+
initialUrl ? "url" : null
|
|
6131
|
+
);
|
|
5558
6132
|
const [jwt, setJwt] = useState6("");
|
|
5559
6133
|
const [seClient, setSeClient] = useState6(null);
|
|
5560
6134
|
const [channels, setChannels] = useState6(null);
|
|
5561
6135
|
const [selectedChannelId, setSelectedChannelId] = useState6(null);
|
|
5562
6136
|
const [connectError, setConnectError] = useState6(null);
|
|
5563
6137
|
const [connecting, setConnecting] = useState6(false);
|
|
6138
|
+
const [overlayList, setOverlayList] = useState6(null);
|
|
6139
|
+
const [overlaysLoading, setOverlaysLoading] = useState6(false);
|
|
6140
|
+
const [overlaysError, setOverlaysError] = useState6(null);
|
|
6141
|
+
const [selectedOverlayIds, setSelectedOverlayIds] = useState6(/* @__PURE__ */ new Set());
|
|
6142
|
+
const [overlayQueue, setOverlayQueue] = useState6([]);
|
|
6143
|
+
const [currentOverlayIndex, setCurrentOverlayIndex] = useState6(0);
|
|
6144
|
+
const [totalOverlaysToImport, setTotalOverlaysToImport] = useState6(0);
|
|
5564
6145
|
const [url, setUrl] = useState6(initialUrl);
|
|
5565
6146
|
const [loadError, setLoadError] = useState6(null);
|
|
5566
6147
|
const [loading, setLoading] = useState6(false);
|
|
@@ -5630,6 +6211,73 @@ function SEImportWizard({
|
|
|
5630
6211
|
setConnecting(false);
|
|
5631
6212
|
}
|
|
5632
6213
|
}, [bindings, jwt]);
|
|
6214
|
+
const loadOverlayList = useCallback3(async (client) => {
|
|
6215
|
+
setOverlaysError(null);
|
|
6216
|
+
setOverlaysLoading(true);
|
|
6217
|
+
try {
|
|
6218
|
+
const list = await fetchSEOverlays(client);
|
|
6219
|
+
setOverlayList(list);
|
|
6220
|
+
} catch (e) {
|
|
6221
|
+
if (e instanceof SEAuthError) {
|
|
6222
|
+
setOverlaysError({ message: e.message, hint: e.code === "unauthorized" ? "Generate a new JWT on streamelements.com/dashboard/account/channels." : void 0 });
|
|
6223
|
+
bindings.onAuthError?.(e);
|
|
6224
|
+
} else {
|
|
6225
|
+
setOverlaysError({ message: e instanceof Error ? e.message : "Could not load your StreamElements overlays." });
|
|
6226
|
+
}
|
|
6227
|
+
} finally {
|
|
6228
|
+
setOverlaysLoading(false);
|
|
6229
|
+
}
|
|
6230
|
+
}, [bindings]);
|
|
6231
|
+
const toggleOverlaySelection = useCallback3((id) => {
|
|
6232
|
+
setSelectedOverlayIds((prev) => {
|
|
6233
|
+
const next = new Set(prev);
|
|
6234
|
+
if (next.has(id)) next.delete(id);
|
|
6235
|
+
else next.add(id);
|
|
6236
|
+
return next;
|
|
6237
|
+
});
|
|
6238
|
+
}, []);
|
|
6239
|
+
const selectAllOverlays = useCallback3(() => {
|
|
6240
|
+
setSelectedOverlayIds(new Set((overlayList ?? []).map((o) => o._id)));
|
|
6241
|
+
}, [overlayList]);
|
|
6242
|
+
const clearAllOverlays = useCallback3(() => {
|
|
6243
|
+
setSelectedOverlayIds(/* @__PURE__ */ new Set());
|
|
6244
|
+
}, []);
|
|
6245
|
+
const loadBootstrapAndKickoff = useCallback3(async (client, overlayId) => {
|
|
6246
|
+
setLoadError(null);
|
|
6247
|
+
setLoading(true);
|
|
6248
|
+
try {
|
|
6249
|
+
const bootstrap = await fetchSEBootstrapForOverlay(client, overlayId);
|
|
6250
|
+
if (!isSEBootstrap(bootstrap)) {
|
|
6251
|
+
throw new Error("StreamElements returned a payload that doesn't look like an overlay bootstrap.");
|
|
6252
|
+
}
|
|
6253
|
+
const imported = importSEBootstrap(bootstrap);
|
|
6254
|
+
setResult(imported);
|
|
6255
|
+
setOptions((o) => ({ ...o, name: imported.overlay.name }));
|
|
6256
|
+
setOriginalReviewItems(imported.reviewItems);
|
|
6257
|
+
setRowStates(Object.fromEntries(imported.reviewItems.map((r) => [r.moduleId, { state: "pending" }])));
|
|
6258
|
+
setReviewIndex(0);
|
|
6259
|
+
setMirrorRows([]);
|
|
6260
|
+
mirrorStartedRef.current = false;
|
|
6261
|
+
setStep("discovery");
|
|
6262
|
+
} catch (e) {
|
|
6263
|
+
setLoadError({
|
|
6264
|
+
message: "Could not load the overlay.",
|
|
6265
|
+
hint: e?.message ?? String(e)
|
|
6266
|
+
});
|
|
6267
|
+
} finally {
|
|
6268
|
+
setLoading(false);
|
|
6269
|
+
}
|
|
6270
|
+
}, []);
|
|
6271
|
+
const handlePickConfirm = useCallback3(async () => {
|
|
6272
|
+
if (!seClient) return;
|
|
6273
|
+
const ids = Array.from(selectedOverlayIds);
|
|
6274
|
+
if (ids.length === 0) return;
|
|
6275
|
+
const [first, ...rest] = ids;
|
|
6276
|
+
setOverlayQueue(rest);
|
|
6277
|
+
setTotalOverlaysToImport(ids.length);
|
|
6278
|
+
setCurrentOverlayIndex(1);
|
|
6279
|
+
await loadBootstrapAndKickoff(seClient, first);
|
|
6280
|
+
}, [seClient, selectedOverlayIds, loadBootstrapAndKickoff]);
|
|
5633
6281
|
const handleLoad = useCallback3(async () => {
|
|
5634
6282
|
setLoadError(null);
|
|
5635
6283
|
const parts = extractSEPreviewParts(url);
|
|
@@ -5765,6 +6413,11 @@ function SEImportWizard({
|
|
|
5765
6413
|
void runMirror();
|
|
5766
6414
|
}
|
|
5767
6415
|
}, [step, runMirror]);
|
|
6416
|
+
useEffect6(() => {
|
|
6417
|
+
if (step === "pick" && seClient && overlayList === null && !overlaysLoading) {
|
|
6418
|
+
void loadOverlayList(seClient);
|
|
6419
|
+
}
|
|
6420
|
+
}, [step, seClient, overlayList, overlaysLoading, loadOverlayList]);
|
|
5768
6421
|
const advanceReview = useCallback3(() => {
|
|
5769
6422
|
if (reviewIndex + 1 < originalReviewItems.length)
|
|
5770
6423
|
setReviewIndex(reviewIndex + 1);
|
|
@@ -5898,7 +6551,23 @@ function SEImportWizard({
|
|
|
5898
6551
|
setStep("confirm");
|
|
5899
6552
|
};
|
|
5900
6553
|
const handleFinalImport = useCallback3(async () => {
|
|
5901
|
-
if (!result)
|
|
6554
|
+
if (!result || !seClient) {
|
|
6555
|
+
if (!result) return;
|
|
6556
|
+
setImporting(true);
|
|
6557
|
+
try {
|
|
6558
|
+
const body = { name: options.name.trim() || result.overlay.name, description: result.overlay.description, settings: result.overlay.settings };
|
|
6559
|
+
const { uuid } = await saveOverlay(body);
|
|
6560
|
+
notify.success("Imported overlay from StreamElements.");
|
|
6561
|
+
mirrorAbortRef.current?.abort();
|
|
6562
|
+
onClose();
|
|
6563
|
+
if (uuid) onComplete({ overlayId: uuid });
|
|
6564
|
+
} catch (error) {
|
|
6565
|
+
notify.error(`Import failed: ${extractErrorMessage(error)}`);
|
|
6566
|
+
} finally {
|
|
6567
|
+
setImporting(false);
|
|
6568
|
+
}
|
|
6569
|
+
return;
|
|
6570
|
+
}
|
|
5902
6571
|
setImporting(true);
|
|
5903
6572
|
try {
|
|
5904
6573
|
const body = {
|
|
@@ -5907,7 +6576,19 @@ function SEImportWizard({
|
|
|
5907
6576
|
settings: result.overlay.settings
|
|
5908
6577
|
};
|
|
5909
6578
|
const { uuid } = await saveOverlay(body);
|
|
5910
|
-
|
|
6579
|
+
const hasMore = overlayQueue.length > 0;
|
|
6580
|
+
if (hasMore) {
|
|
6581
|
+
notify.success(`Imported ${currentOverlayIndex} of ${totalOverlaysToImport} overlays from StreamElements.`);
|
|
6582
|
+
const [nextId, ...rest] = overlayQueue;
|
|
6583
|
+
setOverlayQueue(rest);
|
|
6584
|
+
setCurrentOverlayIndex(currentOverlayIndex + 1);
|
|
6585
|
+
setImporting(false);
|
|
6586
|
+
await loadBootstrapAndKickoff(seClient, nextId);
|
|
6587
|
+
return;
|
|
6588
|
+
}
|
|
6589
|
+
notify.success(
|
|
6590
|
+
totalOverlaysToImport > 1 ? `Imported all ${totalOverlaysToImport} overlays from StreamElements.` : "Imported overlay from StreamElements."
|
|
6591
|
+
);
|
|
5911
6592
|
mirrorAbortRef.current?.abort();
|
|
5912
6593
|
onClose();
|
|
5913
6594
|
if (uuid) onComplete({ overlayId: uuid });
|
|
@@ -5916,12 +6597,32 @@ function SEImportWizard({
|
|
|
5916
6597
|
} finally {
|
|
5917
6598
|
setImporting(false);
|
|
5918
6599
|
}
|
|
5919
|
-
}, [
|
|
6600
|
+
}, [
|
|
6601
|
+
notify,
|
|
6602
|
+
onClose,
|
|
6603
|
+
onComplete,
|
|
6604
|
+
options.name,
|
|
6605
|
+
result,
|
|
6606
|
+
saveOverlay,
|
|
6607
|
+
seClient,
|
|
6608
|
+
overlayQueue,
|
|
6609
|
+
currentOverlayIndex,
|
|
6610
|
+
totalOverlaysToImport,
|
|
6611
|
+
loadBootstrapAndKickoff
|
|
6612
|
+
]);
|
|
5920
6613
|
const visibleSteps = useMemo5(() => {
|
|
5921
6614
|
const showMirrorStep = options.mirrorAssets && (assetUrls.length > 0 || mirrorRows.length > 0 || step === "mirror");
|
|
5922
|
-
|
|
5923
|
-
|
|
5924
|
-
|
|
6615
|
+
if (step === "mode") return [];
|
|
6616
|
+
const onUrlFlow = entryMode === "url" || step === "url";
|
|
6617
|
+
const onJwtFlow = !onUrlFlow;
|
|
6618
|
+
const steps = CORE_STEPS.filter((item) => {
|
|
6619
|
+
if (item.key === "mode") return false;
|
|
6620
|
+
if (item.key === "mirror" && !showMirrorStep) return false;
|
|
6621
|
+
if (item.key === "connect" && !onJwtFlow) return false;
|
|
6622
|
+
if (item.key === "pick" && !onJwtFlow) return false;
|
|
6623
|
+
if (item.key === "url" && !onUrlFlow) return false;
|
|
6624
|
+
return true;
|
|
6625
|
+
});
|
|
5925
6626
|
if (originalReviewItems.length > 0)
|
|
5926
6627
|
steps.splice(Math.max(steps.length - 1, 0), 0, {
|
|
5927
6628
|
key: "review",
|
|
@@ -5930,9 +6631,11 @@ function SEImportWizard({
|
|
|
5930
6631
|
return steps;
|
|
5931
6632
|
}, [
|
|
5932
6633
|
assetUrls.length,
|
|
6634
|
+
entryMode,
|
|
5933
6635
|
mirrorRows.length,
|
|
5934
6636
|
options.mirrorAssets,
|
|
5935
6637
|
originalReviewItems.length,
|
|
6638
|
+
seClient,
|
|
5936
6639
|
step
|
|
5937
6640
|
]);
|
|
5938
6641
|
const goBack = () => {
|
|
@@ -5941,14 +6644,22 @@ function SEImportWizard({
|
|
|
5941
6644
|
setReviewIndex(reviewIndex - 1);
|
|
5942
6645
|
return;
|
|
5943
6646
|
}
|
|
6647
|
+
if (currentIndex === 0 && (step === "connect" || step === "url")) {
|
|
6648
|
+
setStep("mode");
|
|
6649
|
+
setEntryMode(null);
|
|
6650
|
+
return;
|
|
6651
|
+
}
|
|
5944
6652
|
if (currentIndex > 0) setStep(visibleSteps[currentIndex - 1].key);
|
|
5945
6653
|
};
|
|
5946
6654
|
const goNext = () => {
|
|
5947
6655
|
if (step === "connect") {
|
|
5948
6656
|
if (!seClient) return void handleConnect();
|
|
5949
|
-
setStep("
|
|
6657
|
+
setStep("pick");
|
|
5950
6658
|
return;
|
|
5951
6659
|
}
|
|
6660
|
+
if (step === "pick") {
|
|
6661
|
+
return void handlePickConfirm();
|
|
6662
|
+
}
|
|
5952
6663
|
if (step === "url") return void handleLoad();
|
|
5953
6664
|
if (step === "review") {
|
|
5954
6665
|
if (!currentItem) return;
|
|
@@ -5971,6 +6682,11 @@ function SEImportWizard({
|
|
|
5971
6682
|
const nextLabel = (() => {
|
|
5972
6683
|
if (step === "connect")
|
|
5973
6684
|
return connecting ? "Connecting..." : seClient ? "Continue" : "Connect";
|
|
6685
|
+
if (step === "pick") {
|
|
6686
|
+
if (loading) return "Loading overlay...";
|
|
6687
|
+
const n = selectedOverlayIds.size;
|
|
6688
|
+
return n > 1 ? `Import ${n} overlays` : "Import overlay";
|
|
6689
|
+
}
|
|
5974
6690
|
if (step === "url") return loading ? "Loading..." : "Load overlay";
|
|
5975
6691
|
if (step === "mirror") return mirrorRunning ? "Working..." : "Continue";
|
|
5976
6692
|
if (step === "review")
|
|
@@ -5982,6 +6698,8 @@ function SEImportWizard({
|
|
|
5982
6698
|
const nextDisabled = (() => {
|
|
5983
6699
|
if (step === "connect")
|
|
5984
6700
|
return connecting || !seClient && !jwt.trim();
|
|
6701
|
+
if (step === "pick")
|
|
6702
|
+
return loading || overlaysLoading || selectedOverlayIds.size === 0;
|
|
5985
6703
|
if (step === "url") return loading || !url.trim();
|
|
5986
6704
|
if (step === "mirror") return mirrorRunning;
|
|
5987
6705
|
if (step === "review")
|
|
@@ -5990,8 +6708,34 @@ function SEImportWizard({
|
|
|
5990
6708
|
return false;
|
|
5991
6709
|
})();
|
|
5992
6710
|
const activeIndex = visibleSteps.findIndex((item) => item.key === step);
|
|
6711
|
+
const batchActive = totalOverlaysToImport > 1 && step !== "mode" && step !== "pick" && step !== "connect" && step !== "url";
|
|
6712
|
+
const currentOverlayName = overlayList?.find((o) => o._id === Array.from(selectedOverlayIds).filter((id) => !overlayQueue.includes(id))[currentOverlayIndex - 1])?.name;
|
|
5993
6713
|
return /* @__PURE__ */ jsx13("div", { className: "se-import", children: /* @__PURE__ */ jsxs7("div", { className: "se-import__shell", children: [
|
|
5994
|
-
|
|
6714
|
+
batchActive && /* @__PURE__ */ jsxs7(
|
|
6715
|
+
"div",
|
|
6716
|
+
{
|
|
6717
|
+
style: {
|
|
6718
|
+
marginBottom: 16,
|
|
6719
|
+
padding: "10px 16px",
|
|
6720
|
+
border: "1px solid var(--se-border-strong)",
|
|
6721
|
+
borderRadius: 8,
|
|
6722
|
+
background: "rgba(255, 64, 118, 0.08)",
|
|
6723
|
+
color: "var(--se-text)",
|
|
6724
|
+
fontSize: 13,
|
|
6725
|
+
fontWeight: 600,
|
|
6726
|
+
textAlign: "center"
|
|
6727
|
+
},
|
|
6728
|
+
children: [
|
|
6729
|
+
"Importing overlay ",
|
|
6730
|
+
currentOverlayIndex,
|
|
6731
|
+
" of ",
|
|
6732
|
+
totalOverlaysToImport,
|
|
6733
|
+
currentOverlayName ? ` \xB7 ${currentOverlayName}` : "",
|
|
6734
|
+
overlayQueue.length > 0 ? ` \xB7 ${overlayQueue.length} more queued` : ""
|
|
6735
|
+
]
|
|
6736
|
+
}
|
|
6737
|
+
),
|
|
6738
|
+
visibleSteps.length > 0 && /* @__PURE__ */ jsx13("header", { className: "se-import__header", children: /* @__PURE__ */ jsx13("div", { className: "se-import-stepper", children: visibleSteps.map((item, index) => /* @__PURE__ */ jsxs7(
|
|
5995
6739
|
"div",
|
|
5996
6740
|
{
|
|
5997
6741
|
className: stepClass(
|
|
@@ -6007,6 +6751,19 @@ function SEImportWizard({
|
|
|
6007
6751
|
item.key
|
|
6008
6752
|
)) }) }),
|
|
6009
6753
|
/* @__PURE__ */ jsxs7("main", { className: "se-import__content", children: [
|
|
6754
|
+
step === "mode" && /* @__PURE__ */ jsx13(
|
|
6755
|
+
StepModePicker,
|
|
6756
|
+
{
|
|
6757
|
+
onChooseJwt: () => {
|
|
6758
|
+
setEntryMode("jwt");
|
|
6759
|
+
setStep("connect");
|
|
6760
|
+
},
|
|
6761
|
+
onChooseUrl: () => {
|
|
6762
|
+
setEntryMode("url");
|
|
6763
|
+
setStep("url");
|
|
6764
|
+
}
|
|
6765
|
+
}
|
|
6766
|
+
),
|
|
6010
6767
|
step === "connect" && /* @__PURE__ */ jsx13(
|
|
6011
6768
|
StepConnect,
|
|
6012
6769
|
{
|
|
@@ -6017,7 +6774,23 @@ function SEImportWizard({
|
|
|
6017
6774
|
channels,
|
|
6018
6775
|
selectedChannelId,
|
|
6019
6776
|
setSelectedChannelId,
|
|
6020
|
-
onUseUrl: () =>
|
|
6777
|
+
onUseUrl: () => {
|
|
6778
|
+
setEntryMode("url");
|
|
6779
|
+
setStep("url");
|
|
6780
|
+
}
|
|
6781
|
+
}
|
|
6782
|
+
),
|
|
6783
|
+
step === "pick" && /* @__PURE__ */ jsx13(
|
|
6784
|
+
StepPickOverlay,
|
|
6785
|
+
{
|
|
6786
|
+
overlays: overlayList,
|
|
6787
|
+
loading: overlaysLoading,
|
|
6788
|
+
error: overlaysError,
|
|
6789
|
+
selectedIds: selectedOverlayIds,
|
|
6790
|
+
toggleId: toggleOverlaySelection,
|
|
6791
|
+
selectAll: selectAllOverlays,
|
|
6792
|
+
clearAll: clearAllOverlays,
|
|
6793
|
+
onRetry: () => seClient && void loadOverlayList(seClient)
|
|
6021
6794
|
}
|
|
6022
6795
|
),
|
|
6023
6796
|
step === "url" && /* @__PURE__ */ jsx13(
|
|
@@ -6109,7 +6882,7 @@ function SEImportWizard({
|
|
|
6109
6882
|
label: translate("action.cancel", "Cancel")
|
|
6110
6883
|
}
|
|
6111
6884
|
),
|
|
6112
|
-
|
|
6885
|
+
step !== "mode" && /* @__PURE__ */ jsx13(
|
|
6113
6886
|
LSButton,
|
|
6114
6887
|
{
|
|
6115
6888
|
type: "button",
|
|
@@ -6119,7 +6892,7 @@ function SEImportWizard({
|
|
|
6119
6892
|
label: "Back"
|
|
6120
6893
|
}
|
|
6121
6894
|
),
|
|
6122
|
-
/* @__PURE__ */ jsx13(
|
|
6895
|
+
step !== "mode" && /* @__PURE__ */ jsx13(
|
|
6123
6896
|
LSButton,
|
|
6124
6897
|
{
|
|
6125
6898
|
type: "button",
|
|
@@ -6141,7 +6914,10 @@ function extractErrorMessage(err) {
|
|
|
6141
6914
|
// src/se-import/index.ts
|
|
6142
6915
|
var REVIEW_REASONS = {
|
|
6143
6916
|
"se-widget-bit-boss": "No native Lumia equivalent \u2014 bit boss fight bar with HP/damage states.",
|
|
6144
|
-
|
|
6917
|
+
// `se-widget-hype-cup` intentionally omitted — it now routes to the native
|
|
6918
|
+
// `tipjar` module (see dispatcher.ts) and lands with `partial` status, so it
|
|
6919
|
+
// never reaches the placeholder-reason fallback. If a future feature flag
|
|
6920
|
+
// disables tipjar auto-import, add an entry to FLAG_OFF_REASONS instead.
|
|
6145
6921
|
"se-widget-train": "No native Lumia equivalent \u2014 animated subscriber/event train.",
|
|
6146
6922
|
"se-widget-media-share": "No native Lumia equivalent \u2014 viewer-submitted media player.",
|
|
6147
6923
|
"se-widget-contest": "No native Lumia equivalent \u2014 SE-specific contest widget.",
|
|
@@ -6150,13 +6926,17 @@ var REVIEW_REASONS = {
|
|
|
6150
6926
|
"se-widget-top-cheerers-list": 'Lumia loyaltyleaderboard needs a "source" extension before it can render cheer rankings.',
|
|
6151
6927
|
"se-widget-top-gifters-list": 'Lumia loyaltyleaderboard needs a "source" extension before it can render gifter rankings.',
|
|
6152
6928
|
"se-widget-top-tippers-list": 'Lumia loyaltyleaderboard needs a "source" extension before it can render tipper rankings.',
|
|
6153
|
-
|
|
6154
|
-
|
|
6155
|
-
|
|
6156
|
-
|
|
6157
|
-
|
|
6158
|
-
|
|
6159
|
-
"se-widget-
|
|
6929
|
+
// Merch products rotator — imports as an empty slideshow with the title
|
|
6930
|
+
// "Merch products (add images manually)". Lumia's Fourthwall integration is
|
|
6931
|
+
// webhook-driven (events flow in) but doesn't fetch the user's shop catalog,
|
|
6932
|
+
// so we can't pre-populate the slideshow items. The streamer adds their
|
|
6933
|
+
// product images by hand in the slideshow editor. Status = 'partial' (see
|
|
6934
|
+
// dispatcher.ts), so this string surfaces in the review step.
|
|
6935
|
+
"se-widget-merch-products-rotator": "Imported as an empty slideshow. Lumia doesn't fetch your Fourthwall product catalog yet, so add your product images manually in the slideshow editor \u2014 the layer is pre-positioned and styled to match your SE widget."
|
|
6936
|
+
// Seasonal widgets (snow / halloween / halloween-2019 / xmas / valentine / easter
|
|
6937
|
+
// / world-cup) intentionally omitted — they route directly to image/slideshow/
|
|
6938
|
+
// video modules now (see dispatcher.ts SEASONAL_IMAGE_TYPES), so they always
|
|
6939
|
+
// produce a `direct` status and never need a fallback review reason.
|
|
6160
6940
|
};
|
|
6161
6941
|
function reasonFor(seType, status, flaggedOff) {
|
|
6162
6942
|
if (flaggedOff && FLAG_OFF_REASONS[seType]) return FLAG_OFF_REASONS[seType];
|