@thestatic-tv/dcl-sdk 2.2.10 → 2.3.0-dev.0

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.mjs CHANGED
@@ -96,8 +96,8 @@ function getPlayerWallet() {
96
96
  return cachedWallet;
97
97
  }
98
98
  try {
99
- const { getPlayer: getPlayer2 } = __require("@dcl/sdk/players");
100
- const player = getPlayer2();
99
+ const { getPlayer: getPlayer3 } = __require("@dcl/sdk/players");
100
+ const player = getPlayer3();
101
101
  return player?.userId ?? null;
102
102
  } catch {
103
103
  return null;
@@ -108,8 +108,8 @@ function getPlayerDisplayName() {
108
108
  return cachedDisplayName;
109
109
  }
110
110
  try {
111
- const { getPlayer: getPlayer2 } = __require("@dcl/sdk/players");
112
- const player = getPlayer2();
111
+ const { getPlayer: getPlayer3 } = __require("@dcl/sdk/players");
112
+ const player = getPlayer3();
113
113
  return player?.name ?? null;
114
114
  } catch {
115
115
  return null;
@@ -2121,6 +2121,1064 @@ var ChatUIModule = class {
2121
2121
  }
2122
2122
  };
2123
2123
 
2124
+ // src/ui/admin-panel-ui.tsx
2125
+ import ReactEcs4, { UiEntity as UiEntity4, Button as Button3, Label as Label4, Input as Input4 } from "@dcl/sdk/react-ecs";
2126
+ import { Color4 as Color45, Vector3 } from "@dcl/sdk/math";
2127
+ import { getPlayer as getPlayer2 } from "@dcl/sdk/players";
2128
+ import { movePlayerTo, openExternalUrl as openExternalUrl3 } from "~system/RestrictedActions";
2129
+ var BAN_KICK_POSITION = Vector3.create(16, -50, 16);
2130
+ var C = {
2131
+ bg: Color45.create(0.08, 0.08, 0.12, 0.98),
2132
+ header: Color45.create(0.9, 0.15, 0.15, 1),
2133
+ tabActive: Color45.create(0, 0.7, 0.7, 1),
2134
+ tabInactive: Color45.create(0.15, 0.15, 0.2, 1),
2135
+ section: Color45.create(0.12, 0.12, 0.18, 1),
2136
+ btn: Color45.create(0.2, 0.2, 0.28, 1),
2137
+ cyan: Color45.create(0, 0.8, 0.8, 1),
2138
+ magenta: Color45.create(0.85, 0.2, 0.55, 1),
2139
+ yellow: Color45.create(0.95, 0.75, 0.1, 1),
2140
+ green: Color45.create(0.2, 0.75, 0.3, 1),
2141
+ red: Color45.create(0.85, 0.2, 0.2, 1),
2142
+ purple: Color45.create(0.6, 0.3, 0.85, 1),
2143
+ orange: Color45.create(0.95, 0.5, 0.15, 1),
2144
+ text: Color45.White(),
2145
+ textDim: Color45.create(0.6, 0.6, 0.7, 1)
2146
+ };
2147
+ var AdminPanelUIModule = class {
2148
+ constructor(client, config) {
2149
+ // State
2150
+ this.isAdmin = false;
2151
+ this.isOwner = false;
2152
+ this.panelOpen = false;
2153
+ this.activeTab = "video";
2154
+ this.playerWallet = "";
2155
+ // Video tab state
2156
+ this.customVideoUrl = "";
2157
+ this.streamData = null;
2158
+ this.streamFetched = false;
2159
+ this.channelCreating = false;
2160
+ this.channelCreateError = "";
2161
+ this.channelDeleting = false;
2162
+ this.channelDeleteError = "";
2163
+ this.keyRotating = false;
2164
+ this.keyRotateStatus = "";
2165
+ this.streamControlling = false;
2166
+ this.streamControlStatus = "";
2167
+ this.pollIntervalId = null;
2168
+ this.trialClaiming = false;
2169
+ this.trialClaimError = "";
2170
+ // Mod tab state
2171
+ this.sceneAdmins = [];
2172
+ this.bannedWallets = [];
2173
+ this.newAdminWallet = "";
2174
+ this.newBanWallet = "";
2175
+ this.broadcastText = "";
2176
+ this.modStatus = "";
2177
+ this.modsFetched = false;
2178
+ // --- UI Components ---
2179
+ this.SectionHead = ({ label, color }) => /* @__PURE__ */ ReactEcs4.createElement(
2180
+ UiEntity4,
2181
+ {
2182
+ uiTransform: { width: "100%", height: 24, margin: { bottom: 6 }, padding: { left: 8 }, alignItems: "center" },
2183
+ uiBackground: { color: Color45.create(color.r * 0.3, color.g * 0.3, color.b * 0.3, 0.5) }
2184
+ },
2185
+ /* @__PURE__ */ ReactEcs4.createElement(Label4, { value: label, fontSize: 12, color })
2186
+ );
2187
+ this.TabBtn = ({ label, tab }) => /* @__PURE__ */ ReactEcs4.createElement(
2188
+ Button3,
2189
+ {
2190
+ uiTransform: { flexGrow: 1, height: 36, justifyContent: "center", alignItems: "center" },
2191
+ uiBackground: { color: this.activeTab === tab ? C.tabActive : C.tabInactive },
2192
+ value: label,
2193
+ fontSize: 12,
2194
+ color: this.activeTab === tab ? Color45.Black() : C.text,
2195
+ textAlign: "middle-center",
2196
+ onMouseDown: () => this.setActiveTab(tab)
2197
+ }
2198
+ );
2199
+ this.VideoTab = () => {
2200
+ if (!this.streamFetched) {
2201
+ this.fetchStreamData();
2202
+ }
2203
+ return /* @__PURE__ */ ReactEcs4.createElement(UiEntity4, { uiTransform: { flexDirection: "column", width: "100%", padding: 8 } }, !this.streamData?.hasChannel && /* @__PURE__ */ ReactEcs4.createElement(UiEntity4, { uiTransform: { flexDirection: "column", margin: { bottom: 12 } } }, /* @__PURE__ */ ReactEcs4.createElement(this.SectionHead, { label: "LIVE STREAM", color: C.btn }), /* @__PURE__ */ ReactEcs4.createElement(Label4, { value: "No streaming channel linked", fontSize: 10, color: C.textDim, uiTransform: { margin: { bottom: 6 } } }), this.isOwner && /* @__PURE__ */ ReactEcs4.createElement(UiEntity4, { uiTransform: { flexDirection: "column" } }, this.streamData?.trialAvailable && /* @__PURE__ */ ReactEcs4.createElement(UiEntity4, { uiTransform: { flexDirection: "column", margin: { bottom: 8 } } }, /* @__PURE__ */ ReactEcs4.createElement(
2204
+ Button3,
2205
+ {
2206
+ uiTransform: { width: 180, height: 40, margin: { bottom: 4 } },
2207
+ uiBackground: { color: this.trialClaiming ? C.btn : C.green },
2208
+ value: this.trialClaiming ? "Claiming..." : "\u{1F381} Start Free 4-Hour Trial",
2209
+ fontSize: 11,
2210
+ color: C.text,
2211
+ onMouseDown: () => this.claimTrial()
2212
+ }
2213
+ ), this.trialClaimError && /* @__PURE__ */ ReactEcs4.createElement(Label4, { value: this.trialClaimError, fontSize: 9, color: C.red }), /* @__PURE__ */ ReactEcs4.createElement(Label4, { value: "One-time trial \u2022 4 hours of streaming", fontSize: 8, color: C.textDim, uiTransform: { margin: { top: 2 } } })), !this.streamData?.trialAvailable && /* @__PURE__ */ ReactEcs4.createElement(UiEntity4, { uiTransform: { flexDirection: "column" } }, /* @__PURE__ */ ReactEcs4.createElement(
2214
+ Button3,
2215
+ {
2216
+ uiTransform: { width: 150, height: 36, margin: { bottom: 4 } },
2217
+ uiBackground: { color: this.channelCreating ? C.btn : C.cyan },
2218
+ value: this.channelCreating ? "Creating..." : "+ Create Channel",
2219
+ fontSize: 11,
2220
+ color: C.text,
2221
+ onMouseDown: () => this.createChannel()
2222
+ }
2223
+ ), this.channelCreateError && /* @__PURE__ */ ReactEcs4.createElement(Label4, { value: this.channelCreateError, fontSize: 9, color: C.red }), /* @__PURE__ */ ReactEcs4.createElement(Label4, { value: "Relay tier \u2022 $25/mo \u2022 8 hours streaming", fontSize: 8, color: C.textDim, uiTransform: { margin: { top: 2 } } })))), this.streamData?.hasChannel && this.isOwner && /* @__PURE__ */ ReactEcs4.createElement(UiEntity4, { uiTransform: { flexDirection: "column", margin: { bottom: 12 } } }, /* @__PURE__ */ ReactEcs4.createElement(this.SectionHead, { label: this.streamData.isLive ? "\u{1F534} LIVE STREAM" : "STREAM SETTINGS", color: this.streamData.isLive ? C.red : C.yellow }), /* @__PURE__ */ ReactEcs4.createElement(UiEntity4, { uiTransform: { flexDirection: "row", alignItems: "center", margin: { bottom: 6 } } }, /* @__PURE__ */ ReactEcs4.createElement(
2224
+ Label4,
2225
+ {
2226
+ value: this.streamData.isLive ? `LIVE \u2022 ${this.streamData.currentViewers || 0} viewers` : "OFFLINE",
2227
+ fontSize: 11,
2228
+ color: this.streamData.isLive ? C.red : C.textDim
2229
+ }
2230
+ ), /* @__PURE__ */ ReactEcs4.createElement(Label4, { value: ` \u2022 ${this.streamData.tier?.toUpperCase() || "RELAY"}`, fontSize: 10, color: C.cyan }), /* @__PURE__ */ ReactEcs4.createElement(Label4, { value: ` \u2022 ${this.streamData.sparksBalance || 0} Sparks`, fontSize: 10, color: C.textDim })), /* @__PURE__ */ ReactEcs4.createElement(
2231
+ Label4,
2232
+ {
2233
+ value: `Channel: ${this.streamData.channelName || this.streamData.channelId}`,
2234
+ fontSize: 9,
2235
+ color: C.textDim,
2236
+ uiTransform: { margin: { bottom: 8 } }
2237
+ }
2238
+ ), /* @__PURE__ */ ReactEcs4.createElement(UiEntity4, { uiTransform: { flexDirection: "column", margin: { bottom: 4 } } }, /* @__PURE__ */ ReactEcs4.createElement(Label4, { value: "RTMP Server:", fontSize: 9, color: C.textDim }), /* @__PURE__ */ ReactEcs4.createElement(
2239
+ Label4,
2240
+ {
2241
+ value: this.streamData.rtmpUrl || "Loading...",
2242
+ fontSize: 10,
2243
+ color: this.streamData.rtmpUrl ? C.text : C.textDim,
2244
+ uiTransform: { margin: { left: 4 } }
2245
+ }
2246
+ )), /* @__PURE__ */ ReactEcs4.createElement(UiEntity4, { uiTransform: { flexDirection: "column", margin: { bottom: 4 } } }, /* @__PURE__ */ ReactEcs4.createElement(Label4, { value: "Stream Key:", fontSize: 9, color: C.textDim }), /* @__PURE__ */ ReactEcs4.createElement(
2247
+ Label4,
2248
+ {
2249
+ value: this.streamData.streamKey || "Loading...",
2250
+ fontSize: 10,
2251
+ color: this.streamData.streamKey ? C.cyan : C.textDim,
2252
+ uiTransform: { margin: { left: 4 } }
2253
+ }
2254
+ )), /* @__PURE__ */ ReactEcs4.createElement(UiEntity4, { uiTransform: { flexDirection: "column", margin: { bottom: 8 } } }, /* @__PURE__ */ ReactEcs4.createElement(Label4, { value: "HLS Playback:", fontSize: 9, color: C.textDim }), /* @__PURE__ */ ReactEcs4.createElement(
2255
+ Label4,
2256
+ {
2257
+ value: this.streamData.hlsUrl || "Not available",
2258
+ fontSize: 10,
2259
+ color: this.streamData.hlsUrl ? C.green : C.textDim,
2260
+ uiTransform: { margin: { left: 4 } }
2261
+ }
2262
+ )), /* @__PURE__ */ ReactEcs4.createElement(UiEntity4, { uiTransform: { flexDirection: "row", flexWrap: "wrap", margin: { bottom: 4 } } }, !this.streamData.isLive && /* @__PURE__ */ ReactEcs4.createElement(
2263
+ Button3,
2264
+ {
2265
+ uiTransform: { width: 100, height: 32, margin: 3 },
2266
+ uiBackground: { color: this.streamControlling ? C.btn : C.green },
2267
+ value: this.streamControlling ? "Starting..." : "Start Stream",
2268
+ fontSize: 10,
2269
+ color: C.text,
2270
+ onMouseDown: () => this.startStream()
2271
+ }
2272
+ ), this.streamData.isLive && /* @__PURE__ */ ReactEcs4.createElement(
2273
+ Button3,
2274
+ {
2275
+ uiTransform: { width: 100, height: 32, margin: 3 },
2276
+ uiBackground: { color: this.streamControlling ? C.btn : C.red },
2277
+ value: this.streamControlling ? "Stopping..." : "Stop Stream",
2278
+ fontSize: 10,
2279
+ color: C.text,
2280
+ onMouseDown: () => this.stopStream()
2281
+ }
2282
+ ), this.streamData.isLive && this.streamData.hlsUrl && /* @__PURE__ */ ReactEcs4.createElement(
2283
+ Button3,
2284
+ {
2285
+ uiTransform: { width: 90, height: 32, margin: 3 },
2286
+ uiBackground: { color: C.cyan },
2287
+ value: "Play on Screen",
2288
+ fontSize: 9,
2289
+ color: C.text,
2290
+ onMouseDown: () => this.config.onVideoPlay?.(this.streamData.hlsUrl)
2291
+ }
2292
+ )), /* @__PURE__ */ ReactEcs4.createElement(UiEntity4, { uiTransform: { flexDirection: "row", flexWrap: "wrap", margin: { bottom: 4 } } }, /* @__PURE__ */ ReactEcs4.createElement(
2293
+ Button3,
2294
+ {
2295
+ uiTransform: { width: 70, height: 28, margin: 3 },
2296
+ uiBackground: { color: C.btn },
2297
+ value: "Refresh",
2298
+ fontSize: 9,
2299
+ color: C.text,
2300
+ onMouseDown: () => this.refreshStreamStatus()
2301
+ }
2302
+ ), /* @__PURE__ */ ReactEcs4.createElement(
2303
+ Button3,
2304
+ {
2305
+ uiTransform: { width: 85, height: 28, margin: 3 },
2306
+ uiBackground: { color: this.keyRotating ? C.btn : C.yellow },
2307
+ value: this.keyRotating ? "..." : "Rotate Key",
2308
+ fontSize: 9,
2309
+ color: C.text,
2310
+ onMouseDown: () => this.rotateStreamKey()
2311
+ }
2312
+ ), !this.streamData.isLive && /* @__PURE__ */ ReactEcs4.createElement(
2313
+ Button3,
2314
+ {
2315
+ uiTransform: { width: 60, height: 28, margin: 3 },
2316
+ uiBackground: { color: this.channelDeleting ? C.btn : C.red },
2317
+ value: this.channelDeleting ? "..." : "Delete",
2318
+ fontSize: 9,
2319
+ color: C.text,
2320
+ onMouseDown: () => this.deleteChannel()
2321
+ }
2322
+ )), this.streamControlStatus === "started" && /* @__PURE__ */ ReactEcs4.createElement(Label4, { value: "Stream started - begin broadcasting in OBS", fontSize: 9, color: C.green }), this.streamControlStatus === "stopped" && /* @__PURE__ */ ReactEcs4.createElement(Label4, { value: "Stream stopped", fontSize: 9, color: C.textDim }), this.keyRotateStatus === "success" && /* @__PURE__ */ ReactEcs4.createElement(Label4, { value: "Key rotated! Update OBS", fontSize: 9, color: C.green }), this.keyRotateStatus === "error" && /* @__PURE__ */ ReactEcs4.createElement(Label4, { value: "Failed to rotate key", fontSize: 9, color: C.red }), this.channelDeleteError && /* @__PURE__ */ ReactEcs4.createElement(Label4, { value: this.channelDeleteError, fontSize: 9, color: C.red }), this.streamControlStatus === "error" && /* @__PURE__ */ ReactEcs4.createElement(Label4, { value: "Stream control failed", fontSize: 9, color: C.red })), /* @__PURE__ */ ReactEcs4.createElement(this.SectionHead, { label: "PLAY NOW", color: C.orange }), /* @__PURE__ */ ReactEcs4.createElement(UiEntity4, { uiTransform: { flexDirection: "row", alignItems: "center", margin: { bottom: 12 } } }, /* @__PURE__ */ ReactEcs4.createElement(
2323
+ Input4,
2324
+ {
2325
+ uiTransform: { width: 220, height: 32 },
2326
+ uiBackground: { color: Color45.create(0.15, 0.15, 0.2, 1) },
2327
+ placeholder: "Video URL...",
2328
+ placeholderColor: C.textDim,
2329
+ color: C.text,
2330
+ fontSize: 11,
2331
+ value: this.customVideoUrl,
2332
+ onChange: (val) => {
2333
+ this.customVideoUrl = val;
2334
+ }
2335
+ }
2336
+ ), /* @__PURE__ */ ReactEcs4.createElement(
2337
+ Button3,
2338
+ {
2339
+ uiTransform: { width: 70, height: 32, margin: { left: 6 } },
2340
+ uiBackground: { color: C.green },
2341
+ value: "Play",
2342
+ fontSize: 11,
2343
+ color: C.text,
2344
+ onMouseDown: () => {
2345
+ if (this.customVideoUrl) this.config.onVideoPlay?.(this.customVideoUrl);
2346
+ }
2347
+ }
2348
+ ), /* @__PURE__ */ ReactEcs4.createElement(
2349
+ Button3,
2350
+ {
2351
+ uiTransform: { width: 60, height: 32, margin: { left: 4 } },
2352
+ uiBackground: { color: C.btn },
2353
+ value: "Clear",
2354
+ fontSize: 11,
2355
+ color: C.text,
2356
+ onMouseDown: () => {
2357
+ this.customVideoUrl = "";
2358
+ }
2359
+ }
2360
+ )), /* @__PURE__ */ ReactEcs4.createElement(this.SectionHead, { label: "PLAYBACK", color: C.green }), /* @__PURE__ */ ReactEcs4.createElement(UiEntity4, { uiTransform: { flexDirection: "row", flexWrap: "wrap", margin: { bottom: 12 } } }, /* @__PURE__ */ ReactEcs4.createElement(
2361
+ Button3,
2362
+ {
2363
+ uiTransform: { width: 70, height: 32, margin: 3 },
2364
+ uiBackground: { color: C.green },
2365
+ value: "Play",
2366
+ fontSize: 11,
2367
+ color: C.text,
2368
+ onMouseDown: () => this.config.onCommand?.("videoPlay", { playing: true })
2369
+ }
2370
+ ), /* @__PURE__ */ ReactEcs4.createElement(
2371
+ Button3,
2372
+ {
2373
+ uiTransform: { width: 70, height: 32, margin: 3 },
2374
+ uiBackground: { color: C.red },
2375
+ value: "Stop",
2376
+ fontSize: 11,
2377
+ color: C.text,
2378
+ onMouseDown: () => this.config.onVideoStop?.()
2379
+ }
2380
+ ), /* @__PURE__ */ ReactEcs4.createElement(
2381
+ Button3,
2382
+ {
2383
+ uiTransform: { width: 90, height: 32, margin: 3 },
2384
+ uiBackground: { color: C.btn },
2385
+ value: "Reset Default",
2386
+ fontSize: 10,
2387
+ color: C.text,
2388
+ onMouseDown: () => this.config.onCommand?.("videoClear", {})
2389
+ }
2390
+ )), /* @__PURE__ */ ReactEcs4.createElement(this.SectionHead, { label: "VIDEO SLOTS", color: C.cyan }), /* @__PURE__ */ ReactEcs4.createElement(UiEntity4, { uiTransform: { flexDirection: "row", flexWrap: "wrap", margin: { bottom: 8 } } }, /* @__PURE__ */ ReactEcs4.createElement(Button3, { uiTransform: { width: 65, height: 32, margin: 3 }, uiBackground: { color: C.cyan }, value: "Play 1", fontSize: 11, color: C.text, onMouseDown: () => this.config.onVideoSlotPlay?.("slot1") }), /* @__PURE__ */ ReactEcs4.createElement(Button3, { uiTransform: { width: 65, height: 32, margin: 3 }, uiBackground: { color: C.cyan }, value: "Play 2", fontSize: 11, color: C.text, onMouseDown: () => this.config.onVideoSlotPlay?.("slot2") }), /* @__PURE__ */ ReactEcs4.createElement(Button3, { uiTransform: { width: 65, height: 32, margin: 3 }, uiBackground: { color: C.cyan }, value: "Play 3", fontSize: 11, color: C.text, onMouseDown: () => this.config.onVideoSlotPlay?.("slot3") }), /* @__PURE__ */ ReactEcs4.createElement(Button3, { uiTransform: { width: 65, height: 32, margin: 3 }, uiBackground: { color: C.cyan }, value: "Play 4", fontSize: 11, color: C.text, onMouseDown: () => this.config.onVideoSlotPlay?.("slot4") }), /* @__PURE__ */ ReactEcs4.createElement(Button3, { uiTransform: { width: 65, height: 32, margin: 3 }, uiBackground: { color: C.cyan }, value: "Play 5", fontSize: 11, color: C.text, onMouseDown: () => this.config.onVideoSlotPlay?.("slot5") })), /* @__PURE__ */ ReactEcs4.createElement(
2391
+ Button3,
2392
+ {
2393
+ uiTransform: { height: 20 },
2394
+ uiBackground: { color: Color45.create(0, 0, 0, 0) },
2395
+ value: "Edit slots at thestatic.tv \u2192",
2396
+ fontSize: 10,
2397
+ color: C.cyan,
2398
+ onMouseDown: () => openExternalUrl3({ url: this.config.footerLink || `https://thestatic.tv/scene/${this.config.sceneId}` })
2399
+ }
2400
+ ));
2401
+ };
2402
+ this.ModTab = () => /* @__PURE__ */ ReactEcs4.createElement(UiEntity4, { uiTransform: { flexDirection: "column", width: "100%", padding: 8 } }, this.modStatus === "loading" && /* @__PURE__ */ ReactEcs4.createElement(Label4, { value: "Loading...", fontSize: 11, color: C.yellow }), this.modStatus === "saved" && /* @__PURE__ */ ReactEcs4.createElement(Label4, { value: "Saved!", fontSize: 11, color: C.green }), this.modStatus === "error" && /* @__PURE__ */ ReactEcs4.createElement(Label4, { value: "Error - check input", fontSize: 11, color: C.red }), /* @__PURE__ */ ReactEcs4.createElement(this.SectionHead, { label: "BROADCAST", color: C.orange }), /* @__PURE__ */ ReactEcs4.createElement(UiEntity4, { uiTransform: { flexDirection: "row", alignItems: "center", margin: { bottom: 12 } } }, /* @__PURE__ */ ReactEcs4.createElement(
2403
+ Input4,
2404
+ {
2405
+ uiTransform: { width: 200, height: 32 },
2406
+ uiBackground: { color: Color45.create(0.15, 0.15, 0.2, 1) },
2407
+ placeholder: "Message to all players...",
2408
+ placeholderColor: C.textDim,
2409
+ color: C.text,
2410
+ fontSize: 11,
2411
+ value: this.broadcastText,
2412
+ onChange: (val) => {
2413
+ this.broadcastText = val;
2414
+ }
2415
+ }
2416
+ ), /* @__PURE__ */ ReactEcs4.createElement(
2417
+ Button3,
2418
+ {
2419
+ uiTransform: { width: 55, height: 32, margin: { left: 6 } },
2420
+ uiBackground: { color: C.orange },
2421
+ value: "Send",
2422
+ fontSize: 11,
2423
+ color: C.text,
2424
+ onMouseDown: () => this.sendBroadcast()
2425
+ }
2426
+ )), /* @__PURE__ */ ReactEcs4.createElement(this.SectionHead, { label: "CHAOS MODE", color: C.red }), /* @__PURE__ */ ReactEcs4.createElement(UiEntity4, { uiTransform: { flexDirection: "row", flexWrap: "wrap", margin: { bottom: 12 } } }, /* @__PURE__ */ ReactEcs4.createElement(
2427
+ Button3,
2428
+ {
2429
+ uiTransform: { width: 120, height: 32, margin: 3 },
2430
+ uiBackground: { color: C.red },
2431
+ value: "KICK ALL",
2432
+ fontSize: 11,
2433
+ color: C.text,
2434
+ onMouseDown: () => this.config.onCommand?.("kickAll", {})
2435
+ }
2436
+ )), /* @__PURE__ */ ReactEcs4.createElement(this.SectionHead, { label: "SCENE ADMINS", color: C.purple }), /* @__PURE__ */ ReactEcs4.createElement(UiEntity4, { uiTransform: { flexDirection: "column", margin: { bottom: 4 } } }, this.sceneAdmins.length === 0 && this.modStatus !== "loading" && /* @__PURE__ */ ReactEcs4.createElement(Label4, { value: "No scene admins", fontSize: 10, color: C.textDim }), this.sceneAdmins.map((wallet, i) => /* @__PURE__ */ ReactEcs4.createElement(
2437
+ UiEntity4,
2438
+ {
2439
+ key: `admin-${i}`,
2440
+ uiTransform: { flexDirection: "row", alignItems: "center", margin: { bottom: 3 }, width: "100%" }
2441
+ },
2442
+ /* @__PURE__ */ ReactEcs4.createElement(
2443
+ Label4,
2444
+ {
2445
+ value: `${wallet.slice(0, 6)}...${wallet.slice(-4)}`,
2446
+ fontSize: 10,
2447
+ color: C.text,
2448
+ uiTransform: { width: 110 }
2449
+ }
2450
+ ),
2451
+ /* @__PURE__ */ ReactEcs4.createElement(
2452
+ Button3,
2453
+ {
2454
+ uiTransform: { width: 55, height: 24, margin: { left: 6 } },
2455
+ uiBackground: { color: C.btn },
2456
+ value: "Remove",
2457
+ fontSize: 9,
2458
+ color: C.text,
2459
+ onMouseDown: () => this.removeSceneAdmin(wallet)
2460
+ }
2461
+ )
2462
+ ))), /* @__PURE__ */ ReactEcs4.createElement(UiEntity4, { uiTransform: { flexDirection: "row", alignItems: "center", margin: { bottom: 12 } } }, /* @__PURE__ */ ReactEcs4.createElement(
2463
+ Input4,
2464
+ {
2465
+ uiTransform: { width: 200, height: 28 },
2466
+ uiBackground: { color: Color45.create(0.15, 0.15, 0.2, 1) },
2467
+ placeholder: "0x... add admin",
2468
+ placeholderColor: C.textDim,
2469
+ color: C.text,
2470
+ fontSize: 10,
2471
+ value: this.newAdminWallet,
2472
+ onChange: (val) => {
2473
+ this.newAdminWallet = val;
2474
+ }
2475
+ }
2476
+ ), /* @__PURE__ */ ReactEcs4.createElement(
2477
+ Button3,
2478
+ {
2479
+ uiTransform: { width: 50, height: 28, margin: { left: 6 } },
2480
+ uiBackground: { color: C.purple },
2481
+ value: "Add",
2482
+ fontSize: 10,
2483
+ color: C.text,
2484
+ onMouseDown: () => {
2485
+ if (this.newAdminWallet) this.addSceneAdmin(this.newAdminWallet);
2486
+ }
2487
+ }
2488
+ )), /* @__PURE__ */ ReactEcs4.createElement(this.SectionHead, { label: "BANNED WALLETS", color: C.red }), /* @__PURE__ */ ReactEcs4.createElement(UiEntity4, { uiTransform: { flexDirection: "column", margin: { bottom: 4 } } }, this.bannedWallets.length === 0 && this.modStatus !== "loading" && /* @__PURE__ */ ReactEcs4.createElement(Label4, { value: "No banned wallets", fontSize: 10, color: C.textDim }), this.bannedWallets.map((wallet, i) => /* @__PURE__ */ ReactEcs4.createElement(
2489
+ UiEntity4,
2490
+ {
2491
+ key: `ban-${i}`,
2492
+ uiTransform: { flexDirection: "row", alignItems: "center", margin: { bottom: 3 }, width: "100%" }
2493
+ },
2494
+ /* @__PURE__ */ ReactEcs4.createElement(
2495
+ Label4,
2496
+ {
2497
+ value: `${wallet.slice(0, 6)}...${wallet.slice(-4)}`,
2498
+ fontSize: 10,
2499
+ color: C.red,
2500
+ uiTransform: { width: 110 }
2501
+ }
2502
+ ),
2503
+ /* @__PURE__ */ ReactEcs4.createElement(
2504
+ Button3,
2505
+ {
2506
+ uiTransform: { width: 55, height: 24, margin: { left: 6 } },
2507
+ uiBackground: { color: C.green },
2508
+ value: "Unban",
2509
+ fontSize: 9,
2510
+ color: C.text,
2511
+ onMouseDown: () => this.unbanWallet(wallet)
2512
+ }
2513
+ )
2514
+ ))), /* @__PURE__ */ ReactEcs4.createElement(UiEntity4, { uiTransform: { flexDirection: "row", alignItems: "center", margin: { bottom: 8 } } }, /* @__PURE__ */ ReactEcs4.createElement(
2515
+ Input4,
2516
+ {
2517
+ uiTransform: { width: 200, height: 28 },
2518
+ uiBackground: { color: Color45.create(0.15, 0.15, 0.2, 1) },
2519
+ placeholder: "0x... ban wallet",
2520
+ placeholderColor: C.textDim,
2521
+ color: C.text,
2522
+ fontSize: 10,
2523
+ value: this.newBanWallet,
2524
+ onChange: (val) => {
2525
+ this.newBanWallet = val;
2526
+ }
2527
+ }
2528
+ ), /* @__PURE__ */ ReactEcs4.createElement(
2529
+ Button3,
2530
+ {
2531
+ uiTransform: { width: 50, height: 28, margin: { left: 6 } },
2532
+ uiBackground: { color: C.red },
2533
+ value: "Ban",
2534
+ fontSize: 10,
2535
+ color: C.text,
2536
+ onMouseDown: () => {
2537
+ if (this.newBanWallet) this.banWallet(this.newBanWallet);
2538
+ }
2539
+ }
2540
+ )), /* @__PURE__ */ ReactEcs4.createElement(
2541
+ Button3,
2542
+ {
2543
+ uiTransform: { height: 20 },
2544
+ uiBackground: { color: Color45.create(0, 0, 0, 0) },
2545
+ value: "Manage at thestatic.tv \u2192",
2546
+ fontSize: 9,
2547
+ color: C.cyan,
2548
+ onMouseDown: () => openExternalUrl3({ url: this.config.footerLink || `https://thestatic.tv/scene/${this.config.sceneId}` })
2549
+ }
2550
+ ));
2551
+ /**
2552
+ * Get the React-ECS component for the admin panel
2553
+ */
2554
+ this.getComponent = () => {
2555
+ if (!this.isAdmin) return null;
2556
+ const tabs = [];
2557
+ if (this.config.sceneTabs && this.config.sceneTabs.length > 0) {
2558
+ this.config.sceneTabs.forEach((tab) => tabs.push({ label: tab.label, id: tab.id }));
2559
+ }
2560
+ if (this.config.showVideoTab !== false) {
2561
+ tabs.push({ label: "VIDEO", id: "video" });
2562
+ }
2563
+ if (this.config.showModTab !== false && this.isOwner) {
2564
+ tabs.push({ label: "MOD", id: "mod" });
2565
+ }
2566
+ if (!tabs.find((t) => t.id === this.activeTab) && tabs.length > 0) {
2567
+ this.activeTab = tabs[0].id;
2568
+ }
2569
+ return /* @__PURE__ */ ReactEcs4.createElement(
2570
+ UiEntity4,
2571
+ {
2572
+ uiTransform: {
2573
+ width: "100%",
2574
+ height: "100%",
2575
+ positionType: "absolute"
2576
+ }
2577
+ },
2578
+ /* @__PURE__ */ ReactEcs4.createElement(
2579
+ UiEntity4,
2580
+ {
2581
+ uiTransform: {
2582
+ position: { right: 16, bottom: 16 },
2583
+ positionType: "absolute"
2584
+ }
2585
+ },
2586
+ /* @__PURE__ */ ReactEcs4.createElement(
2587
+ Button3,
2588
+ {
2589
+ uiTransform: { width: 90, height: 36 },
2590
+ uiBackground: { color: this.panelOpen ? C.btn : C.header },
2591
+ value: this.panelOpen ? "CLOSE" : "ADMIN",
2592
+ fontSize: 13,
2593
+ color: C.text,
2594
+ onMouseDown: () => this.toggle()
2595
+ }
2596
+ )
2597
+ ),
2598
+ this.panelOpen && /* @__PURE__ */ ReactEcs4.createElement(
2599
+ UiEntity4,
2600
+ {
2601
+ uiTransform: {
2602
+ width: 380,
2603
+ maxHeight: 800,
2604
+ position: { right: 16, bottom: 60 },
2605
+ positionType: "absolute",
2606
+ flexDirection: "column"
2607
+ },
2608
+ uiBackground: { color: C.bg }
2609
+ },
2610
+ /* @__PURE__ */ ReactEcs4.createElement(
2611
+ UiEntity4,
2612
+ {
2613
+ uiTransform: {
2614
+ width: "100%",
2615
+ height: 44,
2616
+ justifyContent: "center",
2617
+ alignItems: "center",
2618
+ flexDirection: "row"
2619
+ },
2620
+ uiBackground: { color: C.header }
2621
+ },
2622
+ /* @__PURE__ */ ReactEcs4.createElement(Label4, { value: this.config.title || "ADMIN PANEL", fontSize: 15, color: C.text })
2623
+ ),
2624
+ /* @__PURE__ */ ReactEcs4.createElement(UiEntity4, { uiTransform: { width: "100%", height: 36, flexDirection: "row" } }, tabs.map((tab) => /* @__PURE__ */ ReactEcs4.createElement(this.TabBtn, { label: tab.label, tab: tab.id }))),
2625
+ /* @__PURE__ */ ReactEcs4.createElement(
2626
+ UiEntity4,
2627
+ {
2628
+ uiTransform: {
2629
+ width: "100%",
2630
+ flexGrow: 1,
2631
+ overflow: "scroll",
2632
+ flexDirection: "column"
2633
+ }
2634
+ },
2635
+ this.activeTab === "video" && /* @__PURE__ */ ReactEcs4.createElement(this.VideoTab, null),
2636
+ this.activeTab === "mod" && this.isOwner && /* @__PURE__ */ ReactEcs4.createElement(this.ModTab, null),
2637
+ this.config.sceneTabs?.map((tab) => this.activeTab === tab.id && tab.render())
2638
+ ),
2639
+ /* @__PURE__ */ ReactEcs4.createElement(
2640
+ UiEntity4,
2641
+ {
2642
+ uiTransform: {
2643
+ width: "100%",
2644
+ height: 28,
2645
+ justifyContent: "center",
2646
+ alignItems: "center"
2647
+ },
2648
+ uiBackground: { color: C.section }
2649
+ },
2650
+ /* @__PURE__ */ ReactEcs4.createElement(
2651
+ Button3,
2652
+ {
2653
+ uiTransform: { height: 20 },
2654
+ uiBackground: { color: Color45.create(0, 0, 0, 0) },
2655
+ value: `thestatic.tv/scene/${this.config.sceneId} \u2192`,
2656
+ fontSize: 10,
2657
+ color: C.cyan,
2658
+ onMouseDown: () => openExternalUrl3({ url: this.config.footerLink || `https://thestatic.tv/scene/${this.config.sceneId}` })
2659
+ }
2660
+ )
2661
+ )
2662
+ )
2663
+ );
2664
+ };
2665
+ this.client = client;
2666
+ this.config = {
2667
+ showVideoTab: true,
2668
+ showModTab: true,
2669
+ title: "ADMIN PANEL",
2670
+ debug: false,
2671
+ ...config
2672
+ };
2673
+ this.baseUrl = client.baseUrl || "https://thestatic.tv/api/v1/dcl";
2674
+ if (config.headerColor) {
2675
+ C.header = Color45.create(
2676
+ config.headerColor.r,
2677
+ config.headerColor.g,
2678
+ config.headerColor.b,
2679
+ config.headerColor.a
2680
+ );
2681
+ }
2682
+ }
2683
+ log(msg, ...args) {
2684
+ if (this.config.debug) {
2685
+ console.log(`[AdminPanel] ${msg}`, ...args);
2686
+ }
2687
+ }
2688
+ /**
2689
+ * Initialize the admin panel - checks admin status
2690
+ */
2691
+ async init() {
2692
+ await new Promise((resolve) => setTimeout(resolve, 3e3));
2693
+ await this.checkAdminStatus();
2694
+ this.log("Initialized");
2695
+ }
2696
+ /**
2697
+ * Check if current player is an admin for this scene
2698
+ */
2699
+ async checkAdminStatus() {
2700
+ const player = getPlayer2();
2701
+ if (!player?.userId) {
2702
+ this.log("No player data yet");
2703
+ return;
2704
+ }
2705
+ this.playerWallet = player.userId;
2706
+ try {
2707
+ const res = await fetch(
2708
+ `${this.baseUrl}/scene/${this.config.sceneId}/admin-check?wallet=${player.userId}`
2709
+ );
2710
+ if (res.ok) {
2711
+ const data = await res.json();
2712
+ this.isAdmin = data.hasAccess;
2713
+ this.isOwner = data.isOwner || data.isAdmin || data.isSceneAdmin;
2714
+ if (data.isBanned) {
2715
+ this.config.onBroadcast?.("You have been banned from this scene.");
2716
+ setTimeout(() => this.banKickPlayer(), 2e3);
2717
+ this.log("Player is banned - kicking");
2718
+ }
2719
+ this.log("Admin status:", this.isAdmin, "Owner:", this.isOwner);
2720
+ }
2721
+ } catch (err) {
2722
+ this.log("Admin check error:", err);
2723
+ }
2724
+ }
2725
+ /**
2726
+ * Toggle the admin panel open/closed
2727
+ */
2728
+ toggle() {
2729
+ this.panelOpen = !this.panelOpen;
2730
+ if (!this.panelOpen) {
2731
+ this.stopStreamPolling();
2732
+ } else if (this.activeTab === "video") {
2733
+ this.startStreamPolling();
2734
+ }
2735
+ }
2736
+ /**
2737
+ * Check if the panel is currently open
2738
+ */
2739
+ get isOpen() {
2740
+ return this.panelOpen;
2741
+ }
2742
+ /**
2743
+ * Check if current user has admin access
2744
+ */
2745
+ get hasAccess() {
2746
+ return this.isAdmin;
2747
+ }
2748
+ /**
2749
+ * Register a custom scene tab (Full/Custom tier)
2750
+ */
2751
+ registerSceneTab(tab) {
2752
+ if (!this.config.sceneTabs) {
2753
+ this.config.sceneTabs = [];
2754
+ }
2755
+ this.config.sceneTabs.push(tab);
2756
+ this.log("Registered scene tab:", tab.label);
2757
+ }
2758
+ // --- Stream Polling ---
2759
+ startStreamPolling() {
2760
+ if (this.pollIntervalId !== null) return;
2761
+ this.pollIntervalId = setInterval(() => {
2762
+ if (this.activeTab === "video" && this.panelOpen && this.streamData?.hasChannel) {
2763
+ this.refreshStreamStatus();
2764
+ }
2765
+ }, 1e4);
2766
+ this.log("Stream polling started");
2767
+ }
2768
+ stopStreamPolling() {
2769
+ if (this.pollIntervalId !== null) {
2770
+ clearInterval(this.pollIntervalId);
2771
+ this.pollIntervalId = null;
2772
+ this.log("Stream polling stopped");
2773
+ }
2774
+ }
2775
+ // --- Stream API Calls ---
2776
+ async fetchStreamData() {
2777
+ if (this.streamFetched || !this.playerWallet) return;
2778
+ try {
2779
+ const res = await fetch(
2780
+ `${this.baseUrl}/scene/${this.config.sceneId}/stream?wallet=${this.playerWallet}`
2781
+ );
2782
+ if (res.ok) {
2783
+ this.streamData = await res.json();
2784
+ this.streamFetched = true;
2785
+ this.log("Stream data fetched:", this.streamData?.channelId || "no channel");
2786
+ }
2787
+ } catch (err) {
2788
+ this.log("Stream fetch error:", err);
2789
+ }
2790
+ }
2791
+ async refreshStreamStatus() {
2792
+ if (!this.playerWallet) return;
2793
+ try {
2794
+ const res = await fetch(
2795
+ `${this.baseUrl}/scene/${this.config.sceneId}/stream?wallet=${this.playerWallet}`
2796
+ );
2797
+ if (res.ok) {
2798
+ this.streamData = await res.json();
2799
+ }
2800
+ } catch (err) {
2801
+ }
2802
+ }
2803
+ async createChannel() {
2804
+ if (!this.playerWallet || this.channelCreating) return;
2805
+ this.channelCreating = true;
2806
+ this.channelCreateError = "";
2807
+ try {
2808
+ this.log("Creating channel for scene...");
2809
+ const res = await fetch(
2810
+ `${this.baseUrl}/scene/${this.config.sceneId}/stream/create`,
2811
+ {
2812
+ method: "POST",
2813
+ headers: { "Content-Type": "application/json" },
2814
+ body: JSON.stringify({ wallet: this.playerWallet })
2815
+ }
2816
+ );
2817
+ const data = await res.json();
2818
+ if (res.ok) {
2819
+ this.log("Channel created:", data.channel);
2820
+ this.streamFetched = false;
2821
+ await this.fetchStreamData();
2822
+ this.config.onBroadcast?.(`Channel created: ${data.channel.channelId}`);
2823
+ } else {
2824
+ this.channelCreateError = data.error || "Failed to create channel";
2825
+ this.log("Channel creation failed:", data.error);
2826
+ }
2827
+ } catch (err) {
2828
+ this.channelCreateError = "Network error creating channel";
2829
+ this.log("Channel creation error:", err);
2830
+ }
2831
+ this.channelCreating = false;
2832
+ }
2833
+ async claimTrial() {
2834
+ if (!this.playerWallet || this.trialClaiming) return;
2835
+ this.trialClaiming = true;
2836
+ this.trialClaimError = "";
2837
+ try {
2838
+ this.log("Claiming streaming trial...");
2839
+ const res = await fetch(
2840
+ `${this.baseUrl}/scene/${this.config.sceneId}/stream/claim-trial`,
2841
+ {
2842
+ method: "POST",
2843
+ headers: { "Content-Type": "application/json" },
2844
+ body: JSON.stringify({ wallet: this.playerWallet })
2845
+ }
2846
+ );
2847
+ const data = await res.json();
2848
+ if (res.ok) {
2849
+ this.log("Trial claimed:", data.channel);
2850
+ this.streamFetched = false;
2851
+ await this.fetchStreamData();
2852
+ this.config.onBroadcast?.(`4-hour streaming trial activated!`);
2853
+ } else {
2854
+ this.trialClaimError = data.error || "Failed to claim trial";
2855
+ this.log("Trial claim failed:", data.error);
2856
+ }
2857
+ } catch (err) {
2858
+ this.trialClaimError = "Network error claiming trial";
2859
+ this.log("Trial claim error:", err);
2860
+ }
2861
+ this.trialClaiming = false;
2862
+ }
2863
+ async deleteChannel() {
2864
+ if (!this.playerWallet || this.channelDeleting || !this.streamData?.hasChannel) return;
2865
+ this.channelDeleting = true;
2866
+ this.channelDeleteError = "";
2867
+ try {
2868
+ this.log("Deleting channel for scene...");
2869
+ const res = await fetch(
2870
+ `${this.baseUrl}/scene/${this.config.sceneId}/stream/delete`,
2871
+ {
2872
+ method: "POST",
2873
+ headers: { "Content-Type": "application/json" },
2874
+ body: JSON.stringify({ wallet: this.playerWallet })
2875
+ }
2876
+ );
2877
+ const data = await res.json();
2878
+ if (res.ok) {
2879
+ this.log("Channel deleted:", data);
2880
+ this.streamData = null;
2881
+ this.streamFetched = false;
2882
+ this.config.onBroadcast?.("Channel deleted successfully");
2883
+ } else {
2884
+ this.channelDeleteError = data.error || "Failed to delete channel";
2885
+ this.log("Channel deletion failed:", data.error);
2886
+ }
2887
+ } catch (err) {
2888
+ this.channelDeleteError = "Network error deleting channel";
2889
+ this.log("Channel deletion error:", err);
2890
+ }
2891
+ this.channelDeleting = false;
2892
+ }
2893
+ async startStream() {
2894
+ if (!this.playerWallet || this.streamControlling || !this.streamData?.hasChannel || this.streamData.isLive) return;
2895
+ if ((this.streamData.sparksBalance || 0) <= 0) {
2896
+ this.streamControlStatus = "error";
2897
+ this.config.onBroadcast?.("No Sparks - cannot start stream");
2898
+ return;
2899
+ }
2900
+ this.streamControlling = true;
2901
+ this.streamControlStatus = "";
2902
+ try {
2903
+ this.log("Starting stream...");
2904
+ const res = await fetch(
2905
+ `${this.baseUrl}/scene/${this.config.sceneId}/stream`,
2906
+ {
2907
+ method: "POST",
2908
+ headers: { "Content-Type": "application/json" },
2909
+ body: JSON.stringify({ action: "start", wallet: this.playerWallet })
2910
+ }
2911
+ );
2912
+ const data = await res.json();
2913
+ if (res.ok) {
2914
+ this.streamControlStatus = "started";
2915
+ this.log("Stream started");
2916
+ this.config.onBroadcast?.("Stream started!");
2917
+ this.streamFetched = false;
2918
+ await this.fetchStreamData();
2919
+ setTimeout(() => {
2920
+ this.streamControlStatus = "";
2921
+ }, 3e3);
2922
+ } else {
2923
+ this.streamControlStatus = "error";
2924
+ this.log("Start stream failed:", data.error);
2925
+ this.config.onBroadcast?.(data.error || "Failed to start");
2926
+ }
2927
+ } catch (err) {
2928
+ this.streamControlStatus = "error";
2929
+ this.log("Start stream error:", err);
2930
+ }
2931
+ this.streamControlling = false;
2932
+ }
2933
+ async stopStream() {
2934
+ if (!this.playerWallet || this.streamControlling || !this.streamData?.hasChannel || !this.streamData.isLive) return;
2935
+ this.streamControlling = true;
2936
+ this.streamControlStatus = "";
2937
+ try {
2938
+ this.log("Stopping stream...");
2939
+ const res = await fetch(
2940
+ `${this.baseUrl}/scene/${this.config.sceneId}/stream`,
2941
+ {
2942
+ method: "POST",
2943
+ headers: { "Content-Type": "application/json" },
2944
+ body: JSON.stringify({ action: "stop", wallet: this.playerWallet })
2945
+ }
2946
+ );
2947
+ const data = await res.json();
2948
+ if (res.ok) {
2949
+ this.streamControlStatus = "stopped";
2950
+ this.log("Stream stopped");
2951
+ this.config.onBroadcast?.("Stream stopped");
2952
+ this.streamFetched = false;
2953
+ await this.fetchStreamData();
2954
+ setTimeout(() => {
2955
+ this.streamControlStatus = "";
2956
+ }, 3e3);
2957
+ } else {
2958
+ this.streamControlStatus = "error";
2959
+ this.log("Stop stream failed:", data.error);
2960
+ }
2961
+ } catch (err) {
2962
+ this.streamControlStatus = "error";
2963
+ this.log("Stop stream error:", err);
2964
+ }
2965
+ this.streamControlling = false;
2966
+ }
2967
+ async rotateStreamKey() {
2968
+ if (!this.playerWallet || this.keyRotating || !this.streamData?.hasChannel) return;
2969
+ this.keyRotating = true;
2970
+ this.keyRotateStatus = "";
2971
+ try {
2972
+ this.log("Rotating stream key...");
2973
+ const res = await fetch(
2974
+ `${this.baseUrl}/scene/${this.config.sceneId}/stream`,
2975
+ {
2976
+ method: "POST",
2977
+ headers: { "Content-Type": "application/json" },
2978
+ body: JSON.stringify({ action: "rotateKey", wallet: this.playerWallet })
2979
+ }
2980
+ );
2981
+ const data = await res.json();
2982
+ if (res.ok) {
2983
+ this.keyRotateStatus = "success";
2984
+ this.log("Key rotated:", data.message);
2985
+ this.config.onBroadcast?.("Stream key rotated - update OBS settings");
2986
+ setTimeout(() => {
2987
+ this.keyRotateStatus = "";
2988
+ }, 3e3);
2989
+ } else {
2990
+ this.keyRotateStatus = "error";
2991
+ this.log("Key rotation failed:", data.error);
2992
+ }
2993
+ } catch (err) {
2994
+ this.keyRotateStatus = "error";
2995
+ this.log("Key rotation error:", err);
2996
+ }
2997
+ this.keyRotating = false;
2998
+ }
2999
+ // --- Mod Tab API Calls ---
3000
+ async fetchModData() {
3001
+ if (!this.playerWallet) return;
3002
+ this.modStatus = "loading";
3003
+ try {
3004
+ const res = await fetch(
3005
+ `${this.baseUrl}/scene/${this.config.sceneId}/config?wallet=${this.playerWallet}`,
3006
+ { headers: { "Content-Type": "application/json" } }
3007
+ );
3008
+ if (res.ok) {
3009
+ const data = await res.json();
3010
+ this.sceneAdmins = data.sceneAdmins || [];
3011
+ this.bannedWallets = data.bannedWallets || [];
3012
+ this.modsFetched = true;
3013
+ this.modStatus = "";
3014
+ this.log("Fetched mod data:", { sceneAdmins: this.sceneAdmins, bannedWallets: this.bannedWallets });
3015
+ } else {
3016
+ this.modStatus = "error";
3017
+ }
3018
+ } catch (err) {
3019
+ this.modStatus = "error";
3020
+ this.log("Fetch mod data error:", err);
3021
+ }
3022
+ }
3023
+ async addSceneAdmin(wallet) {
3024
+ if (!wallet || !this.playerWallet) return;
3025
+ const normalized = wallet.toLowerCase().trim();
3026
+ if (!/^0x[a-f0-9]{40}$/i.test(normalized)) {
3027
+ this.modStatus = "error";
3028
+ return;
3029
+ }
3030
+ if (this.sceneAdmins.includes(normalized)) return;
3031
+ this.modStatus = "loading";
3032
+ try {
3033
+ const res = await fetch(
3034
+ `${this.baseUrl}/scene/${this.config.sceneId}/config`,
3035
+ {
3036
+ method: "POST",
3037
+ headers: { "Content-Type": "application/json" },
3038
+ body: JSON.stringify({
3039
+ sceneAdmins: [...this.sceneAdmins, normalized],
3040
+ wallet: this.playerWallet
3041
+ })
3042
+ }
3043
+ );
3044
+ if (res.ok) {
3045
+ this.sceneAdmins = [...this.sceneAdmins, normalized];
3046
+ this.newAdminWallet = "";
3047
+ this.modStatus = "saved";
3048
+ setTimeout(() => {
3049
+ this.modStatus = "";
3050
+ }, 2e3);
3051
+ this.log("Added scene admin:", normalized);
3052
+ } else {
3053
+ this.modStatus = "error";
3054
+ }
3055
+ } catch (err) {
3056
+ this.modStatus = "error";
3057
+ this.log("Add admin error:", err);
3058
+ }
3059
+ }
3060
+ async removeSceneAdmin(wallet) {
3061
+ if (!wallet || !this.playerWallet) return;
3062
+ this.modStatus = "loading";
3063
+ try {
3064
+ const res = await fetch(
3065
+ `${this.baseUrl}/scene/${this.config.sceneId}/config`,
3066
+ {
3067
+ method: "POST",
3068
+ headers: { "Content-Type": "application/json" },
3069
+ body: JSON.stringify({
3070
+ sceneAdmins: this.sceneAdmins.filter((w) => w !== wallet),
3071
+ wallet: this.playerWallet
3072
+ })
3073
+ }
3074
+ );
3075
+ if (res.ok) {
3076
+ this.sceneAdmins = this.sceneAdmins.filter((w) => w !== wallet);
3077
+ this.modStatus = "saved";
3078
+ setTimeout(() => {
3079
+ this.modStatus = "";
3080
+ }, 2e3);
3081
+ this.log("Removed scene admin:", wallet);
3082
+ } else {
3083
+ this.modStatus = "error";
3084
+ }
3085
+ } catch (err) {
3086
+ this.modStatus = "error";
3087
+ this.log("Remove admin error:", err);
3088
+ }
3089
+ }
3090
+ async banWallet(wallet) {
3091
+ if (!wallet || !this.playerWallet) return;
3092
+ const normalized = wallet.toLowerCase().trim();
3093
+ if (!/^0x[a-f0-9]{40}$/i.test(normalized)) {
3094
+ this.modStatus = "error";
3095
+ return;
3096
+ }
3097
+ if (this.bannedWallets.includes(normalized)) return;
3098
+ this.modStatus = "loading";
3099
+ try {
3100
+ const res = await fetch(
3101
+ `${this.baseUrl}/scene/${this.config.sceneId}/config`,
3102
+ {
3103
+ method: "POST",
3104
+ headers: { "Content-Type": "application/json" },
3105
+ body: JSON.stringify({
3106
+ bannedWallets: [...this.bannedWallets, normalized],
3107
+ wallet: this.playerWallet
3108
+ })
3109
+ }
3110
+ );
3111
+ if (res.ok) {
3112
+ this.bannedWallets = [...this.bannedWallets, normalized];
3113
+ this.newBanWallet = "";
3114
+ this.modStatus = "saved";
3115
+ setTimeout(() => {
3116
+ this.modStatus = "";
3117
+ }, 2e3);
3118
+ this.log("Banned wallet:", normalized);
3119
+ this.config.onCommand?.("kickBanned", { wallet: normalized });
3120
+ } else {
3121
+ this.modStatus = "error";
3122
+ }
3123
+ } catch (err) {
3124
+ this.modStatus = "error";
3125
+ this.log("Ban error:", err);
3126
+ }
3127
+ }
3128
+ async unbanWallet(wallet) {
3129
+ if (!wallet || !this.playerWallet) return;
3130
+ this.modStatus = "loading";
3131
+ try {
3132
+ const res = await fetch(
3133
+ `${this.baseUrl}/scene/${this.config.sceneId}/config`,
3134
+ {
3135
+ method: "POST",
3136
+ headers: { "Content-Type": "application/json" },
3137
+ body: JSON.stringify({
3138
+ bannedWallets: this.bannedWallets.filter((w) => w !== wallet),
3139
+ wallet: this.playerWallet
3140
+ })
3141
+ }
3142
+ );
3143
+ if (res.ok) {
3144
+ this.bannedWallets = this.bannedWallets.filter((w) => w !== wallet);
3145
+ this.modStatus = "saved";
3146
+ setTimeout(() => {
3147
+ this.modStatus = "";
3148
+ }, 2e3);
3149
+ this.log("Unbanned wallet:", wallet);
3150
+ } else {
3151
+ this.modStatus = "error";
3152
+ }
3153
+ } catch (err) {
3154
+ this.modStatus = "error";
3155
+ this.log("Unban error:", err);
3156
+ }
3157
+ }
3158
+ banKickPlayer() {
3159
+ movePlayerTo({ newRelativePosition: BAN_KICK_POSITION });
3160
+ this.log("Player ban-kicked");
3161
+ }
3162
+ sendBroadcast() {
3163
+ if (!this.broadcastText.trim()) return;
3164
+ this.config.onBroadcast?.(this.broadcastText.trim());
3165
+ this.broadcastText = "";
3166
+ this.log("Broadcast sent");
3167
+ }
3168
+ setActiveTab(tab) {
3169
+ const previousTab = this.activeTab;
3170
+ this.activeTab = tab;
3171
+ if (tab === "mod" && !this.modsFetched && this.isOwner) {
3172
+ this.fetchModData();
3173
+ }
3174
+ if (tab === "video" && previousTab !== "video") {
3175
+ this.startStreamPolling();
3176
+ } else if (tab !== "video" && previousTab === "video") {
3177
+ this.stopStreamPolling();
3178
+ }
3179
+ }
3180
+ };
3181
+
2124
3182
  // src/StaticTVClient.ts
2125
3183
  var DEFAULT_BASE_URL = "https://thestatic.tv/api/v1/dcl";
2126
3184
  var KEY_TYPE_CHANNEL = "channel";
@@ -2133,22 +3191,22 @@ var StaticTVClient = class {
2133
3191
  *
2134
3192
  * @example
2135
3193
  * ```typescript
2136
- * // Full access with channel key
2137
- * const staticTV = new StaticTVClient({
2138
- * apiKey: 'dclk_your_channel_key_here',
2139
- * debug: true
2140
- * });
3194
+ * let staticTV: StaticTVClient
2141
3195
  *
2142
- * // Lite mode with scene key (visitors only)
2143
- * const staticTV = new StaticTVClient({
2144
- * apiKey: 'dcls_your_scene_key_here'
2145
- * });
3196
+ * export function main() {
3197
+ * // All keys use dcls_ prefix - features determined by subscription
3198
+ * staticTV = new StaticTVClient({
3199
+ * apiKey: 'dcls_your_key_here'
3200
+ * })
3201
+ * // Session tracking starts automatically!
3202
+ * }
2146
3203
  * ```
2147
3204
  */
2148
3205
  constructor(config) {
2149
3206
  this._keyType = null;
2150
3207
  this._disabled = false;
2151
3208
  this._fullFeaturesEnabled = false;
3209
+ this._proFeaturesEnabled = false;
2152
3210
  /** Guide module - fetch channel lineup (full SDK only) */
2153
3211
  this.guide = null;
2154
3212
  /** Session module - track visitor sessions (all keys, null when disabled) */
@@ -2161,6 +3219,8 @@ var StaticTVClient = class {
2161
3219
  this.guideUI = null;
2162
3220
  /** Chat UI module - real-time chat UI (full SDK only) */
2163
3221
  this.chatUI = null;
3222
+ /** Admin Panel module - Video/Mod tabs (pro SDK only) */
3223
+ this.adminPanel = null;
2164
3224
  this.config = {
2165
3225
  autoStartSession: true,
2166
3226
  sessionHeartbeatInterval: 3e4,
@@ -2290,12 +3350,72 @@ var StaticTVClient = class {
2290
3350
  get hasFullFeatures() {
2291
3351
  return this._fullFeaturesEnabled;
2292
3352
  }
3353
+ /**
3354
+ * Check if pro features are enabled (admin panel)
3355
+ */
3356
+ get hasProFeatures() {
3357
+ return this._proFeaturesEnabled;
3358
+ }
2293
3359
  /**
2294
3360
  * Get the SDK type (lite or full) - only available after session starts
2295
3361
  */
2296
3362
  get sdkType() {
2297
3363
  return this.session?.sdkType || "lite";
2298
3364
  }
3365
+ /**
3366
+ * Enable Pro features (Admin Panel with Video + Mod tabs)
3367
+ * Call this after creating the client to add admin panel functionality.
3368
+ *
3369
+ * @param config Admin panel configuration
3370
+ *
3371
+ * @example
3372
+ * ```typescript
3373
+ * const staticTV = new StaticTVClient({ apiKey: 'dcls_...' })
3374
+ *
3375
+ * // Enable admin panel
3376
+ * staticTV.enableProFeatures({
3377
+ * sceneId: 'my-scene',
3378
+ * title: 'MY SCENE ADMIN',
3379
+ * onVideoPlay: (url) => videoPlayer.play(url),
3380
+ * onVideoStop: () => videoPlayer.stop(),
3381
+ * onBroadcast: (text) => showNotification(text)
3382
+ * })
3383
+ * ```
3384
+ */
3385
+ enableProFeatures(config) {
3386
+ if (this._proFeaturesEnabled) {
3387
+ this.log("Pro features already enabled");
3388
+ return;
3389
+ }
3390
+ this.adminPanel = new AdminPanelUIModule(this, config);
3391
+ this._proFeaturesEnabled = true;
3392
+ this.adminPanel.init().catch((err) => {
3393
+ this.log(`Admin panel init failed: ${err}`);
3394
+ });
3395
+ this.log("Pro features enabled (admin panel)");
3396
+ }
3397
+ /**
3398
+ * Register a custom scene tab for the admin panel (Full/Custom tier)
3399
+ * Must call enableProFeatures() first.
3400
+ *
3401
+ * @param tab The tab definition with label, id, and render function
3402
+ *
3403
+ * @example
3404
+ * ```typescript
3405
+ * staticTV.registerSceneTab({
3406
+ * label: 'LIGHTS',
3407
+ * id: 'lights',
3408
+ * render: () => <MyLightsControls />
3409
+ * })
3410
+ * ```
3411
+ */
3412
+ registerSceneTab(tab) {
3413
+ if (!this.adminPanel) {
3414
+ this.log("Cannot register scene tab - call enableProFeatures() first");
3415
+ return;
3416
+ }
3417
+ this.adminPanel.registerSceneTab(tab);
3418
+ }
2299
3419
  /**
2300
3420
  * Cleanup when done (call before scene unload)
2301
3421
  */
@@ -2317,6 +3437,7 @@ var StaticTVClient = class {
2317
3437
  }
2318
3438
  };
2319
3439
  export {
3440
+ AdminPanelUIModule,
2320
3441
  ChatUIModule,
2321
3442
  GuideModule,
2322
3443
  GuideUIModule,