@thestatic-tv/dcl-sdk 2.2.10 → 2.3.0-beta.1

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,
@@ -41,7 +42,9 @@ __export(index_exports, {
41
42
  StaticTVClient: () => StaticTVClient,
42
43
  fetchUserData: () => fetchUserData,
43
44
  getPlayerDisplayName: () => getPlayerDisplayName,
44
- getPlayerWallet: () => getPlayerWallet
45
+ getPlayerWallet: () => getPlayerWallet,
46
+ setupStaticUI: () => setupStaticUI,
47
+ showNotification: () => showNotification
45
48
  });
46
49
  module.exports = __toCommonJS(index_exports);
47
50
 
@@ -136,8 +139,8 @@ function getPlayerWallet() {
136
139
  return cachedWallet;
137
140
  }
138
141
  try {
139
- const { getPlayer: getPlayer2 } = require("@dcl/sdk/players");
140
- const player = getPlayer2();
142
+ const { getPlayer: getPlayer3 } = require("@dcl/sdk/players");
143
+ const player = getPlayer3();
141
144
  return player?.userId ?? null;
142
145
  } catch {
143
146
  return null;
@@ -148,8 +151,8 @@ function getPlayerDisplayName() {
148
151
  return cachedDisplayName;
149
152
  }
150
153
  try {
151
- const { getPlayer: getPlayer2 } = require("@dcl/sdk/players");
152
- const player = getPlayer2();
154
+ const { getPlayer: getPlayer3 } = require("@dcl/sdk/players");
155
+ const player = getPlayer3();
153
156
  return player?.name ?? null;
154
157
  } catch {
155
158
  return null;
@@ -172,7 +175,7 @@ function ensureTimerSystem() {
172
175
  try {
173
176
  timer.callback();
174
177
  } catch (e) {
175
- console.error("[StaticTV Timer] Callback error:", e);
178
+ console.error("[TheStatic] Timer error");
176
179
  }
177
180
  }
178
181
  }
@@ -213,7 +216,7 @@ function ensureTimeoutSystem() {
213
216
  try {
214
217
  timeout.callback();
215
218
  } catch (e) {
216
- console.error("[StaticTV Timer] Timeout callback error:", e);
219
+ console.error("[TheStatic] Timeout error");
217
220
  }
218
221
  }
219
222
  }
@@ -244,20 +247,41 @@ function dclClearTimeout(timeoutId) {
244
247
  }
245
248
 
246
249
  // src/modules/session.ts
250
+ function normalizeTier(tier) {
251
+ if (tier === "lite") return "free";
252
+ if (tier === "full") return "standard";
253
+ if (tier === "free" || tier === "standard" || tier === "pro") return tier;
254
+ return "free";
255
+ }
247
256
  var SessionModule = class {
248
257
  constructor(client) {
249
258
  this.sessionId = null;
259
+ this._keyId = null;
250
260
  this.heartbeatTimerId = null;
251
261
  this.isActive = false;
252
- this._sdkType = "lite";
262
+ this._tier = "free";
253
263
  this.client = client;
254
264
  }
255
265
  /**
256
- * Get the SDK type returned by the server
257
- * Determines what features are available (lite = sessions only, full = guide + chat + more)
266
+ * Get the API key ID (used as default sceneId for Pro users)
267
+ */
268
+ get keyId() {
269
+ return this._keyId;
270
+ }
271
+ /**
272
+ * Get the SDK tier returned by the server
273
+ * - free: Session tracking only
274
+ * - standard: Guide, Chat, Heartbeat, Interactions
275
+ * - pro: Everything + Admin Panel
276
+ */
277
+ get tier() {
278
+ return this._tier;
279
+ }
280
+ /**
281
+ * @deprecated Use `tier` instead. Returns mapped value for compatibility.
258
282
  */
259
283
  get sdkType() {
260
- return this._sdkType;
284
+ return this._tier === "free" ? "lite" : "full";
261
285
  }
262
286
  /**
263
287
  * Get the appropriate session endpoint based on key type
@@ -304,13 +328,12 @@ var SessionModule = class {
304
328
  });
305
329
  if (response.success && response.sessionId) {
306
330
  this.sessionId = response.sessionId;
331
+ this._keyId = response.keyId || null;
307
332
  this.isActive = true;
308
- this._sdkType = response.sdkType || "lite";
333
+ this._tier = normalizeTier(response.sdkType);
309
334
  this.startHeartbeat();
310
- this.client.log(`Session started: ${this.sessionId}, sdkType: ${this._sdkType}`);
311
- if (this._sdkType === "full") {
312
- this.client._enableFullFeatures();
313
- }
335
+ this.client.log(`Session started: ${this.sessionId}, tier: ${this._tier}`);
336
+ this.client._enableFeaturesForTier(this._tier, this._keyId);
314
337
  return this.sessionId;
315
338
  }
316
339
  return null;
@@ -598,7 +621,8 @@ var UI_DIMENSIONS = {
598
621
  // Guide UI - positioned to the left of chat (chat is 380px wide at right:20)
599
622
  guide: {
600
623
  width: 900,
601
- height: "55%",
624
+ height: 580,
625
+ // Numeric value for scaling (matches chat/admin)
602
626
  bottom: 55,
603
627
  right: 410,
604
628
  // 20 + 380 (chat width) + 10 (gap)
@@ -617,15 +641,33 @@ var UI_DIMENSIONS = {
617
641
  padding: 15
618
642
  }
619
643
  },
620
- // Chat UI - match m1d-hq-lifted dimensions
644
+ // Chat UI - positioned at right side
621
645
  chat: {
622
646
  width: 380,
623
647
  height: 580,
624
648
  bottom: 55,
625
649
  right: 20,
650
+ headerHeight: 40,
626
651
  messagesPerPage: 5,
627
652
  channelsPerPage: 6
628
653
  },
654
+ // Admin Panel - positioned left of chat
655
+ admin: {
656
+ width: 400,
657
+ height: 580,
658
+ // Match chat height
659
+ maxHeight: 700,
660
+ bottom: 55,
661
+ right: 410,
662
+ // 20 + 380 (chat width) + 10 (gap)
663
+ headerHeight: 48,
664
+ tabHeight: 40,
665
+ footerHeight: 32,
666
+ sectionHeadHeight: 28,
667
+ buttonHeight: 36,
668
+ buttonHeightSmall: 30,
669
+ inputHeight: 36
670
+ },
629
671
  // Shared
630
672
  closeButton: {
631
673
  size: 40,
@@ -633,18 +675,18 @@ var UI_DIMENSIONS = {
633
675
  }
634
676
  };
635
677
  var DEFAULT_CHAT_THEME = {
636
- header: 22,
678
+ header: 16,
637
679
  channelButton: 14,
638
680
  channelDropdown: 14,
639
- systemMessage: 14,
640
- chatUsername: 16,
681
+ systemMessage: 13,
682
+ chatUsername: 14,
641
683
  chatTimestamp: 11,
642
- chatMessage: 16,
643
- input: 16,
684
+ chatMessage: 14,
685
+ input: 14,
644
686
  sendButton: 14,
645
- userInfo: 14,
687
+ userInfo: 13,
646
688
  authStatus: 12,
647
- notification: 18,
689
+ notification: 16,
648
690
  closeButton: 16
649
691
  };
650
692
  function scaleChatTheme(theme, fontScale) {
@@ -664,6 +706,30 @@ function scaleChatTheme(theme, fontScale) {
664
706
  closeButton: Math.round(theme.closeButton * fontScale)
665
707
  };
666
708
  }
709
+ var DEFAULT_ADMIN_THEME = {
710
+ header: 16,
711
+ tabButton: 14,
712
+ sectionHead: 13,
713
+ label: 13,
714
+ labelSmall: 11,
715
+ button: 13,
716
+ buttonSmall: 11,
717
+ input: 13,
718
+ status: 12
719
+ };
720
+ function scaleAdminTheme(theme, fontScale) {
721
+ return {
722
+ header: Math.round(theme.header * fontScale),
723
+ tabButton: Math.round(theme.tabButton * fontScale),
724
+ sectionHead: Math.round(theme.sectionHead * fontScale),
725
+ label: Math.round(theme.label * fontScale),
726
+ labelSmall: Math.round(theme.labelSmall * fontScale),
727
+ button: Math.round(theme.button * fontScale),
728
+ buttonSmall: Math.round(theme.buttonSmall * fontScale),
729
+ input: Math.round(theme.input * fontScale),
730
+ status: Math.round(theme.status * fontScale)
731
+ };
732
+ }
667
733
 
668
734
  // src/ui/components.tsx
669
735
  var import_react_ecs = __toESM(require("@dcl/sdk/react-ecs"));
@@ -719,21 +785,21 @@ var PanelHeader = (props) => {
719
785
  }),
720
786
  import_react_ecs.default.createElement(import_react_ecs.UiEntity, { key: "pos-spacer", uiTransform: { width: 6 } })
721
787
  ] : [],
722
- // Font controls
788
+ // Font controls (with visual disabled state at limits)
723
789
  ...props.showFontControls ? [
724
790
  import_react_ecs.default.createElement(import_react_ecs.UiEntity, {
725
791
  key: "font-down",
726
792
  uiTransform: { width: 22, height: 22, justifyContent: "center", alignItems: "center" },
727
- uiBackground: { color: THEME.colors.buttonBackground },
728
- onMouseDown: () => props.onFontScaleDown?.(),
729
- children: [import_react_ecs.default.createElement(import_react_ecs.UiEntity, { uiText: { value: "\u2212", fontSize: 14, color: THEME.colors.cyan } })]
793
+ uiBackground: { color: props.fontScaleAtMin ? import_math2.Color4.create(0.1, 0.1, 0.1, 0.3) : THEME.colors.buttonBackground },
794
+ onMouseDown: () => !props.fontScaleAtMin && props.onFontScaleDown?.(),
795
+ children: [import_react_ecs.default.createElement(import_react_ecs.UiEntity, { uiText: { value: "\u2212", fontSize: 14, color: props.fontScaleAtMin ? THEME.colors.gray : THEME.colors.cyan } })]
730
796
  }),
731
797
  import_react_ecs.default.createElement(import_react_ecs.UiEntity, {
732
798
  key: "font-up",
733
799
  uiTransform: { width: 22, height: 22, justifyContent: "center", alignItems: "center" },
734
- uiBackground: { color: THEME.colors.buttonBackground },
735
- onMouseDown: () => props.onFontScaleUp?.(),
736
- children: [import_react_ecs.default.createElement(import_react_ecs.UiEntity, { uiText: { value: "+", fontSize: 14, color: THEME.colors.cyan } })]
800
+ uiBackground: { color: props.fontScaleAtMax ? import_math2.Color4.create(0.1, 0.1, 0.1, 0.3) : THEME.colors.buttonBackground },
801
+ onMouseDown: () => !props.fontScaleAtMax && props.onFontScaleUp?.(),
802
+ children: [import_react_ecs.default.createElement(import_react_ecs.UiEntity, { uiText: { value: "+", fontSize: 14, color: props.fontScaleAtMax ? THEME.colors.gray : THEME.colors.cyan } })]
737
803
  }),
738
804
  import_react_ecs.default.createElement(import_react_ecs.UiEntity, { key: "font-spacer", uiTransform: { width: 6 } })
739
805
  ] : [],
@@ -771,7 +837,6 @@ var GuideUIModule = class {
771
837
  this.currentPage = 0;
772
838
  this.itemsPerPage = 6;
773
839
  this.searchQuery = "";
774
- this.uiScale = 1;
775
840
  // Current video tracking (for "PLAYING" indicator)
776
841
  this._currentVideoId = null;
777
842
  // =============================================================================
@@ -779,34 +844,46 @@ var GuideUIModule = class {
779
844
  // =============================================================================
780
845
  /**
781
846
  * Get the guide UI component for rendering
782
- * Returns toggle button when hidden, full panel when visible
847
+ * Always renders toggle button, plus panel when visible
783
848
  */
784
849
  this.getComponent = () => {
785
- if (!this._isVisible) {
786
- return this.renderToggleButton();
787
- }
788
- const windowW = this.s(UI_DIMENSIONS.guide.width);
850
+ const windowW = UI_DIMENSIONS.guide.width;
851
+ const windowH = UI_DIMENSIONS.guide.height;
789
852
  return import_react_ecs2.default.createElement(import_react_ecs2.UiEntity, {
853
+ key: `guide-root-${this.client.uiScale}`,
790
854
  uiTransform: {
791
- width: windowW,
792
- height: UI_DIMENSIONS.guide.height,
793
- positionType: "absolute",
794
- position: { bottom: UI_DIMENSIONS.guide.bottom, right: UI_DIMENSIONS.guide.right },
795
- flexDirection: "row",
796
- border: { top: 2, bottom: 2, left: 2, right: 2 },
797
- borderColor: THEME.colors.panelBorder
855
+ width: "100%",
856
+ height: "100%",
857
+ positionType: "absolute"
798
858
  },
799
- uiBackground: { color: THEME.colors.panel },
800
859
  children: [
801
- this.renderLeftPanel(),
802
- this.renderRightPanel(),
803
- this.renderCloseButton()
860
+ // Always render toggle button
861
+ this.renderToggleButton(),
862
+ // Render panel when visible - Guide positioned relative to Chat's scaled width
863
+ // Guide.right = Chat.right + Chat.scaledWidth + gap (10px)
864
+ this._isVisible ? import_react_ecs2.default.createElement(import_react_ecs2.UiEntity, {
865
+ key: `guide-panel-${this.client.uiScale}`,
866
+ uiTransform: {
867
+ width: windowW,
868
+ height: windowH,
869
+ positionType: "absolute",
870
+ position: { bottom: UI_DIMENSIONS.guide.bottom, right: UI_DIMENSIONS.chat.right + this.s(UI_DIMENSIONS.chat.width) + 10 },
871
+ flexDirection: "row",
872
+ border: { top: 2, bottom: 2, left: 2, right: 2 },
873
+ borderColor: THEME.colors.panelBorder
874
+ },
875
+ uiBackground: { color: THEME.colors.panel },
876
+ children: [
877
+ this.renderLeftPanel(),
878
+ this.renderRightPanel(),
879
+ this.renderCloseButton()
880
+ ]
881
+ }) : null
804
882
  ]
805
883
  });
806
884
  };
807
885
  this.client = client;
808
886
  this.config = config;
809
- this.uiScale = config.uiScale || 1;
810
887
  this._currentVideoId = config.currentVideoId || null;
811
888
  }
812
889
  /**
@@ -820,6 +897,8 @@ var GuideUIModule = class {
820
897
  * Show the guide UI
821
898
  */
822
899
  show() {
900
+ if (this._isVisible) return;
901
+ this.client.closeOtherPanels("guide");
823
902
  this._isVisible = true;
824
903
  this.fetchGuideData().catch(() => {
825
904
  });
@@ -955,7 +1034,7 @@ var GuideUIModule = class {
955
1034
  // --- UTILITIES ---
956
1035
  // =============================================================================
957
1036
  s(value) {
958
- return Math.round(value * this.uiScale);
1037
+ return Math.round(value * this.client.uiScale);
959
1038
  }
960
1039
  handleVideoSelect(video) {
961
1040
  if (this.config.onVideoSelect) {
@@ -991,15 +1070,26 @@ var GuideUIModule = class {
991
1070
  import_react_ecs2.default.createElement(import_react_ecs2.UiEntity, {
992
1071
  uiTransform: { width: "100%", flexDirection: "column", padding: 10 },
993
1072
  children: [
994
- // Title
1073
+ // Title row
995
1074
  import_react_ecs2.default.createElement(import_react_ecs2.UiEntity, {
996
- uiText: {
997
- value: "THE STATIC TV",
998
- fontSize: this.s(UI_DIMENSIONS.guide.sidebar.headerSize),
999
- color: THEME.colors.cyan,
1000
- textAlign: "middle-center"
1075
+ uiTransform: {
1076
+ width: "100%",
1077
+ height: this.s(40),
1078
+ marginBottom: 10,
1079
+ flexDirection: "row",
1080
+ alignItems: "center"
1001
1081
  },
1002
- uiTransform: { height: this.s(40), marginBottom: 10 }
1082
+ children: [
1083
+ // Title
1084
+ import_react_ecs2.default.createElement(import_react_ecs2.UiEntity, {
1085
+ uiText: {
1086
+ value: "THE STATIC TV",
1087
+ fontSize: this.s(UI_DIMENSIONS.guide.sidebar.headerSize),
1088
+ color: THEME.colors.cyan,
1089
+ textAlign: "middle-left"
1090
+ }
1091
+ })
1092
+ ]
1003
1093
  }),
1004
1094
  // Random Signal button
1005
1095
  import_react_ecs2.default.createElement(import_react_ecs2.UiEntity, {
@@ -1349,23 +1439,25 @@ var GuideUIModule = class {
1349
1439
  });
1350
1440
  }
1351
1441
  renderToggleButton() {
1442
+ const buttonText = this._isVisible ? "CLOSE" : "GUIDE";
1443
+ const buttonColor = this._isVisible ? import_math3.Color4.create(0.2, 0.2, 0.28, 0.9) : import_math3.Color4.create(0, 0.5, 0.5, 0.9);
1444
+ const buttonPos = 20 + this.s(100) + 10;
1352
1445
  return import_react_ecs2.default.createElement(import_react_ecs2.UiEntity, {
1353
1446
  uiTransform: {
1354
1447
  positionType: "absolute",
1355
- position: { right: 130, bottom: 10 },
1356
- // To the left of CHAT button
1448
+ position: { right: buttonPos, bottom: 10 },
1357
1449
  width: this.s(100),
1358
1450
  height: this.s(45),
1359
1451
  justifyContent: "center",
1360
1452
  alignItems: "center"
1361
1453
  },
1362
- uiBackground: { color: import_math3.Color4.create(0, 0.5, 0.5, 0.9) },
1363
- onMouseDown: () => this.show(),
1454
+ uiBackground: { color: buttonColor },
1455
+ onMouseDown: () => this._isVisible ? this.hide() : this.show(),
1364
1456
  children: [
1365
1457
  import_react_ecs2.default.createElement(import_react_ecs2.UiEntity, {
1366
1458
  uiText: {
1367
- value: "GUIDE",
1368
- fontSize: this.s(16),
1459
+ value: buttonText,
1460
+ fontSize: this.s(14),
1369
1461
  color: THEME.colors.white,
1370
1462
  textAlign: "middle-center"
1371
1463
  }
@@ -1407,7 +1499,6 @@ var ChatUIModule = class {
1407
1499
  this.chatScrollOffset = 0;
1408
1500
  // UI preferences
1409
1501
  this.position = "right";
1410
- this.fontScale = 1;
1411
1502
  // Timers
1412
1503
  this.chatTimerId = null;
1413
1504
  this.playerInfoTimerId = null;
@@ -1417,43 +1508,52 @@ var ChatUIModule = class {
1417
1508
  // =============================================================================
1418
1509
  /**
1419
1510
  * Get the chat UI component for rendering
1420
- * Returns toggle button when hidden, full panel when visible
1511
+ * Always renders toggle button, plus panel when visible
1421
1512
  */
1422
1513
  this.getComponent = () => {
1423
- if (!this._isVisible) {
1424
- return this.renderToggleButton();
1425
- }
1426
- const scaledTheme = scaleChatTheme(DEFAULT_CHAT_THEME, this.fontScale);
1514
+ const scaledTheme = scaleChatTheme(DEFAULT_CHAT_THEME, this.client.uiScale);
1427
1515
  const positionStyle = this.getPositionStyle();
1428
1516
  return import_react_ecs3.default.createElement(import_react_ecs3.UiEntity, {
1429
1517
  uiTransform: {
1430
- width: UI_DIMENSIONS.chat.width,
1431
- height: UI_DIMENSIONS.chat.height,
1432
- positionType: "absolute",
1433
- position: positionStyle,
1434
- // Must be nested object, not spread!
1435
- flexDirection: "column",
1436
- border: { top: 2, bottom: 2, left: 2, right: 2 },
1437
- borderColor: THEME.colors.panelBorder
1518
+ width: "100%",
1519
+ height: "100%",
1520
+ positionType: "absolute"
1438
1521
  },
1439
- uiBackground: { color: THEME.colors.panel },
1440
1522
  children: [
1441
- this.renderHeader(),
1442
- this.renderChannelButton(scaledTheme),
1443
- this.renderMessagesArea(scaledTheme),
1444
- import_react_ecs3.default.createElement(import_react_ecs3.UiEntity, {
1445
- uiTransform: { width: "100%", height: 1, flexShrink: 0 },
1446
- uiBackground: { color: THEME.colors.panelBorder }
1447
- }),
1448
- this.renderUserInfoBar(scaledTheme),
1449
- this.renderInputArea(scaledTheme),
1450
- this.renderChannelDropdown(scaledTheme)
1523
+ // Always render toggle button
1524
+ this.renderToggleButton(),
1525
+ // Render panel when visible - scaled sizes, fixed position
1526
+ // Key includes uiScale to force re-render when scale changes from other panels
1527
+ this._isVisible ? import_react_ecs3.default.createElement(import_react_ecs3.UiEntity, {
1528
+ key: `chat-panel-${this.client.uiScale}`,
1529
+ uiTransform: {
1530
+ width: this.s(UI_DIMENSIONS.chat.width),
1531
+ height: this.s(UI_DIMENSIONS.chat.height),
1532
+ positionType: "absolute",
1533
+ position: { bottom: UI_DIMENSIONS.chat.bottom, right: UI_DIMENSIONS.chat.right },
1534
+ flexDirection: "column",
1535
+ border: { top: 2, bottom: 2, left: 2, right: 2 },
1536
+ borderColor: THEME.colors.panelBorder
1537
+ },
1538
+ uiBackground: { color: THEME.colors.panel },
1539
+ children: [
1540
+ this.renderHeader(),
1541
+ this.renderChannelButton(scaledTheme),
1542
+ this.renderMessagesArea(scaledTheme),
1543
+ import_react_ecs3.default.createElement(import_react_ecs3.UiEntity, {
1544
+ uiTransform: { width: "100%", height: 1, flexShrink: 0 },
1545
+ uiBackground: { color: THEME.colors.panelBorder }
1546
+ }),
1547
+ this.renderUserInfoBar(scaledTheme),
1548
+ this.renderInputArea(scaledTheme),
1549
+ this.renderChannelDropdown(scaledTheme)
1550
+ ]
1551
+ }) : null
1451
1552
  ]
1452
1553
  });
1453
1554
  };
1454
1555
  this.client = client;
1455
1556
  this.config = config;
1456
- this.fontScale = config.fontScale || 1;
1457
1557
  }
1458
1558
  /**
1459
1559
  * Initialize the chat system
@@ -1472,6 +1572,7 @@ var ChatUIModule = class {
1472
1572
  * Show the chat UI
1473
1573
  */
1474
1574
  show() {
1575
+ if (this._isVisible) return;
1475
1576
  this._isVisible = true;
1476
1577
  this._unreadCount = 0;
1477
1578
  this.chatScrollOffset = 0;
@@ -1733,6 +1834,10 @@ var ChatUIModule = class {
1733
1834
  const d = new Date(input);
1734
1835
  if (!isNaN(d.getTime())) return d.getTime();
1735
1836
  }
1837
+ if (input instanceof Date) return input.getTime();
1838
+ if (typeof input === "object" && "seconds" in input) {
1839
+ return input.seconds * 1e3;
1840
+ }
1736
1841
  return 0;
1737
1842
  }
1738
1843
  formatTime(isoString) {
@@ -1745,6 +1850,10 @@ var ChatUIModule = class {
1745
1850
  return "";
1746
1851
  }
1747
1852
  }
1853
+ /** Scale a dimension by shared uiScale */
1854
+ s(value) {
1855
+ return Math.round(value * this.client.uiScale);
1856
+ }
1748
1857
  getPositionStyle() {
1749
1858
  return { bottom: 55, right: 20 };
1750
1859
  }
@@ -1752,15 +1861,9 @@ var ChatUIModule = class {
1752
1861
  return PanelHeader({
1753
1862
  title: "LIVE CHAT",
1754
1863
  fontSize: 14,
1755
- fontScale: this.fontScale,
1864
+ fontScale: 1,
1756
1865
  showPositionControls: false,
1757
- showFontControls: true,
1758
- onFontScaleUp: () => {
1759
- this.fontScale = Math.min(1.4, this.fontScale + 0.1);
1760
- },
1761
- onFontScaleDown: () => {
1762
- this.fontScale = Math.max(0.7, this.fontScale - 0.1);
1763
- },
1866
+ showFontControls: false,
1764
1867
  onClose: () => this.hide()
1765
1868
  });
1766
1869
  }
@@ -2135,23 +2238,24 @@ var ChatUIModule = class {
2135
2238
  }
2136
2239
  renderToggleButton() {
2137
2240
  const unreadBadge = this._unreadCount > 0 ? ` (${this._unreadCount})` : "";
2241
+ const buttonText = this._isVisible ? "CLOSE" : `CHAT${unreadBadge}`;
2242
+ const buttonColor = this._isVisible ? import_math4.Color4.create(0.2, 0.2, 0.28, 0.9) : import_math4.Color4.create(0.6, 0, 0.5, 0.9);
2138
2243
  return import_react_ecs3.default.createElement(import_react_ecs3.UiEntity, {
2139
2244
  uiTransform: {
2140
2245
  positionType: "absolute",
2141
2246
  position: { right: 20, bottom: 10 },
2142
- // Far right corner
2143
- width: 100,
2144
- height: 45,
2247
+ width: this.s(100),
2248
+ height: this.s(45),
2145
2249
  justifyContent: "center",
2146
2250
  alignItems: "center"
2147
2251
  },
2148
- uiBackground: { color: import_math4.Color4.create(0.6, 0, 0.5, 0.9) },
2149
- onMouseDown: () => this.show(),
2252
+ uiBackground: { color: buttonColor },
2253
+ onMouseDown: () => this._isVisible ? this.hide() : this.show(),
2150
2254
  children: [
2151
2255
  import_react_ecs3.default.createElement(import_react_ecs3.UiEntity, {
2152
2256
  uiText: {
2153
- value: `CHAT${unreadBadge}`,
2154
- fontSize: 16,
2257
+ value: buttonText,
2258
+ fontSize: this.s(14),
2155
2259
  color: THEME.colors.white,
2156
2260
  textAlign: "middle-center"
2157
2261
  }
@@ -2161,6 +2265,1232 @@ var ChatUIModule = class {
2161
2265
  }
2162
2266
  };
2163
2267
 
2268
+ // src/ui/admin-panel-ui.tsx
2269
+ var import_react_ecs4 = __toESM(require("@dcl/sdk/react-ecs"));
2270
+ var import_math5 = require("@dcl/sdk/math");
2271
+ var import_players2 = require("@dcl/sdk/players");
2272
+ var import_RestrictedActions3 = require("~system/RestrictedActions");
2273
+ var BAN_KICK_POSITION = import_math5.Vector3.create(16, -50, 16);
2274
+ var C = {
2275
+ bg: import_math5.Color4.create(0.08, 0.08, 0.12, 0.98),
2276
+ header: import_math5.Color4.create(0.9, 0.15, 0.15, 1),
2277
+ tabActive: import_math5.Color4.create(0, 0.7, 0.7, 1),
2278
+ tabInactive: import_math5.Color4.create(0.15, 0.15, 0.2, 1),
2279
+ section: import_math5.Color4.create(0.12, 0.12, 0.18, 1),
2280
+ btn: import_math5.Color4.create(0.2, 0.2, 0.28, 1),
2281
+ cyan: import_math5.Color4.create(0, 0.8, 0.8, 1),
2282
+ magenta: import_math5.Color4.create(0.85, 0.2, 0.55, 1),
2283
+ yellow: import_math5.Color4.create(0.95, 0.75, 0.1, 1),
2284
+ green: import_math5.Color4.create(0.2, 0.75, 0.3, 1),
2285
+ red: import_math5.Color4.create(0.85, 0.2, 0.2, 1),
2286
+ purple: import_math5.Color4.create(0.6, 0.3, 0.85, 1),
2287
+ orange: import_math5.Color4.create(0.95, 0.5, 0.15, 1),
2288
+ text: import_math5.Color4.White(),
2289
+ textDim: import_math5.Color4.create(0.6, 0.6, 0.7, 1)
2290
+ };
2291
+ var AdminPanelUIModule = class {
2292
+ // UI scaling - uses shared client.uiScale
2293
+ constructor(client, config) {
2294
+ // State
2295
+ this.isAdmin = false;
2296
+ this.isOwner = false;
2297
+ this.panelOpen = false;
2298
+ this.activeTab = "video";
2299
+ this.playerWallet = "";
2300
+ // Video tab state
2301
+ this.customVideoUrl = "";
2302
+ this.streamData = null;
2303
+ this.streamFetched = false;
2304
+ this.videoState = null;
2305
+ this.videoStateFetched = false;
2306
+ this.channelCreating = false;
2307
+ this.channelCreateError = "";
2308
+ this.channelDeleting = false;
2309
+ this.channelDeleteError = "";
2310
+ this.keyRotating = false;
2311
+ this.keyRotateStatus = "";
2312
+ this.streamControlling = false;
2313
+ this.streamControlStatus = "";
2314
+ this.pollIntervalId = null;
2315
+ this.trialClaiming = false;
2316
+ this.trialClaimError = "";
2317
+ // Mod tab state
2318
+ this.sceneAdmins = [];
2319
+ this.bannedWallets = [];
2320
+ this.newAdminWallet = "";
2321
+ this.newBanWallet = "";
2322
+ this.broadcastText = "";
2323
+ this.modStatus = "";
2324
+ this.modsFetched = false;
2325
+ // --- UI Components ---
2326
+ this.SectionHead = ({ label, color }) => /* @__PURE__ */ import_react_ecs4.default.createElement(
2327
+ import_react_ecs4.UiEntity,
2328
+ {
2329
+ uiTransform: { width: "100%", height: this.s(UI_DIMENSIONS.admin.sectionHeadHeight), margin: { bottom: 8 }, padding: { left: 10 }, alignItems: "center" },
2330
+ uiBackground: { color: import_math5.Color4.create(color.r * 0.3, color.g * 0.3, color.b * 0.3, 0.5) }
2331
+ },
2332
+ /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.Label, { value: label, fontSize: this.theme.sectionHead, color })
2333
+ );
2334
+ this.TabBtn = ({ label, tab }) => /* @__PURE__ */ import_react_ecs4.default.createElement(
2335
+ import_react_ecs4.Button,
2336
+ {
2337
+ uiTransform: { flexGrow: 1, height: this.s(UI_DIMENSIONS.admin.tabHeight), justifyContent: "center", alignItems: "center" },
2338
+ uiBackground: { color: this.activeTab === tab ? C.tabActive : C.tabInactive },
2339
+ value: label,
2340
+ fontSize: this.theme.tabButton,
2341
+ color: this.activeTab === tab ? import_math5.Color4.Black() : C.text,
2342
+ textAlign: "middle-center",
2343
+ onMouseDown: () => this.setActiveTab(tab)
2344
+ }
2345
+ );
2346
+ this.VideoTab = () => {
2347
+ if (!this.streamFetched) {
2348
+ this.fetchStreamData();
2349
+ }
2350
+ const t = this.theme;
2351
+ return /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.UiEntity, { uiTransform: { flexDirection: "column", width: "100%", padding: 10 } }, !this.streamData?.hasChannel && /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.UiEntity, { uiTransform: { flexDirection: "column", margin: { bottom: 14 } } }, /* @__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: t.labelSmall, color: C.textDim, uiTransform: { margin: { bottom: 8 } } }), 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: 10 } } }, /* @__PURE__ */ import_react_ecs4.default.createElement(
2352
+ import_react_ecs4.Button,
2353
+ {
2354
+ uiTransform: { width: this.s(200), height: this.s(UI_DIMENSIONS.admin.buttonHeight), margin: { bottom: 6 } },
2355
+ uiBackground: { color: this.trialClaiming ? C.btn : C.green },
2356
+ value: this.trialClaiming ? "Claiming..." : "Start Free 4-Hour Trial",
2357
+ fontSize: t.button,
2358
+ color: C.text,
2359
+ onMouseDown: () => this.claimTrial()
2360
+ }
2361
+ ), this.trialClaimError && /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.Label, { value: this.trialClaimError, fontSize: t.status, color: C.red }), /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.Label, { value: "One-time trial \u2022 4 hours of streaming", fontSize: t.labelSmall, color: C.textDim, uiTransform: { margin: { top: 4 } } })), !this.streamData?.trialAvailable && /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.UiEntity, { uiTransform: { flexDirection: "column" } }, /* @__PURE__ */ import_react_ecs4.default.createElement(
2362
+ import_react_ecs4.Button,
2363
+ {
2364
+ uiTransform: { width: this.s(170), height: this.s(UI_DIMENSIONS.admin.buttonHeight), margin: { bottom: 6 } },
2365
+ uiBackground: { color: this.channelCreating ? C.btn : C.cyan },
2366
+ value: this.channelCreating ? "Creating..." : "+ Create Channel",
2367
+ fontSize: t.button,
2368
+ color: C.text,
2369
+ onMouseDown: () => this.createChannel()
2370
+ }
2371
+ ), this.channelCreateError && /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.Label, { value: this.channelCreateError, fontSize: t.status, color: C.red }), /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.Label, { value: "Relay tier \u2022 $25/mo \u2022 8 hours streaming", fontSize: t.labelSmall, color: C.textDim, uiTransform: { margin: { top: 4 } } })))), this.streamData?.hasChannel && this.isOwner && /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.UiEntity, { uiTransform: { flexDirection: "column", margin: { bottom: 14 } } }, /* @__PURE__ */ import_react_ecs4.default.createElement(this.SectionHead, { label: this.streamData.isLive ? "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: 8 } } }, /* @__PURE__ */ import_react_ecs4.default.createElement(
2372
+ import_react_ecs4.Label,
2373
+ {
2374
+ value: this.streamData.isLive ? `LIVE \u2022 ${this.streamData.currentViewers || 0} viewers` : "OFFLINE",
2375
+ fontSize: t.label,
2376
+ color: this.streamData.isLive ? C.red : C.textDim
2377
+ }
2378
+ ), /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.Label, { value: ` \u2022 ${this.streamData.tier?.toUpperCase() || "RELAY"}`, fontSize: t.labelSmall, color: C.cyan }), /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.Label, { value: ` \u2022 ${this.streamData.sparksBalance || 0} Sparks`, fontSize: t.labelSmall, color: C.textDim })), /* @__PURE__ */ import_react_ecs4.default.createElement(
2379
+ import_react_ecs4.Label,
2380
+ {
2381
+ value: `Channel: ${this.streamData.channelName || this.streamData.channelId}`,
2382
+ fontSize: t.labelSmall,
2383
+ color: C.textDim,
2384
+ uiTransform: { margin: { bottom: 10 } }
2385
+ }
2386
+ ), /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.UiEntity, { uiTransform: { flexDirection: "column", margin: { bottom: 6 } } }, /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.Label, { value: "RTMP Server:", fontSize: t.labelSmall, color: C.textDim }), /* @__PURE__ */ import_react_ecs4.default.createElement(
2387
+ import_react_ecs4.Label,
2388
+ {
2389
+ value: this.streamData.rtmpUrl || "Loading...",
2390
+ fontSize: t.label,
2391
+ color: this.streamData.rtmpUrl ? C.text : C.textDim,
2392
+ uiTransform: { margin: { left: 6 } }
2393
+ }
2394
+ )), /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.UiEntity, { uiTransform: { flexDirection: "column", margin: { bottom: 6 } } }, /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.Label, { value: "Stream Key:", fontSize: t.labelSmall, color: C.textDim }), /* @__PURE__ */ import_react_ecs4.default.createElement(
2395
+ import_react_ecs4.Label,
2396
+ {
2397
+ value: this.streamData.streamKey || "Loading...",
2398
+ fontSize: t.label,
2399
+ color: this.streamData.streamKey ? C.cyan : C.textDim,
2400
+ uiTransform: { margin: { left: 6 } }
2401
+ }
2402
+ )), /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.UiEntity, { uiTransform: { flexDirection: "column", margin: { bottom: 10 } } }, /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.Label, { value: "HLS Playback:", fontSize: t.labelSmall, color: C.textDim }), /* @__PURE__ */ import_react_ecs4.default.createElement(
2403
+ import_react_ecs4.Label,
2404
+ {
2405
+ value: this.streamData.hlsUrl || "Not available",
2406
+ fontSize: t.label,
2407
+ color: this.streamData.hlsUrl ? C.green : C.textDim,
2408
+ uiTransform: { margin: { left: 6 } }
2409
+ }
2410
+ )), /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.UiEntity, { uiTransform: { flexDirection: "row", flexWrap: "wrap", margin: { bottom: 6 } } }, !this.streamData.isLive && /* @__PURE__ */ import_react_ecs4.default.createElement(
2411
+ import_react_ecs4.Button,
2412
+ {
2413
+ uiTransform: { width: this.s(110), height: this.s(UI_DIMENSIONS.admin.buttonHeightSmall), margin: 4 },
2414
+ uiBackground: { color: this.streamControlling ? C.btn : C.green },
2415
+ value: this.streamControlling ? "Starting..." : "Start Stream",
2416
+ fontSize: t.buttonSmall,
2417
+ color: C.text,
2418
+ onMouseDown: () => this.startStream()
2419
+ }
2420
+ ), this.streamData.isLive && /* @__PURE__ */ import_react_ecs4.default.createElement(
2421
+ import_react_ecs4.Button,
2422
+ {
2423
+ uiTransform: { width: this.s(110), height: this.s(UI_DIMENSIONS.admin.buttonHeightSmall), margin: 4 },
2424
+ uiBackground: { color: this.streamControlling ? C.btn : C.red },
2425
+ value: this.streamControlling ? "Stopping..." : "Stop Stream",
2426
+ fontSize: t.buttonSmall,
2427
+ color: C.text,
2428
+ onMouseDown: () => this.stopStream()
2429
+ }
2430
+ ), this.streamData.isLive && this.streamData.hlsUrl && /* @__PURE__ */ import_react_ecs4.default.createElement(
2431
+ import_react_ecs4.Button,
2432
+ {
2433
+ uiTransform: { width: this.s(100), height: this.s(UI_DIMENSIONS.admin.buttonHeightSmall), margin: 4 },
2434
+ uiBackground: { color: C.cyan },
2435
+ value: "Play on Screen",
2436
+ fontSize: t.buttonSmall,
2437
+ color: C.text,
2438
+ onMouseDown: () => this.config.onVideoPlay?.(this.streamData.hlsUrl)
2439
+ }
2440
+ )), /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.UiEntity, { uiTransform: { flexDirection: "row", flexWrap: "wrap", margin: { bottom: 6 } } }, /* @__PURE__ */ import_react_ecs4.default.createElement(
2441
+ import_react_ecs4.Button,
2442
+ {
2443
+ uiTransform: { width: this.s(80), height: this.s(UI_DIMENSIONS.admin.buttonHeightSmall), margin: 4 },
2444
+ uiBackground: { color: C.btn },
2445
+ value: "Refresh",
2446
+ fontSize: t.buttonSmall,
2447
+ color: C.text,
2448
+ onMouseDown: () => this.refreshStreamStatus()
2449
+ }
2450
+ ), /* @__PURE__ */ import_react_ecs4.default.createElement(
2451
+ import_react_ecs4.Button,
2452
+ {
2453
+ uiTransform: { width: this.s(95), height: this.s(UI_DIMENSIONS.admin.buttonHeightSmall), margin: 4 },
2454
+ uiBackground: { color: this.keyRotating ? C.btn : C.yellow },
2455
+ value: this.keyRotating ? "..." : "Rotate Key",
2456
+ fontSize: t.buttonSmall,
2457
+ color: C.text,
2458
+ onMouseDown: () => this.rotateStreamKey()
2459
+ }
2460
+ ), !this.streamData.isLive && /* @__PURE__ */ import_react_ecs4.default.createElement(
2461
+ import_react_ecs4.Button,
2462
+ {
2463
+ uiTransform: { width: this.s(70), height: this.s(UI_DIMENSIONS.admin.buttonHeightSmall), margin: 4 },
2464
+ uiBackground: { color: this.channelDeleting ? C.btn : C.red },
2465
+ value: this.channelDeleting ? "..." : "Delete",
2466
+ fontSize: t.buttonSmall,
2467
+ color: C.text,
2468
+ onMouseDown: () => this.deleteChannel()
2469
+ }
2470
+ )), this.streamControlStatus === "started" && /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.Label, { value: "Stream started - begin broadcasting in OBS", fontSize: t.status, color: C.green }), this.streamControlStatus === "stopped" && /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.Label, { value: "Stream stopped", fontSize: t.status, color: C.textDim }), this.keyRotateStatus === "success" && /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.Label, { value: "Key rotated! Update OBS", fontSize: t.status, color: C.green }), this.keyRotateStatus === "error" && /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.Label, { value: "Failed to rotate key", fontSize: t.status, color: C.red }), this.channelDeleteError && /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.Label, { value: this.channelDeleteError, fontSize: t.status, color: C.red }), this.streamControlStatus === "error" && /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.Label, { value: "Stream control failed", fontSize: t.status, 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: 14 } } }, /* @__PURE__ */ import_react_ecs4.default.createElement(
2471
+ import_react_ecs4.Input,
2472
+ {
2473
+ uiTransform: { width: this.s(230), height: this.s(UI_DIMENSIONS.admin.inputHeight) },
2474
+ uiBackground: { color: import_math5.Color4.create(0.15, 0.15, 0.2, 1) },
2475
+ placeholder: "Video URL...",
2476
+ placeholderColor: C.textDim,
2477
+ color: C.text,
2478
+ fontSize: t.input,
2479
+ value: this.customVideoUrl,
2480
+ onChange: (val) => {
2481
+ this.customVideoUrl = val;
2482
+ }
2483
+ }
2484
+ ), /* @__PURE__ */ import_react_ecs4.default.createElement(
2485
+ import_react_ecs4.Button,
2486
+ {
2487
+ uiTransform: { width: this.s(75), height: this.s(UI_DIMENSIONS.admin.inputHeight), margin: { left: 8 } },
2488
+ uiBackground: { color: C.green },
2489
+ value: "Play",
2490
+ fontSize: t.button,
2491
+ color: C.text,
2492
+ onMouseDown: () => {
2493
+ if (this.customVideoUrl) this.config.onVideoPlay?.(this.customVideoUrl);
2494
+ }
2495
+ }
2496
+ ), /* @__PURE__ */ import_react_ecs4.default.createElement(
2497
+ import_react_ecs4.Button,
2498
+ {
2499
+ uiTransform: { width: this.s(65), height: this.s(UI_DIMENSIONS.admin.inputHeight), margin: { left: 6 } },
2500
+ uiBackground: { color: C.btn },
2501
+ value: "Clear",
2502
+ fontSize: t.button,
2503
+ color: C.text,
2504
+ onMouseDown: () => {
2505
+ this.customVideoUrl = "";
2506
+ }
2507
+ }
2508
+ )), /* @__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: 14 } } }, /* @__PURE__ */ import_react_ecs4.default.createElement(
2509
+ import_react_ecs4.Button,
2510
+ {
2511
+ uiTransform: { width: this.s(75), height: this.s(UI_DIMENSIONS.admin.buttonHeightSmall), margin: 4 },
2512
+ uiBackground: { color: C.green },
2513
+ value: "Play",
2514
+ fontSize: t.button,
2515
+ color: C.text,
2516
+ onMouseDown: () => this.config.onCommand?.("videoPlay", { playing: true })
2517
+ }
2518
+ ), /* @__PURE__ */ import_react_ecs4.default.createElement(
2519
+ import_react_ecs4.Button,
2520
+ {
2521
+ uiTransform: { width: this.s(75), height: this.s(UI_DIMENSIONS.admin.buttonHeightSmall), margin: 4 },
2522
+ uiBackground: { color: C.red },
2523
+ value: "Stop",
2524
+ fontSize: t.button,
2525
+ color: C.text,
2526
+ onMouseDown: () => this.config.onVideoStop?.()
2527
+ }
2528
+ ), /* @__PURE__ */ import_react_ecs4.default.createElement(
2529
+ import_react_ecs4.Button,
2530
+ {
2531
+ uiTransform: { width: this.s(100), height: this.s(UI_DIMENSIONS.admin.buttonHeightSmall), margin: 4 },
2532
+ uiBackground: { color: C.btn },
2533
+ value: "Reset Default",
2534
+ fontSize: t.buttonSmall,
2535
+ color: C.text,
2536
+ onMouseDown: () => this.config.onCommand?.("videoClear", {})
2537
+ }
2538
+ )), /* @__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: 10 } } }, /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.Button, { uiTransform: { width: this.s(70), height: this.s(UI_DIMENSIONS.admin.buttonHeightSmall), margin: 4 }, uiBackground: { color: C.cyan }, value: "Play 1", fontSize: t.button, color: C.text, onMouseDown: () => this.playSlot("slot1") }), /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.Button, { uiTransform: { width: this.s(70), height: this.s(UI_DIMENSIONS.admin.buttonHeightSmall), margin: 4 }, uiBackground: { color: C.cyan }, value: "Play 2", fontSize: t.button, color: C.text, onMouseDown: () => this.playSlot("slot2") }), /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.Button, { uiTransform: { width: this.s(70), height: this.s(UI_DIMENSIONS.admin.buttonHeightSmall), margin: 4 }, uiBackground: { color: C.cyan }, value: "Play 3", fontSize: t.button, color: C.text, onMouseDown: () => this.playSlot("slot3") }), /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.Button, { uiTransform: { width: this.s(70), height: this.s(UI_DIMENSIONS.admin.buttonHeightSmall), margin: 4 }, uiBackground: { color: C.cyan }, value: "Play 4", fontSize: t.button, color: C.text, onMouseDown: () => this.playSlot("slot4") }), /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.Button, { uiTransform: { width: this.s(70), height: this.s(UI_DIMENSIONS.admin.buttonHeightSmall), margin: 4 }, uiBackground: { color: C.cyan }, value: "Play 5", fontSize: t.button, color: C.text, onMouseDown: () => this.playSlot("slot5") })), /* @__PURE__ */ import_react_ecs4.default.createElement(
2539
+ import_react_ecs4.Button,
2540
+ {
2541
+ uiTransform: { height: this.s(24) },
2542
+ uiBackground: { color: import_math5.Color4.create(0, 0, 0, 0) },
2543
+ value: "Edit slots at thestatic.tv \u2192",
2544
+ fontSize: t.labelSmall,
2545
+ color: C.cyan,
2546
+ onMouseDown: () => (0, import_RestrictedActions3.openExternalUrl)({ url: this.config.footerLink || `https://thestatic.tv/scene/${this.config.sceneId}` })
2547
+ }
2548
+ ));
2549
+ };
2550
+ this.ModTab = () => {
2551
+ const t = this.theme;
2552
+ return /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.UiEntity, { uiTransform: { flexDirection: "column", width: "100%", padding: 10 } }, this.modStatus === "loading" && /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.Label, { value: "Loading...", fontSize: t.status, color: C.yellow }), this.modStatus === "saved" && /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.Label, { value: "Saved!", fontSize: t.status, color: C.green }), this.modStatus === "error" && /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.Label, { value: "Error - check input", fontSize: t.status, 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: 14 } } }, /* @__PURE__ */ import_react_ecs4.default.createElement(
2553
+ import_react_ecs4.Input,
2554
+ {
2555
+ uiTransform: { width: this.s(230), height: this.s(UI_DIMENSIONS.admin.inputHeight) },
2556
+ uiBackground: { color: import_math5.Color4.create(0.15, 0.15, 0.2, 1) },
2557
+ placeholder: "Message to all players...",
2558
+ placeholderColor: C.textDim,
2559
+ color: C.text,
2560
+ fontSize: t.input,
2561
+ value: this.broadcastText,
2562
+ onChange: (val) => {
2563
+ this.broadcastText = val;
2564
+ }
2565
+ }
2566
+ ), /* @__PURE__ */ import_react_ecs4.default.createElement(
2567
+ import_react_ecs4.Button,
2568
+ {
2569
+ uiTransform: { width: this.s(65), height: this.s(UI_DIMENSIONS.admin.inputHeight), margin: { left: 8 } },
2570
+ uiBackground: { color: C.orange },
2571
+ value: "Send",
2572
+ fontSize: t.button,
2573
+ color: C.text,
2574
+ onMouseDown: () => this.sendBroadcast()
2575
+ }
2576
+ )), /* @__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: 14 } } }, /* @__PURE__ */ import_react_ecs4.default.createElement(
2577
+ import_react_ecs4.Button,
2578
+ {
2579
+ uiTransform: { width: this.s(130), height: this.s(UI_DIMENSIONS.admin.buttonHeight), margin: 4 },
2580
+ uiBackground: { color: C.red },
2581
+ value: "KICK ALL",
2582
+ fontSize: t.button,
2583
+ color: C.text,
2584
+ onMouseDown: () => {
2585
+ this.log("KICK ALL clicked");
2586
+ this.config.onCommand?.("kickAll", {});
2587
+ }
2588
+ }
2589
+ )), /* @__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: 6 } } }, this.sceneAdmins.length === 0 && this.modStatus !== "loading" && /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.Label, { value: "No scene admins", fontSize: t.labelSmall, color: C.textDim }), this.sceneAdmins.map((wallet, i) => /* @__PURE__ */ import_react_ecs4.default.createElement(
2590
+ import_react_ecs4.UiEntity,
2591
+ {
2592
+ key: `admin-${i}`,
2593
+ uiTransform: { flexDirection: "row", alignItems: "center", margin: { bottom: 5 }, width: "100%" }
2594
+ },
2595
+ /* @__PURE__ */ import_react_ecs4.default.createElement(
2596
+ import_react_ecs4.Label,
2597
+ {
2598
+ value: `${wallet.slice(0, 6)}...${wallet.slice(-4)}`,
2599
+ fontSize: t.label,
2600
+ color: C.text,
2601
+ uiTransform: { width: this.s(130) }
2602
+ }
2603
+ ),
2604
+ /* @__PURE__ */ import_react_ecs4.default.createElement(
2605
+ import_react_ecs4.Button,
2606
+ {
2607
+ uiTransform: { width: this.s(70), height: this.s(28), margin: { left: 8 } },
2608
+ uiBackground: { color: C.btn },
2609
+ value: "Remove",
2610
+ fontSize: t.buttonSmall,
2611
+ color: C.text,
2612
+ onMouseDown: () => this.removeSceneAdmin(wallet)
2613
+ }
2614
+ )
2615
+ ))), /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.UiEntity, { uiTransform: { flexDirection: "row", alignItems: "center", margin: { bottom: 14 } } }, /* @__PURE__ */ import_react_ecs4.default.createElement(
2616
+ import_react_ecs4.Input,
2617
+ {
2618
+ uiTransform: { width: this.s(230), height: this.s(UI_DIMENSIONS.admin.inputHeight) },
2619
+ uiBackground: { color: import_math5.Color4.create(0.15, 0.15, 0.2, 1) },
2620
+ placeholder: "0x... add admin",
2621
+ placeholderColor: C.textDim,
2622
+ color: C.text,
2623
+ fontSize: t.input,
2624
+ value: this.newAdminWallet,
2625
+ onChange: (val) => {
2626
+ this.newAdminWallet = val;
2627
+ }
2628
+ }
2629
+ ), /* @__PURE__ */ import_react_ecs4.default.createElement(
2630
+ import_react_ecs4.Button,
2631
+ {
2632
+ uiTransform: { width: this.s(60), height: this.s(UI_DIMENSIONS.admin.inputHeight), margin: { left: 8 } },
2633
+ uiBackground: { color: C.purple },
2634
+ value: "Add",
2635
+ fontSize: t.button,
2636
+ color: C.text,
2637
+ onMouseDown: () => {
2638
+ if (this.newAdminWallet) this.addSceneAdmin(this.newAdminWallet);
2639
+ }
2640
+ }
2641
+ )), /* @__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: 6 } } }, this.bannedWallets.length === 0 && this.modStatus !== "loading" && /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.Label, { value: "No banned wallets", fontSize: t.labelSmall, color: C.textDim }), this.bannedWallets.map((wallet, i) => /* @__PURE__ */ import_react_ecs4.default.createElement(
2642
+ import_react_ecs4.UiEntity,
2643
+ {
2644
+ key: `ban-${i}`,
2645
+ uiTransform: { flexDirection: "row", alignItems: "center", margin: { bottom: 5 }, width: "100%" }
2646
+ },
2647
+ /* @__PURE__ */ import_react_ecs4.default.createElement(
2648
+ import_react_ecs4.Label,
2649
+ {
2650
+ value: `${wallet.slice(0, 6)}...${wallet.slice(-4)}`,
2651
+ fontSize: t.label,
2652
+ color: C.red,
2653
+ uiTransform: { width: this.s(130) }
2654
+ }
2655
+ ),
2656
+ /* @__PURE__ */ import_react_ecs4.default.createElement(
2657
+ import_react_ecs4.Button,
2658
+ {
2659
+ uiTransform: { width: this.s(70), height: this.s(28), margin: { left: 8 } },
2660
+ uiBackground: { color: C.green },
2661
+ value: "Unban",
2662
+ fontSize: t.buttonSmall,
2663
+ color: C.text,
2664
+ onMouseDown: () => this.unbanWallet(wallet)
2665
+ }
2666
+ )
2667
+ ))), /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.UiEntity, { uiTransform: { flexDirection: "row", alignItems: "center", margin: { bottom: 10 } } }, /* @__PURE__ */ import_react_ecs4.default.createElement(
2668
+ import_react_ecs4.Input,
2669
+ {
2670
+ uiTransform: { width: this.s(230), height: this.s(UI_DIMENSIONS.admin.inputHeight) },
2671
+ uiBackground: { color: import_math5.Color4.create(0.15, 0.15, 0.2, 1) },
2672
+ placeholder: "0x... ban wallet",
2673
+ placeholderColor: C.textDim,
2674
+ color: C.text,
2675
+ fontSize: t.input,
2676
+ value: this.newBanWallet,
2677
+ onChange: (val) => {
2678
+ this.newBanWallet = val;
2679
+ }
2680
+ }
2681
+ ), /* @__PURE__ */ import_react_ecs4.default.createElement(
2682
+ import_react_ecs4.Button,
2683
+ {
2684
+ uiTransform: { width: this.s(60), height: this.s(UI_DIMENSIONS.admin.inputHeight), margin: { left: 8 } },
2685
+ uiBackground: { color: C.red },
2686
+ value: "Ban",
2687
+ fontSize: t.button,
2688
+ color: C.text,
2689
+ onMouseDown: () => {
2690
+ if (this.newBanWallet) this.banWallet(this.newBanWallet);
2691
+ }
2692
+ }
2693
+ )), /* @__PURE__ */ import_react_ecs4.default.createElement(
2694
+ import_react_ecs4.Button,
2695
+ {
2696
+ uiTransform: { height: this.s(24) },
2697
+ uiBackground: { color: import_math5.Color4.create(0, 0, 0, 0) },
2698
+ value: "Manage at thestatic.tv \u2192",
2699
+ fontSize: t.labelSmall,
2700
+ color: C.cyan,
2701
+ onMouseDown: () => (0, import_RestrictedActions3.openExternalUrl)({ url: this.config.footerLink || `https://thestatic.tv/scene/${this.config.sceneId}` })
2702
+ }
2703
+ ));
2704
+ };
2705
+ /**
2706
+ * Get the React-ECS component for the admin panel
2707
+ */
2708
+ this.getComponent = () => {
2709
+ if (!this.isAdmin) return null;
2710
+ const t = this.theme;
2711
+ const tabs = [];
2712
+ if (this.config.sceneTabs && this.config.sceneTabs.length > 0) {
2713
+ this.config.sceneTabs.forEach((tab) => tabs.push({ label: tab.label, id: tab.id }));
2714
+ }
2715
+ if (this.config.showVideoTab !== false) {
2716
+ tabs.push({ label: "VIDEO", id: "video" });
2717
+ }
2718
+ if (this.config.showModTab !== false && this.isOwner) {
2719
+ tabs.push({ label: "MOD", id: "mod" });
2720
+ }
2721
+ if (!tabs.find((tab) => tab.id === this.activeTab) && tabs.length > 0) {
2722
+ this.activeTab = tabs[0].id;
2723
+ }
2724
+ return /* @__PURE__ */ import_react_ecs4.default.createElement(
2725
+ import_react_ecs4.UiEntity,
2726
+ {
2727
+ uiTransform: {
2728
+ width: "100%",
2729
+ height: "100%",
2730
+ positionType: "absolute"
2731
+ }
2732
+ },
2733
+ /* @__PURE__ */ import_react_ecs4.default.createElement(
2734
+ import_react_ecs4.UiEntity,
2735
+ {
2736
+ uiTransform: {
2737
+ position: { right: 20 + this.s(100) + 10 + this.s(100) + 10, bottom: 10 },
2738
+ positionType: "absolute"
2739
+ }
2740
+ },
2741
+ /* @__PURE__ */ import_react_ecs4.default.createElement(
2742
+ import_react_ecs4.Button,
2743
+ {
2744
+ uiTransform: { width: this.s(100), height: this.s(45) },
2745
+ uiBackground: { color: this.panelOpen ? C.btn : C.header },
2746
+ value: this.panelOpen ? "CLOSE" : "ADMIN",
2747
+ fontSize: this.s(14),
2748
+ color: C.text,
2749
+ onMouseDown: () => this.toggle()
2750
+ }
2751
+ )
2752
+ ),
2753
+ this.panelOpen && /* @__PURE__ */ import_react_ecs4.default.createElement(
2754
+ import_react_ecs4.UiEntity,
2755
+ {
2756
+ key: `admin-panel-${this.client.uiScale}`,
2757
+ uiTransform: {
2758
+ width: this.s(UI_DIMENSIONS.admin.width),
2759
+ height: this.s(UI_DIMENSIONS.admin.height),
2760
+ maxHeight: this.s(UI_DIMENSIONS.admin.maxHeight),
2761
+ position: { right: UI_DIMENSIONS.chat.right + this.s(UI_DIMENSIONS.chat.width) + 10, bottom: UI_DIMENSIONS.admin.bottom },
2762
+ positionType: "absolute",
2763
+ flexDirection: "column"
2764
+ },
2765
+ uiBackground: { color: C.bg }
2766
+ },
2767
+ /* @__PURE__ */ import_react_ecs4.default.createElement(
2768
+ import_react_ecs4.UiEntity,
2769
+ {
2770
+ uiTransform: {
2771
+ width: "100%",
2772
+ height: this.s(UI_DIMENSIONS.admin.headerHeight),
2773
+ justifyContent: "space-between",
2774
+ alignItems: "center",
2775
+ flexDirection: "row",
2776
+ padding: { left: 12, right: 8 }
2777
+ },
2778
+ uiBackground: { color: C.header }
2779
+ },
2780
+ /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.Label, { value: this.config.title || "ADMIN PANEL", fontSize: t.header, color: C.text }),
2781
+ /* @__PURE__ */ import_react_ecs4.default.createElement(
2782
+ import_react_ecs4.Button,
2783
+ {
2784
+ uiTransform: { width: this.s(28), height: this.s(28) },
2785
+ uiBackground: { color: import_math5.Color4.create(0, 0, 0, 0.3) },
2786
+ value: "X",
2787
+ fontSize: t.header,
2788
+ color: THEME.colors.red,
2789
+ onMouseDown: () => this.hide()
2790
+ }
2791
+ )
2792
+ ),
2793
+ /* @__PURE__ */ import_react_ecs4.default.createElement(import_react_ecs4.UiEntity, { uiTransform: { width: "100%", height: this.s(UI_DIMENSIONS.admin.tabHeight), flexDirection: "row" } }, tabs.map((tab) => /* @__PURE__ */ import_react_ecs4.default.createElement(this.TabBtn, { label: tab.label, tab: tab.id }))),
2794
+ /* @__PURE__ */ import_react_ecs4.default.createElement(
2795
+ import_react_ecs4.UiEntity,
2796
+ {
2797
+ uiTransform: {
2798
+ width: "100%",
2799
+ flexGrow: 1,
2800
+ overflow: "scroll",
2801
+ flexDirection: "column"
2802
+ }
2803
+ },
2804
+ this.activeTab === "video" && /* @__PURE__ */ import_react_ecs4.default.createElement(this.VideoTab, null),
2805
+ this.activeTab === "mod" && this.isOwner && /* @__PURE__ */ import_react_ecs4.default.createElement(this.ModTab, null),
2806
+ this.config.sceneTabs?.map((tab) => this.activeTab === tab.id && tab.render())
2807
+ ),
2808
+ /* @__PURE__ */ import_react_ecs4.default.createElement(
2809
+ import_react_ecs4.UiEntity,
2810
+ {
2811
+ uiTransform: {
2812
+ width: "100%",
2813
+ height: this.s(UI_DIMENSIONS.admin.footerHeight),
2814
+ justifyContent: "center",
2815
+ alignItems: "center"
2816
+ },
2817
+ uiBackground: { color: C.section }
2818
+ },
2819
+ /* @__PURE__ */ import_react_ecs4.default.createElement(
2820
+ import_react_ecs4.Button,
2821
+ {
2822
+ uiTransform: { height: this.s(24) },
2823
+ uiBackground: { color: import_math5.Color4.create(0, 0, 0, 0) },
2824
+ value: `thestatic.tv/scene/${this.config.sceneId} \u2192`,
2825
+ fontSize: t.labelSmall,
2826
+ color: C.cyan,
2827
+ onMouseDown: () => (0, import_RestrictedActions3.openExternalUrl)({ url: this.config.footerLink || `https://thestatic.tv/scene/${this.config.sceneId}` })
2828
+ }
2829
+ )
2830
+ )
2831
+ )
2832
+ );
2833
+ };
2834
+ this.client = client;
2835
+ if (!config.sceneId) {
2836
+ throw new Error("[AdminPanel] sceneId is required");
2837
+ }
2838
+ this.config = {
2839
+ showVideoTab: true,
2840
+ showModTab: true,
2841
+ title: "ADMIN PANEL",
2842
+ debug: false,
2843
+ ...config,
2844
+ sceneId: config.sceneId
2845
+ // Ensure sceneId is set
2846
+ };
2847
+ this.baseUrl = client.getBaseUrl();
2848
+ if (config.headerColor) {
2849
+ C.header = import_math5.Color4.create(
2850
+ config.headerColor.r,
2851
+ config.headerColor.g,
2852
+ config.headerColor.b,
2853
+ config.headerColor.a
2854
+ );
2855
+ }
2856
+ }
2857
+ log(msg, ...args) {
2858
+ if (this.config.debug) {
2859
+ console.log(`[AdminPanel] ${msg}`, ...args);
2860
+ }
2861
+ }
2862
+ /** Scale a dimension by shared uiScale */
2863
+ s(value) {
2864
+ return Math.round(value * this.client.uiScale);
2865
+ }
2866
+ /** Get scaled theme */
2867
+ get theme() {
2868
+ return scaleAdminTheme(DEFAULT_ADMIN_THEME, this.client.uiScale);
2869
+ }
2870
+ /**
2871
+ * Initialize the admin panel - checks admin status and fetches video state
2872
+ */
2873
+ async init() {
2874
+ await this.checkAdminStatus();
2875
+ await this.fetchVideoState();
2876
+ this.autoPlayDefault();
2877
+ this.log("Initialized");
2878
+ }
2879
+ /**
2880
+ * Check if current player is an admin for this scene
2881
+ */
2882
+ async checkAdminStatus() {
2883
+ const player = (0, import_players2.getPlayer)();
2884
+ if (!player?.userId) {
2885
+ this.log("No player data yet");
2886
+ return;
2887
+ }
2888
+ this.playerWallet = player.userId;
2889
+ if (this.config.forceAdmin) {
2890
+ this.isAdmin = true;
2891
+ this.isOwner = true;
2892
+ this.log("Admin status FORCED (forceAdmin: true)");
2893
+ return;
2894
+ }
2895
+ try {
2896
+ const res = await fetch(
2897
+ `${this.baseUrl}/scene/${this.config.sceneId}/admin-check?wallet=${player.userId}`
2898
+ );
2899
+ if (res.ok) {
2900
+ const data = await res.json();
2901
+ this.isAdmin = data.showButton ?? data.hasAccess;
2902
+ this.isOwner = data.isOwner || data.isSceneAdmin;
2903
+ if (data.isBanned) {
2904
+ this.config.onBroadcast?.("You have been banned from this scene.");
2905
+ this.banKickPlayer();
2906
+ this.log("Player is banned - kicking");
2907
+ }
2908
+ this.log("Admin status:", this.isAdmin, "Owner:", this.isOwner);
2909
+ }
2910
+ } catch (err) {
2911
+ this.log("Admin check error:", err);
2912
+ }
2913
+ }
2914
+ /**
2915
+ * Toggle the admin panel open/closed
2916
+ */
2917
+ toggle() {
2918
+ if (this.panelOpen) {
2919
+ this.hide();
2920
+ } else {
2921
+ this.show();
2922
+ }
2923
+ }
2924
+ /**
2925
+ * Show the admin panel
2926
+ */
2927
+ show() {
2928
+ if (this.panelOpen) return;
2929
+ this.client.closeOtherPanels("admin");
2930
+ this.panelOpen = true;
2931
+ if (this.activeTab === "video") {
2932
+ this.startStreamPolling();
2933
+ }
2934
+ }
2935
+ /**
2936
+ * Hide the admin panel
2937
+ */
2938
+ hide() {
2939
+ if (!this.panelOpen) return;
2940
+ this.panelOpen = false;
2941
+ this.stopStreamPolling();
2942
+ }
2943
+ /**
2944
+ * Check if the panel is currently open
2945
+ */
2946
+ get isOpen() {
2947
+ return this.panelOpen;
2948
+ }
2949
+ /**
2950
+ * Check if current user has admin access
2951
+ */
2952
+ get hasAccess() {
2953
+ return this.isAdmin;
2954
+ }
2955
+ /**
2956
+ * Register a custom scene tab (Pro tier)
2957
+ */
2958
+ registerSceneTab(tab) {
2959
+ if (!this.config.sceneTabs) {
2960
+ this.config.sceneTabs = [];
2961
+ }
2962
+ this.config.sceneTabs.push(tab);
2963
+ this.log("Registered scene tab:", tab.label);
2964
+ }
2965
+ // --- Stream Polling ---
2966
+ startStreamPolling() {
2967
+ if (this.pollIntervalId !== null) return;
2968
+ this.pollIntervalId = dclSetInterval(() => {
2969
+ if (this.activeTab === "video" && this.panelOpen && this.streamData?.hasChannel) {
2970
+ this.refreshStreamStatus();
2971
+ }
2972
+ }, 1e4);
2973
+ this.log("Stream polling started");
2974
+ }
2975
+ stopStreamPolling() {
2976
+ if (this.pollIntervalId !== null) {
2977
+ dclClearInterval(this.pollIntervalId);
2978
+ this.pollIntervalId = null;
2979
+ this.log("Stream polling stopped");
2980
+ }
2981
+ }
2982
+ // --- Stream API Calls ---
2983
+ async fetchStreamData() {
2984
+ if (this.streamFetched || !this.playerWallet) return;
2985
+ try {
2986
+ const res = await fetch(
2987
+ `${this.baseUrl}/scene/${this.config.sceneId}/stream?wallet=${this.playerWallet}`
2988
+ );
2989
+ if (res.ok) {
2990
+ this.streamData = await res.json();
2991
+ this.streamFetched = true;
2992
+ this.log("Stream data fetched:", this.streamData?.channelId || "no channel");
2993
+ }
2994
+ } catch (err) {
2995
+ this.log("Stream fetch error:", err);
2996
+ }
2997
+ }
2998
+ async refreshStreamStatus() {
2999
+ if (!this.playerWallet) return;
3000
+ try {
3001
+ const res = await fetch(
3002
+ `${this.baseUrl}/scene/${this.config.sceneId}/stream?wallet=${this.playerWallet}`
3003
+ );
3004
+ if (res.ok) {
3005
+ this.streamData = await res.json();
3006
+ }
3007
+ } catch (err) {
3008
+ }
3009
+ }
3010
+ // --- Video State (Slots) ---
3011
+ async fetchVideoState() {
3012
+ if (this.videoStateFetched) return;
3013
+ try {
3014
+ const res = await fetch(
3015
+ `${this.baseUrl}/scene/${this.config.sceneId}/video-state`
3016
+ );
3017
+ if (res.ok) {
3018
+ const data = await res.json();
3019
+ this.videoState = {
3020
+ defaultSlot: data.defaultSlot || null,
3021
+ videoSlots: data.videoSlots || {}
3022
+ };
3023
+ this.videoStateFetched = true;
3024
+ this.log("Video state fetched:", this.videoState.defaultSlot || "no default");
3025
+ }
3026
+ } catch (err) {
3027
+ this.log("Video state fetch error:", err);
3028
+ }
3029
+ }
3030
+ /**
3031
+ * Play a video slot by ID - looks up URL and calls onVideoPlay
3032
+ */
3033
+ playSlot(slotId) {
3034
+ const slot = this.videoState?.videoSlots?.[slotId];
3035
+ if (slot?.url) {
3036
+ this.log("Playing slot:", slotId, slot.url);
3037
+ this.config.onVideoPlay?.(slot.url);
3038
+ } else {
3039
+ this.log("Slot has no URL:", slotId);
3040
+ this.config.onVideoSlotPlay?.(slotId);
3041
+ }
3042
+ }
3043
+ /**
3044
+ * Auto-play the default slot if configured
3045
+ */
3046
+ autoPlayDefault() {
3047
+ if (!this.videoState?.defaultSlot) return;
3048
+ const defaultSlot = this.videoState.defaultSlot;
3049
+ const slot = this.videoState.videoSlots?.[defaultSlot];
3050
+ if (slot?.url) {
3051
+ this.log("Auto-playing default slot:", defaultSlot);
3052
+ this.config.onVideoPlay?.(slot.url);
3053
+ }
3054
+ }
3055
+ async createChannel() {
3056
+ if (!this.playerWallet || this.channelCreating) return;
3057
+ this.channelCreating = true;
3058
+ this.channelCreateError = "";
3059
+ try {
3060
+ this.log("Creating channel for scene...");
3061
+ const res = await fetch(
3062
+ `${this.baseUrl}/scene/${this.config.sceneId}/stream/create`,
3063
+ {
3064
+ method: "POST",
3065
+ headers: { "Content-Type": "application/json" },
3066
+ body: JSON.stringify({ wallet: this.playerWallet })
3067
+ }
3068
+ );
3069
+ const data = await res.json();
3070
+ if (res.ok) {
3071
+ this.log("Channel created:", data.channel);
3072
+ this.streamFetched = false;
3073
+ await this.fetchStreamData();
3074
+ this.config.onBroadcast?.(`Channel created: ${data.channel.channelId}`);
3075
+ } else {
3076
+ this.channelCreateError = data.error || "Failed to create channel";
3077
+ this.log("Channel creation failed:", data.error);
3078
+ }
3079
+ } catch (err) {
3080
+ this.channelCreateError = "Network error creating channel";
3081
+ this.log("Channel creation error:", err);
3082
+ }
3083
+ this.channelCreating = false;
3084
+ }
3085
+ async claimTrial() {
3086
+ if (!this.playerWallet || this.trialClaiming) return;
3087
+ this.trialClaiming = true;
3088
+ this.trialClaimError = "";
3089
+ try {
3090
+ this.log("Claiming streaming trial...");
3091
+ const res = await fetch(
3092
+ `${this.baseUrl}/scene/${this.config.sceneId}/stream/claim-trial`,
3093
+ {
3094
+ method: "POST",
3095
+ headers: { "Content-Type": "application/json" },
3096
+ body: JSON.stringify({ wallet: this.playerWallet })
3097
+ }
3098
+ );
3099
+ const data = await res.json();
3100
+ if (res.ok) {
3101
+ this.log("Trial claimed:", data.channel);
3102
+ this.streamFetched = false;
3103
+ await this.fetchStreamData();
3104
+ this.config.onBroadcast?.(`4-hour streaming trial activated!`);
3105
+ } else {
3106
+ this.trialClaimError = data.error || "Failed to claim trial";
3107
+ this.log("Trial claim failed:", data.error);
3108
+ }
3109
+ } catch (err) {
3110
+ this.trialClaimError = "Network error claiming trial";
3111
+ this.log("Trial claim error:", err);
3112
+ }
3113
+ this.trialClaiming = false;
3114
+ }
3115
+ async deleteChannel() {
3116
+ if (!this.playerWallet || this.channelDeleting || !this.streamData?.hasChannel) return;
3117
+ this.channelDeleting = true;
3118
+ this.channelDeleteError = "";
3119
+ try {
3120
+ this.log("Deleting channel for scene...");
3121
+ const res = await fetch(
3122
+ `${this.baseUrl}/scene/${this.config.sceneId}/stream/delete`,
3123
+ {
3124
+ method: "POST",
3125
+ headers: { "Content-Type": "application/json" },
3126
+ body: JSON.stringify({ wallet: this.playerWallet })
3127
+ }
3128
+ );
3129
+ const data = await res.json();
3130
+ if (res.ok) {
3131
+ this.log("Channel deleted:", data);
3132
+ this.streamData = null;
3133
+ this.streamFetched = false;
3134
+ this.config.onBroadcast?.("Channel deleted successfully");
3135
+ } else {
3136
+ this.channelDeleteError = data.error || "Failed to delete channel";
3137
+ this.log("Channel deletion failed:", data.error);
3138
+ }
3139
+ } catch (err) {
3140
+ this.channelDeleteError = "Network error deleting channel";
3141
+ this.log("Channel deletion error:", err);
3142
+ }
3143
+ this.channelDeleting = false;
3144
+ }
3145
+ async startStream() {
3146
+ if (!this.playerWallet || this.streamControlling || !this.streamData?.hasChannel || this.streamData.isLive) return;
3147
+ if ((this.streamData.sparksBalance || 0) <= 0) {
3148
+ this.streamControlStatus = "error";
3149
+ this.config.onBroadcast?.("No Sparks - cannot start stream");
3150
+ return;
3151
+ }
3152
+ this.streamControlling = true;
3153
+ this.streamControlStatus = "";
3154
+ try {
3155
+ this.log("Starting stream...");
3156
+ const res = await fetch(
3157
+ `${this.baseUrl}/scene/${this.config.sceneId}/stream`,
3158
+ {
3159
+ method: "POST",
3160
+ headers: { "Content-Type": "application/json" },
3161
+ body: JSON.stringify({ action: "start", wallet: this.playerWallet })
3162
+ }
3163
+ );
3164
+ const data = await res.json();
3165
+ if (res.ok) {
3166
+ this.streamControlStatus = "started";
3167
+ this.log("Stream started");
3168
+ this.config.onBroadcast?.("Stream started!");
3169
+ this.streamFetched = false;
3170
+ await this.fetchStreamData();
3171
+ } else {
3172
+ this.streamControlStatus = "error";
3173
+ this.log("Start stream failed:", data.error);
3174
+ this.config.onBroadcast?.(data.error || "Failed to start");
3175
+ }
3176
+ } catch (err) {
3177
+ this.streamControlStatus = "error";
3178
+ this.log("Start stream error:", err);
3179
+ }
3180
+ this.streamControlling = false;
3181
+ }
3182
+ async stopStream() {
3183
+ if (!this.playerWallet || this.streamControlling || !this.streamData?.hasChannel || !this.streamData.isLive) return;
3184
+ this.streamControlling = true;
3185
+ this.streamControlStatus = "";
3186
+ try {
3187
+ this.log("Stopping stream...");
3188
+ const res = await fetch(
3189
+ `${this.baseUrl}/scene/${this.config.sceneId}/stream`,
3190
+ {
3191
+ method: "POST",
3192
+ headers: { "Content-Type": "application/json" },
3193
+ body: JSON.stringify({ action: "stop", wallet: this.playerWallet })
3194
+ }
3195
+ );
3196
+ const data = await res.json();
3197
+ if (res.ok) {
3198
+ this.streamControlStatus = "stopped";
3199
+ this.log("Stream stopped");
3200
+ this.config.onBroadcast?.("Stream stopped");
3201
+ this.streamFetched = false;
3202
+ await this.fetchStreamData();
3203
+ } else {
3204
+ this.streamControlStatus = "error";
3205
+ this.log("Stop stream failed:", data.error);
3206
+ }
3207
+ } catch (err) {
3208
+ this.streamControlStatus = "error";
3209
+ this.log("Stop stream error:", err);
3210
+ }
3211
+ this.streamControlling = false;
3212
+ }
3213
+ async rotateStreamKey() {
3214
+ if (!this.playerWallet || this.keyRotating || !this.streamData?.hasChannel) return;
3215
+ this.keyRotating = true;
3216
+ this.keyRotateStatus = "";
3217
+ try {
3218
+ this.log("Rotating stream key...");
3219
+ const res = await fetch(
3220
+ `${this.baseUrl}/scene/${this.config.sceneId}/stream`,
3221
+ {
3222
+ method: "POST",
3223
+ headers: { "Content-Type": "application/json" },
3224
+ body: JSON.stringify({ action: "rotateKey", wallet: this.playerWallet })
3225
+ }
3226
+ );
3227
+ const data = await res.json();
3228
+ if (res.ok) {
3229
+ this.keyRotateStatus = "success";
3230
+ this.log("Key rotated:", data.message);
3231
+ this.config.onBroadcast?.("Stream key rotated - update OBS settings");
3232
+ } else {
3233
+ this.keyRotateStatus = "error";
3234
+ this.log("Key rotation failed:", data.error);
3235
+ }
3236
+ } catch (err) {
3237
+ this.keyRotateStatus = "error";
3238
+ this.log("Key rotation error:", err);
3239
+ }
3240
+ this.keyRotating = false;
3241
+ }
3242
+ // --- Mod Tab API Calls ---
3243
+ async fetchModData() {
3244
+ if (!this.playerWallet) return;
3245
+ this.modStatus = "loading";
3246
+ try {
3247
+ const res = await fetch(
3248
+ `${this.baseUrl}/scene/${this.config.sceneId}/config?wallet=${this.playerWallet}`,
3249
+ { headers: { "Content-Type": "application/json" } }
3250
+ );
3251
+ if (res.ok) {
3252
+ const data = await res.json();
3253
+ this.sceneAdmins = data.sceneAdmins || [];
3254
+ this.bannedWallets = data.bannedWallets || [];
3255
+ this.modsFetched = true;
3256
+ this.modStatus = "";
3257
+ this.log("Fetched mod data:", { sceneAdmins: this.sceneAdmins, bannedWallets: this.bannedWallets });
3258
+ } else {
3259
+ this.modStatus = "error";
3260
+ }
3261
+ } catch (err) {
3262
+ this.modStatus = "error";
3263
+ this.log("Fetch mod data error:", err);
3264
+ }
3265
+ }
3266
+ async addSceneAdmin(wallet) {
3267
+ if (!wallet || !this.playerWallet) return;
3268
+ const normalized = wallet.toLowerCase().trim();
3269
+ if (!/^0x[a-f0-9]{40}$/i.test(normalized)) {
3270
+ this.modStatus = "error";
3271
+ return;
3272
+ }
3273
+ if (this.sceneAdmins.includes(normalized)) return;
3274
+ this.modStatus = "loading";
3275
+ try {
3276
+ const res = await fetch(
3277
+ `${this.baseUrl}/scene/${this.config.sceneId}/config`,
3278
+ {
3279
+ method: "POST",
3280
+ headers: { "Content-Type": "application/json" },
3281
+ body: JSON.stringify({
3282
+ sceneAdmins: [...this.sceneAdmins, normalized],
3283
+ wallet: this.playerWallet
3284
+ })
3285
+ }
3286
+ );
3287
+ if (res.ok) {
3288
+ this.sceneAdmins = [...this.sceneAdmins, normalized];
3289
+ this.newAdminWallet = "";
3290
+ this.modStatus = "saved";
3291
+ this.log("Added scene admin:", normalized);
3292
+ } else {
3293
+ this.modStatus = "error";
3294
+ }
3295
+ } catch (err) {
3296
+ this.modStatus = "error";
3297
+ this.log("Add admin error:", err);
3298
+ }
3299
+ }
3300
+ async removeSceneAdmin(wallet) {
3301
+ if (!wallet || !this.playerWallet) return;
3302
+ this.modStatus = "loading";
3303
+ try {
3304
+ const res = await fetch(
3305
+ `${this.baseUrl}/scene/${this.config.sceneId}/config`,
3306
+ {
3307
+ method: "POST",
3308
+ headers: { "Content-Type": "application/json" },
3309
+ body: JSON.stringify({
3310
+ sceneAdmins: this.sceneAdmins.filter((w) => w !== wallet),
3311
+ wallet: this.playerWallet
3312
+ })
3313
+ }
3314
+ );
3315
+ if (res.ok) {
3316
+ this.sceneAdmins = this.sceneAdmins.filter((w) => w !== wallet);
3317
+ this.modStatus = "saved";
3318
+ this.log("Removed scene admin:", wallet);
3319
+ } else {
3320
+ this.modStatus = "error";
3321
+ }
3322
+ } catch (err) {
3323
+ this.modStatus = "error";
3324
+ this.log("Remove admin error:", err);
3325
+ }
3326
+ }
3327
+ async banWallet(wallet) {
3328
+ if (!wallet || !this.playerWallet) return;
3329
+ const normalized = wallet.toLowerCase().trim();
3330
+ if (!/^0x[a-f0-9]{40}$/i.test(normalized)) {
3331
+ this.modStatus = "error";
3332
+ return;
3333
+ }
3334
+ if (this.bannedWallets.includes(normalized)) return;
3335
+ this.modStatus = "loading";
3336
+ try {
3337
+ const res = await fetch(
3338
+ `${this.baseUrl}/scene/${this.config.sceneId}/config`,
3339
+ {
3340
+ method: "POST",
3341
+ headers: { "Content-Type": "application/json" },
3342
+ body: JSON.stringify({
3343
+ bannedWallets: [...this.bannedWallets, normalized],
3344
+ wallet: this.playerWallet
3345
+ })
3346
+ }
3347
+ );
3348
+ if (res.ok) {
3349
+ this.bannedWallets = [...this.bannedWallets, normalized];
3350
+ this.newBanWallet = "";
3351
+ this.modStatus = "saved";
3352
+ this.log("Banned wallet:", normalized);
3353
+ this.config.onCommand?.("kickBanned", { wallet: normalized });
3354
+ } else {
3355
+ this.modStatus = "error";
3356
+ }
3357
+ } catch (err) {
3358
+ this.modStatus = "error";
3359
+ this.log("Ban error:", err);
3360
+ }
3361
+ }
3362
+ async unbanWallet(wallet) {
3363
+ if (!wallet || !this.playerWallet) return;
3364
+ this.modStatus = "loading";
3365
+ try {
3366
+ const res = await fetch(
3367
+ `${this.baseUrl}/scene/${this.config.sceneId}/config`,
3368
+ {
3369
+ method: "POST",
3370
+ headers: { "Content-Type": "application/json" },
3371
+ body: JSON.stringify({
3372
+ bannedWallets: this.bannedWallets.filter((w) => w !== wallet),
3373
+ wallet: this.playerWallet
3374
+ })
3375
+ }
3376
+ );
3377
+ if (res.ok) {
3378
+ this.bannedWallets = this.bannedWallets.filter((w) => w !== wallet);
3379
+ this.modStatus = "saved";
3380
+ this.log("Unbanned wallet:", wallet);
3381
+ } else {
3382
+ this.modStatus = "error";
3383
+ }
3384
+ } catch (err) {
3385
+ this.modStatus = "error";
3386
+ this.log("Unban error:", err);
3387
+ }
3388
+ }
3389
+ banKickPlayer() {
3390
+ (0, import_RestrictedActions3.movePlayerTo)({ newRelativePosition: BAN_KICK_POSITION });
3391
+ this.log("Player ban-kicked");
3392
+ }
3393
+ sendBroadcast() {
3394
+ if (!this.broadcastText.trim()) return;
3395
+ this.config.onBroadcast?.(this.broadcastText.trim());
3396
+ this.broadcastText = "";
3397
+ this.log("Broadcast sent");
3398
+ }
3399
+ setActiveTab(tab) {
3400
+ const previousTab = this.activeTab;
3401
+ this.activeTab = tab;
3402
+ if (tab === "mod" && !this.modsFetched && this.isOwner) {
3403
+ this.fetchModData();
3404
+ }
3405
+ if (tab === "video" && previousTab !== "video") {
3406
+ this.startStreamPolling();
3407
+ } else if (tab !== "video" && previousTab === "video") {
3408
+ this.stopStreamPolling();
3409
+ }
3410
+ }
3411
+ };
3412
+
3413
+ // src/ui/ui-renderer.tsx
3414
+ var import_react_ecs5 = __toESM(require("@dcl/sdk/react-ecs"));
3415
+ var import_math6 = require("@dcl/sdk/math");
3416
+ var import_ecs2 = require("@dcl/sdk/ecs");
3417
+ var notificationText = "";
3418
+ var notificationVisible = false;
3419
+ var notificationEndTime = 0;
3420
+ var notificationInitialized = false;
3421
+ function showNotification(message, durationMs = 5e3) {
3422
+ notificationText = message;
3423
+ notificationVisible = true;
3424
+ notificationEndTime = Date.now() + durationMs;
3425
+ }
3426
+ function initNotificationSystem() {
3427
+ if (notificationInitialized) return;
3428
+ notificationInitialized = true;
3429
+ import_ecs2.engine.addSystem(() => {
3430
+ if (notificationVisible && Date.now() > notificationEndTime) {
3431
+ notificationVisible = false;
3432
+ }
3433
+ });
3434
+ }
3435
+ function NotificationBanner() {
3436
+ if (!notificationVisible) return null;
3437
+ return /* @__PURE__ */ import_react_ecs5.default.createElement(
3438
+ import_react_ecs5.UiEntity,
3439
+ {
3440
+ uiTransform: {
3441
+ positionType: "absolute",
3442
+ position: { top: 80 },
3443
+ width: 500,
3444
+ height: 60,
3445
+ padding: 16,
3446
+ alignSelf: "center",
3447
+ justifyContent: "center",
3448
+ alignItems: "center"
3449
+ },
3450
+ uiBackground: { color: import_math6.Color4.create(0.1, 0.1, 0.15, 0.95) }
3451
+ },
3452
+ /* @__PURE__ */ import_react_ecs5.default.createElement(
3453
+ import_react_ecs5.Label,
3454
+ {
3455
+ value: notificationText,
3456
+ fontSize: 18,
3457
+ color: import_math6.Color4.create(0, 1, 1, 1),
3458
+ textAlign: "middle-center"
3459
+ }
3460
+ )
3461
+ );
3462
+ }
3463
+ function createStaticUI(client) {
3464
+ initNotificationSystem();
3465
+ return function StaticUI() {
3466
+ const currentScale = client.uiScale;
3467
+ const guideComponent = client.guideUI?.getComponent() ?? null;
3468
+ const chatComponent = client.chatUI?.getComponent() ?? null;
3469
+ const adminComponent = client.adminPanel?.getComponent() ?? null;
3470
+ return /* @__PURE__ */ import_react_ecs5.default.createElement(
3471
+ import_react_ecs5.UiEntity,
3472
+ {
3473
+ key: `static-ui-root-${currentScale}`,
3474
+ uiTransform: {
3475
+ width: "100%",
3476
+ height: "100%",
3477
+ positionType: "absolute",
3478
+ flexDirection: "column",
3479
+ alignItems: "center"
3480
+ }
3481
+ },
3482
+ /* @__PURE__ */ import_react_ecs5.default.createElement(NotificationBanner, null),
3483
+ guideComponent,
3484
+ chatComponent,
3485
+ adminComponent
3486
+ );
3487
+ };
3488
+ }
3489
+ function setupStaticUI(client) {
3490
+ const StaticUI = createStaticUI(client);
3491
+ import_react_ecs5.ReactEcsRenderer.setUiRenderer(StaticUI);
3492
+ }
3493
+
2164
3494
  // src/StaticTVClient.ts
2165
3495
  var DEFAULT_BASE_URL = "https://thestatic.tv/api/v1/dcl";
2166
3496
  var KEY_TYPE_CHANNEL = "channel";
@@ -2173,34 +3503,41 @@ var StaticTVClient = class {
2173
3503
  *
2174
3504
  * @example
2175
3505
  * ```typescript
2176
- * // Full access with channel key
2177
- * const staticTV = new StaticTVClient({
2178
- * apiKey: 'dclk_your_channel_key_here',
2179
- * debug: true
2180
- * });
3506
+ * let staticTV: StaticTVClient
2181
3507
  *
2182
- * // Lite mode with scene key (visitors only)
2183
- * const staticTV = new StaticTVClient({
2184
- * apiKey: 'dcls_your_scene_key_here'
2185
- * });
3508
+ * export function main() {
3509
+ * // All keys use dcls_ prefix - features determined by subscription
3510
+ * staticTV = new StaticTVClient({
3511
+ * apiKey: 'dcls_your_key_here'
3512
+ * })
3513
+ * // Session tracking starts automatically!
3514
+ * }
2186
3515
  * ```
2187
3516
  */
2188
3517
  constructor(config) {
2189
3518
  this._keyType = null;
3519
+ this._keyId = null;
2190
3520
  this._disabled = false;
2191
- this._fullFeaturesEnabled = false;
2192
- /** Guide module - fetch channel lineup (full SDK only) */
3521
+ this._tier = "free";
3522
+ this._standardFeaturesEnabled = false;
3523
+ this._proFeaturesEnabled = false;
3524
+ this._pendingProConfig = null;
3525
+ /** Guide module - fetch channel lineup (standard/pro tier) */
2193
3526
  this.guide = null;
2194
- /** Session module - track visitor sessions (all keys, null when disabled) */
3527
+ /** Session module - track visitor sessions (all tiers, null when disabled) */
2195
3528
  this.session = null;
2196
- /** Heartbeat module - track video watching (full SDK only) */
3529
+ /** Heartbeat module - track video watching (standard/pro tier) */
2197
3530
  this.heartbeat = null;
2198
- /** Interactions module - like/follow channels (full SDK only) */
3531
+ /** Interactions module - like/follow channels (standard/pro tier) */
2199
3532
  this.interactions = null;
2200
- /** Guide UI module - channel browser UI (full SDK only) */
3533
+ /** Guide UI module - channel browser UI (standard/pro tier) */
2201
3534
  this.guideUI = null;
2202
- /** Chat UI module - real-time chat UI (full SDK only) */
3535
+ /** Chat UI module - real-time chat UI (standard/pro tier) */
2203
3536
  this.chatUI = null;
3537
+ /** Admin Panel module - Video/Mod tabs (pro tier only) */
3538
+ this.adminPanel = null;
3539
+ /** UI scale - fixed at 1.0. DCL's UI system auto-scales based on viewport. */
3540
+ this.uiScale = 1;
2204
3541
  this.config = {
2205
3542
  autoStartSession: true,
2206
3543
  sessionHeartbeatInterval: 3e4,
@@ -2210,7 +3547,6 @@ var StaticTVClient = class {
2210
3547
  };
2211
3548
  this.baseUrl = config.baseUrl || DEFAULT_BASE_URL;
2212
3549
  if (!config.apiKey) {
2213
- console.log("[StaticTV] No apiKey provided - tracking disabled. Scene will load normally.");
2214
3550
  this._disabled = true;
2215
3551
  this._keyType = null;
2216
3552
  this.session = null;
@@ -2226,14 +3562,15 @@ var StaticTVClient = class {
2226
3562
  } else if (config.apiKey.startsWith("dcls_")) {
2227
3563
  this._keyType = KEY_TYPE_SCENE;
2228
3564
  } else {
2229
- console.log("[StaticTV] Invalid apiKey format (must start with dclk_ or dcls_) - tracking disabled. Scene will load normally.");
3565
+ console.warn("[TheStatic] Invalid API key format - get your key at thestatic.tv/dashboard");
2230
3566
  this._disabled = true;
2231
3567
  this._keyType = null;
2232
3568
  return;
2233
3569
  }
2234
3570
  this.session = new SessionModule(this);
2235
3571
  if (this._keyType === KEY_TYPE_CHANNEL) {
2236
- this._initFullModules();
3572
+ this._tier = "standard";
3573
+ this._initStandardModules();
2237
3574
  }
2238
3575
  if (this.config.autoStartSession) {
2239
3576
  fetchUserData().then(() => {
@@ -2249,6 +3586,10 @@ var StaticTVClient = class {
2249
3586
  }
2250
3587
  this.log(`StaticTVClient initialized (${this._keyType} mode)`);
2251
3588
  }
3589
+ /** Get the API base URL (for internal module use) */
3590
+ getBaseUrl() {
3591
+ return this.baseUrl;
3592
+ }
2252
3593
  /**
2253
3594
  * Get the key type (channel, scene, or null if disabled)
2254
3595
  */
@@ -2262,11 +3603,23 @@ var StaticTVClient = class {
2262
3603
  return this._disabled;
2263
3604
  }
2264
3605
  /**
2265
- * Check if this is a lite client (no full features)
2266
- * Returns true until session confirms sdkType is 'full'
3606
+ * Get the current SDK tier (free, standard, or pro)
3607
+ */
3608
+ get tier() {
3609
+ return this._tier;
3610
+ }
3611
+ /**
3612
+ * Check if this is a free tier client (session tracking only)
3613
+ * Returns true until session confirms a higher tier
3614
+ */
3615
+ get isFree() {
3616
+ return this._tier === "free";
3617
+ }
3618
+ /**
3619
+ * @deprecated Use `isFree` instead. Kept for backward compatibility.
2267
3620
  */
2268
3621
  get isLite() {
2269
- return !this._fullFeaturesEnabled;
3622
+ return this.isFree;
2270
3623
  }
2271
3624
  /**
2272
3625
  * Make an authenticated API request
@@ -2284,14 +3637,29 @@ var StaticTVClient = class {
2284
3637
  });
2285
3638
  }
2286
3639
  /**
2287
- * Log a message if debug is enabled
3640
+ * Log a debug message (only when debug: true)
2288
3641
  * @internal
2289
3642
  */
2290
3643
  log(message, ...args) {
2291
3644
  if (this.config.debug) {
2292
- console.log(`[StaticTV] ${message}`, ...args);
3645
+ console.log(`[TheStatic] ${message}`, ...args);
2293
3646
  }
2294
3647
  }
3648
+ /**
3649
+ * Log a warning (always shown)
3650
+ * @internal
3651
+ */
3652
+ warn(message) {
3653
+ console.warn(`[TheStatic] ${message}`);
3654
+ }
3655
+ /**
3656
+ * Log an error (always shown, user-friendly format)
3657
+ * @internal
3658
+ */
3659
+ error(message, err) {
3660
+ const errorDetail = err instanceof Error ? err.message : String(err || "");
3661
+ console.error(`[TheStatic] ${message}${errorDetail ? `: ${errorDetail}` : ""}`);
3662
+ }
2295
3663
  /**
2296
3664
  * Get the current configuration
2297
3665
  * @internal
@@ -2300,41 +3668,199 @@ var StaticTVClient = class {
2300
3668
  return this.config;
2301
3669
  }
2302
3670
  /**
2303
- * Initialize full feature modules (guide, heartbeat, interactions, UI)
3671
+ * Initialize standard feature modules (guide, heartbeat, interactions, UI)
2304
3672
  * @internal
2305
3673
  */
2306
- _initFullModules() {
2307
- if (this._fullFeaturesEnabled) return;
3674
+ _initStandardModules() {
3675
+ if (this._standardFeaturesEnabled) return;
2308
3676
  this.guide = new GuideModule(this);
2309
3677
  this.heartbeat = new HeartbeatModule(this);
2310
3678
  this.interactions = new InteractionsModule(this);
2311
3679
  this.guideUI = new GuideUIModule(this, this.config.guideUI);
2312
3680
  this.chatUI = new ChatUIModule(this, this.config.chatUI);
2313
- this._fullFeaturesEnabled = true;
3681
+ this._standardFeaturesEnabled = true;
2314
3682
  this.chatUI.init().catch((err) => {
2315
3683
  this.log(`Chat init failed: ${err}`);
2316
3684
  });
2317
- this.log("Full features enabled (guide, chat, heartbeat, interactions)");
3685
+ this.log("Standard features enabled (guide, chat, heartbeat, interactions)");
2318
3686
  }
2319
3687
  /**
2320
- * Called by SessionModule when server confirms sdkType is 'full'
2321
- * Enables guide, chat, heartbeat, and interactions modules
3688
+ * Initialize pro feature modules (admin panel)
3689
+ * @internal
3690
+ */
3691
+ _initProModules() {
3692
+ if (this._proFeaturesEnabled || !this._pendingProConfig) return;
3693
+ const configWithDefaults = {
3694
+ ...this._pendingProConfig,
3695
+ sceneId: this._pendingProConfig.sceneId || this._keyId || void 0
3696
+ };
3697
+ if (!configWithDefaults.sceneId) {
3698
+ this.log("Pro features: No sceneId and no keyId available - admin panel disabled");
3699
+ return;
3700
+ }
3701
+ this.adminPanel = new AdminPanelUIModule(this, configWithDefaults);
3702
+ this._proFeaturesEnabled = true;
3703
+ this.adminPanel.init().catch((err) => {
3704
+ this.log(`Admin panel init failed: ${err}`);
3705
+ });
3706
+ this.log(`Pro features enabled (admin panel) - sceneId: ${configWithDefaults.sceneId}`);
3707
+ }
3708
+ /**
3709
+ * Called by SessionModule when server returns the tier
3710
+ * Enables modules based on tier level
3711
+ * @internal
3712
+ */
3713
+ _enableFeaturesForTier(tier, keyId) {
3714
+ this._tier = tier;
3715
+ if (keyId) {
3716
+ this._keyId = keyId;
3717
+ }
3718
+ if (tier === "standard" || tier === "pro") {
3719
+ this._initStandardModules();
3720
+ }
3721
+ if (tier === "pro" && this._pendingProConfig) {
3722
+ this._initProModules();
3723
+ }
3724
+ }
3725
+ /**
3726
+ * @deprecated Use `_enableFeaturesForTier` instead
2322
3727
  * @internal
2323
3728
  */
2324
3729
  _enableFullFeatures() {
2325
- this._initFullModules();
3730
+ this._enableFeaturesForTier("standard");
2326
3731
  }
2327
3732
  /**
2328
- * Check if full features are enabled (server confirmed sdkType: 'full')
3733
+ * Check if standard features are enabled (guide, chat, etc.)
3734
+ */
3735
+ get hasStandardFeatures() {
3736
+ return this._standardFeaturesEnabled;
3737
+ }
3738
+ /**
3739
+ * @deprecated Use `hasStandardFeatures` instead
2329
3740
  */
2330
3741
  get hasFullFeatures() {
2331
- return this._fullFeaturesEnabled;
3742
+ return this._standardFeaturesEnabled;
3743
+ }
3744
+ /**
3745
+ * Check if pro features are enabled (admin panel)
3746
+ */
3747
+ get hasProFeatures() {
3748
+ return this._proFeaturesEnabled;
2332
3749
  }
2333
3750
  /**
2334
- * Get the SDK type (lite or full) - only available after session starts
3751
+ * @deprecated Use `tier` instead
2335
3752
  */
2336
3753
  get sdkType() {
2337
- return this.session?.sdkType || "lite";
3754
+ return this._tier === "free" ? "lite" : "full";
3755
+ }
3756
+ /**
3757
+ * Configure Pro features (Admin Panel with Video + Mod tabs)
3758
+ * Call this after creating the client to configure admin panel.
3759
+ * The panel will auto-enable when server confirms Pro tier.
3760
+ *
3761
+ * @param config Admin panel configuration (optional - defaults work for basic usage)
3762
+ *
3763
+ * @example
3764
+ * ```typescript
3765
+ * const staticTV = new StaticTVClient({ apiKey: 'dcls_...' })
3766
+ *
3767
+ * // Simplest usage - just enable with callbacks:
3768
+ * staticTV.enableProFeatures({
3769
+ * onVideoPlay: (url) => videoPlayer.play(url),
3770
+ * onVideoStop: () => videoPlayer.stop()
3771
+ * })
3772
+ *
3773
+ * // Advanced usage - custom sceneId and title:
3774
+ * staticTV.enableProFeatures({
3775
+ * sceneId: 'my-scene', // optional - defaults to API key ID
3776
+ * title: 'MY SCENE ADMIN',
3777
+ * onVideoPlay: (url) => videoPlayer.play(url),
3778
+ * onVideoStop: () => videoPlayer.stop(),
3779
+ * onBroadcast: (text) => showNotification(text)
3780
+ * })
3781
+ * ```
3782
+ */
3783
+ enableProFeatures(config = {}) {
3784
+ if (this._proFeaturesEnabled) {
3785
+ this.log("Pro features already enabled");
3786
+ return;
3787
+ }
3788
+ this._pendingProConfig = config;
3789
+ if (this._tier === "pro") {
3790
+ this._initProModules();
3791
+ } else {
3792
+ this.log("Pro features configured - will enable when Pro tier is confirmed");
3793
+ }
3794
+ }
3795
+ /**
3796
+ * Register a custom scene tab for the admin panel (Pro tier)
3797
+ * Must call enableProFeatures() first.
3798
+ *
3799
+ * @param tab The tab definition with label, id, and render function
3800
+ *
3801
+ * @example
3802
+ * ```typescript
3803
+ * staticTV.registerSceneTab({
3804
+ * label: 'LIGHTS',
3805
+ * id: 'lights',
3806
+ * render: () => <MyLightsControls />
3807
+ * })
3808
+ * ```
3809
+ */
3810
+ registerSceneTab(tab) {
3811
+ if (!this.adminPanel) {
3812
+ this.log("Cannot register scene tab - call enableProFeatures() first");
3813
+ return;
3814
+ }
3815
+ this.adminPanel.registerSceneTab(tab);
3816
+ }
3817
+ /**
3818
+ * Close Admin/Guide panels (they share the same screen space)
3819
+ * Chat is independent and stays open.
3820
+ * @param except The panel that should stay open: 'admin' | 'guide'
3821
+ */
3822
+ closeOtherPanels(except) {
3823
+ if (except !== "guide" && this.guideUI?.isVisible) {
3824
+ this.guideUI.hide();
3825
+ }
3826
+ if (except !== "admin" && this.adminPanel?.isOpen) {
3827
+ this.adminPanel.hide();
3828
+ }
3829
+ }
3830
+ /**
3831
+ * Set up the UI renderer for all SDK panels
3832
+ * Call this in your scene's main() function to render Guide, Chat, Admin panels.
3833
+ * No need to create your own ui.tsx - the SDK handles everything.
3834
+ *
3835
+ * @example
3836
+ * ```typescript
3837
+ * const staticTV = new StaticTVClient({ apiKey: 'dcls_...' })
3838
+ *
3839
+ * export function main() {
3840
+ * staticTV.setupUI()
3841
+ * // That's it! All panels will render automatically
3842
+ * }
3843
+ * ```
3844
+ */
3845
+ setupUI() {
3846
+ setupStaticUI(this);
3847
+ this.log("UI renderer initialized");
3848
+ }
3849
+ /**
3850
+ * Show a notification message on screen
3851
+ * Works with both SDK-rendered UI and custom UI setups.
3852
+ *
3853
+ * @param message The message to display
3854
+ * @param durationMs How long to show (default 5000ms)
3855
+ *
3856
+ * @example
3857
+ * ```typescript
3858
+ * staticTV.showNotification('Stream started!')
3859
+ * staticTV.showNotification('Custom message', 10000) // 10 seconds
3860
+ * ```
3861
+ */
3862
+ showNotification(message, durationMs = 5e3) {
3863
+ showNotification(message, durationMs);
2338
3864
  }
2339
3865
  /**
2340
3866
  * Cleanup when done (call before scene unload)
@@ -2358,6 +3884,7 @@ var StaticTVClient = class {
2358
3884
  };
2359
3885
  // Annotate the CommonJS export names for ESM import in node:
2360
3886
  0 && (module.exports = {
3887
+ AdminPanelUIModule,
2361
3888
  ChatUIModule,
2362
3889
  GuideModule,
2363
3890
  GuideUIModule,
@@ -2369,5 +3896,7 @@ var StaticTVClient = class {
2369
3896
  StaticTVClient,
2370
3897
  fetchUserData,
2371
3898
  getPlayerDisplayName,
2372
- getPlayerWallet
3899
+ getPlayerWallet,
3900
+ setupStaticUI,
3901
+ showNotification
2373
3902
  });