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