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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -96,8 +96,8 @@ function getPlayerWallet() {
96
96
  return cachedWallet;
97
97
  }
98
98
  try {
99
- const { getPlayer: getPlayer2 } = __require("@dcl/sdk/players");
100
- const player = getPlayer2();
99
+ const { getPlayer: getPlayer3 } = __require("@dcl/sdk/players");
100
+ const player = getPlayer3();
101
101
  return player?.userId ?? null;
102
102
  } catch {
103
103
  return null;
@@ -108,8 +108,8 @@ function getPlayerDisplayName() {
108
108
  return cachedDisplayName;
109
109
  }
110
110
  try {
111
- const { getPlayer: getPlayer2 } = __require("@dcl/sdk/players");
112
- const player = getPlayer2();
111
+ const { getPlayer: getPlayer3 } = __require("@dcl/sdk/players");
112
+ const player = getPlayer3();
113
113
  return player?.name ?? null;
114
114
  } catch {
115
115
  return null;
@@ -132,7 +132,7 @@ function ensureTimerSystem() {
132
132
  try {
133
133
  timer.callback();
134
134
  } catch (e) {
135
- console.error("[StaticTV Timer] Callback error:", e);
135
+ console.error("[TheStatic] Timer error");
136
136
  }
137
137
  }
138
138
  }
@@ -173,7 +173,7 @@ function ensureTimeoutSystem() {
173
173
  try {
174
174
  timeout.callback();
175
175
  } catch (e) {
176
- console.error("[StaticTV Timer] Timeout callback error:", e);
176
+ console.error("[TheStatic] Timeout error");
177
177
  }
178
178
  }
179
179
  }
@@ -204,20 +204,41 @@ function dclClearTimeout(timeoutId) {
204
204
  }
205
205
 
206
206
  // src/modules/session.ts
207
+ function normalizeTier(tier) {
208
+ if (tier === "lite") return "free";
209
+ if (tier === "full") return "standard";
210
+ if (tier === "free" || tier === "standard" || tier === "pro") return tier;
211
+ return "free";
212
+ }
207
213
  var SessionModule = class {
208
214
  constructor(client) {
209
215
  this.sessionId = null;
216
+ this._keyId = null;
210
217
  this.heartbeatTimerId = null;
211
218
  this.isActive = false;
212
- this._sdkType = "lite";
219
+ this._tier = "free";
213
220
  this.client = client;
214
221
  }
215
222
  /**
216
- * Get the SDK type returned by the server
217
- * Determines what features are available (lite = sessions only, full = guide + chat + more)
223
+ * Get the API key ID (used as default sceneId for Pro users)
224
+ */
225
+ get keyId() {
226
+ return this._keyId;
227
+ }
228
+ /**
229
+ * Get the SDK tier returned by the server
230
+ * - free: Session tracking only
231
+ * - standard: Guide, Chat, Heartbeat, Interactions
232
+ * - pro: Everything + Admin Panel
233
+ */
234
+ get tier() {
235
+ return this._tier;
236
+ }
237
+ /**
238
+ * @deprecated Use `tier` instead. Returns mapped value for compatibility.
218
239
  */
219
240
  get sdkType() {
220
- return this._sdkType;
241
+ return this._tier === "free" ? "lite" : "full";
221
242
  }
222
243
  /**
223
244
  * Get the appropriate session endpoint based on key type
@@ -264,13 +285,12 @@ var SessionModule = class {
264
285
  });
265
286
  if (response.success && response.sessionId) {
266
287
  this.sessionId = response.sessionId;
288
+ this._keyId = response.keyId || null;
267
289
  this.isActive = true;
268
- this._sdkType = response.sdkType || "lite";
290
+ this._tier = normalizeTier(response.sdkType);
269
291
  this.startHeartbeat();
270
- this.client.log(`Session started: ${this.sessionId}, sdkType: ${this._sdkType}`);
271
- if (this._sdkType === "full") {
272
- this.client._enableFullFeatures();
273
- }
292
+ this.client.log(`Session started: ${this.sessionId}, tier: ${this._tier}`);
293
+ this.client._enableFeaturesForTier(this._tier, this._keyId);
274
294
  return this.sessionId;
275
295
  }
276
296
  return null;
@@ -558,7 +578,8 @@ var UI_DIMENSIONS = {
558
578
  // Guide UI - positioned to the left of chat (chat is 380px wide at right:20)
559
579
  guide: {
560
580
  width: 900,
561
- height: "55%",
581
+ height: 580,
582
+ // Numeric value for scaling (matches chat/admin)
562
583
  bottom: 55,
563
584
  right: 410,
564
585
  // 20 + 380 (chat width) + 10 (gap)
@@ -577,15 +598,33 @@ var UI_DIMENSIONS = {
577
598
  padding: 15
578
599
  }
579
600
  },
580
- // Chat UI - match m1d-hq-lifted dimensions
601
+ // Chat UI - positioned at right side
581
602
  chat: {
582
603
  width: 380,
583
604
  height: 580,
584
605
  bottom: 55,
585
606
  right: 20,
607
+ headerHeight: 40,
586
608
  messagesPerPage: 5,
587
609
  channelsPerPage: 6
588
610
  },
611
+ // Admin Panel - positioned left of chat
612
+ admin: {
613
+ width: 400,
614
+ height: 580,
615
+ // Match chat height
616
+ maxHeight: 700,
617
+ bottom: 55,
618
+ right: 410,
619
+ // 20 + 380 (chat width) + 10 (gap)
620
+ headerHeight: 48,
621
+ tabHeight: 40,
622
+ footerHeight: 32,
623
+ sectionHeadHeight: 28,
624
+ buttonHeight: 36,
625
+ buttonHeightSmall: 30,
626
+ inputHeight: 36
627
+ },
589
628
  // Shared
590
629
  closeButton: {
591
630
  size: 40,
@@ -593,18 +632,18 @@ var UI_DIMENSIONS = {
593
632
  }
594
633
  };
595
634
  var DEFAULT_CHAT_THEME = {
596
- header: 22,
635
+ header: 16,
597
636
  channelButton: 14,
598
637
  channelDropdown: 14,
599
- systemMessage: 14,
600
- chatUsername: 16,
638
+ systemMessage: 13,
639
+ chatUsername: 14,
601
640
  chatTimestamp: 11,
602
- chatMessage: 16,
603
- input: 16,
641
+ chatMessage: 14,
642
+ input: 14,
604
643
  sendButton: 14,
605
- userInfo: 14,
644
+ userInfo: 13,
606
645
  authStatus: 12,
607
- notification: 18,
646
+ notification: 16,
608
647
  closeButton: 16
609
648
  };
610
649
  function scaleChatTheme(theme, fontScale) {
@@ -624,6 +663,30 @@ function scaleChatTheme(theme, fontScale) {
624
663
  closeButton: Math.round(theme.closeButton * fontScale)
625
664
  };
626
665
  }
666
+ var DEFAULT_ADMIN_THEME = {
667
+ header: 16,
668
+ tabButton: 14,
669
+ sectionHead: 13,
670
+ label: 13,
671
+ labelSmall: 11,
672
+ button: 13,
673
+ buttonSmall: 11,
674
+ input: 13,
675
+ status: 12
676
+ };
677
+ function scaleAdminTheme(theme, fontScale) {
678
+ return {
679
+ header: Math.round(theme.header * fontScale),
680
+ tabButton: Math.round(theme.tabButton * fontScale),
681
+ sectionHead: Math.round(theme.sectionHead * fontScale),
682
+ label: Math.round(theme.label * fontScale),
683
+ labelSmall: Math.round(theme.labelSmall * fontScale),
684
+ button: Math.round(theme.button * fontScale),
685
+ buttonSmall: Math.round(theme.buttonSmall * fontScale),
686
+ input: Math.round(theme.input * fontScale),
687
+ status: Math.round(theme.status * fontScale)
688
+ };
689
+ }
627
690
 
628
691
  // src/ui/components.tsx
629
692
  import ReactEcs, { UiEntity, Label, Input } from "@dcl/sdk/react-ecs";
@@ -679,21 +742,21 @@ var PanelHeader = (props) => {
679
742
  }),
680
743
  ReactEcs.createElement(UiEntity, { key: "pos-spacer", uiTransform: { width: 6 } })
681
744
  ] : [],
682
- // Font controls
745
+ // Font controls (with visual disabled state at limits)
683
746
  ...props.showFontControls ? [
684
747
  ReactEcs.createElement(UiEntity, {
685
748
  key: "font-down",
686
749
  uiTransform: { width: 22, height: 22, justifyContent: "center", alignItems: "center" },
687
- uiBackground: { color: THEME.colors.buttonBackground },
688
- onMouseDown: () => props.onFontScaleDown?.(),
689
- children: [ReactEcs.createElement(UiEntity, { uiText: { value: "\u2212", fontSize: 14, color: THEME.colors.cyan } })]
750
+ uiBackground: { color: props.fontScaleAtMin ? Color42.create(0.1, 0.1, 0.1, 0.3) : THEME.colors.buttonBackground },
751
+ onMouseDown: () => !props.fontScaleAtMin && props.onFontScaleDown?.(),
752
+ children: [ReactEcs.createElement(UiEntity, { uiText: { value: "\u2212", fontSize: 14, color: props.fontScaleAtMin ? THEME.colors.gray : THEME.colors.cyan } })]
690
753
  }),
691
754
  ReactEcs.createElement(UiEntity, {
692
755
  key: "font-up",
693
756
  uiTransform: { width: 22, height: 22, justifyContent: "center", alignItems: "center" },
694
- uiBackground: { color: THEME.colors.buttonBackground },
695
- onMouseDown: () => props.onFontScaleUp?.(),
696
- children: [ReactEcs.createElement(UiEntity, { uiText: { value: "+", fontSize: 14, color: THEME.colors.cyan } })]
757
+ uiBackground: { color: props.fontScaleAtMax ? Color42.create(0.1, 0.1, 0.1, 0.3) : THEME.colors.buttonBackground },
758
+ onMouseDown: () => !props.fontScaleAtMax && props.onFontScaleUp?.(),
759
+ children: [ReactEcs.createElement(UiEntity, { uiText: { value: "+", fontSize: 14, color: props.fontScaleAtMax ? THEME.colors.gray : THEME.colors.cyan } })]
697
760
  }),
698
761
  ReactEcs.createElement(UiEntity, { key: "font-spacer", uiTransform: { width: 6 } })
699
762
  ] : [],
@@ -731,7 +794,6 @@ var GuideUIModule = class {
731
794
  this.currentPage = 0;
732
795
  this.itemsPerPage = 6;
733
796
  this.searchQuery = "";
734
- this.uiScale = 1;
735
797
  // Current video tracking (for "PLAYING" indicator)
736
798
  this._currentVideoId = null;
737
799
  // =============================================================================
@@ -739,34 +801,46 @@ var GuideUIModule = class {
739
801
  // =============================================================================
740
802
  /**
741
803
  * Get the guide UI component for rendering
742
- * Returns toggle button when hidden, full panel when visible
804
+ * Always renders toggle button, plus panel when visible
743
805
  */
744
806
  this.getComponent = () => {
745
- if (!this._isVisible) {
746
- return this.renderToggleButton();
747
- }
748
- const windowW = this.s(UI_DIMENSIONS.guide.width);
807
+ const windowW = UI_DIMENSIONS.guide.width;
808
+ const windowH = UI_DIMENSIONS.guide.height;
749
809
  return ReactEcs2.createElement(UiEntity2, {
810
+ key: `guide-root-${this.client.uiScale}`,
750
811
  uiTransform: {
751
- width: windowW,
752
- height: UI_DIMENSIONS.guide.height,
753
- positionType: "absolute",
754
- position: { bottom: UI_DIMENSIONS.guide.bottom, right: UI_DIMENSIONS.guide.right },
755
- flexDirection: "row",
756
- border: { top: 2, bottom: 2, left: 2, right: 2 },
757
- borderColor: THEME.colors.panelBorder
812
+ width: "100%",
813
+ height: "100%",
814
+ positionType: "absolute"
758
815
  },
759
- uiBackground: { color: THEME.colors.panel },
760
816
  children: [
761
- this.renderLeftPanel(),
762
- this.renderRightPanel(),
763
- this.renderCloseButton()
817
+ // Always render toggle button
818
+ this.renderToggleButton(),
819
+ // Render panel when visible - Guide positioned relative to Chat's scaled width
820
+ // Guide.right = Chat.right + Chat.scaledWidth + gap (10px)
821
+ this._isVisible ? ReactEcs2.createElement(UiEntity2, {
822
+ key: `guide-panel-${this.client.uiScale}`,
823
+ uiTransform: {
824
+ width: windowW,
825
+ height: windowH,
826
+ positionType: "absolute",
827
+ position: { bottom: UI_DIMENSIONS.guide.bottom, right: UI_DIMENSIONS.chat.right + this.s(UI_DIMENSIONS.chat.width) + 10 },
828
+ flexDirection: "row",
829
+ border: { top: 2, bottom: 2, left: 2, right: 2 },
830
+ borderColor: THEME.colors.panelBorder
831
+ },
832
+ uiBackground: { color: THEME.colors.panel },
833
+ children: [
834
+ this.renderLeftPanel(),
835
+ this.renderRightPanel(),
836
+ this.renderCloseButton()
837
+ ]
838
+ }) : null
764
839
  ]
765
840
  });
766
841
  };
767
842
  this.client = client;
768
843
  this.config = config;
769
- this.uiScale = config.uiScale || 1;
770
844
  this._currentVideoId = config.currentVideoId || null;
771
845
  }
772
846
  /**
@@ -780,6 +854,8 @@ var GuideUIModule = class {
780
854
  * Show the guide UI
781
855
  */
782
856
  show() {
857
+ if (this._isVisible) return;
858
+ this.client.closeOtherPanels("guide");
783
859
  this._isVisible = true;
784
860
  this.fetchGuideData().catch(() => {
785
861
  });
@@ -915,7 +991,7 @@ var GuideUIModule = class {
915
991
  // --- UTILITIES ---
916
992
  // =============================================================================
917
993
  s(value) {
918
- return Math.round(value * this.uiScale);
994
+ return Math.round(value * this.client.uiScale);
919
995
  }
920
996
  handleVideoSelect(video) {
921
997
  if (this.config.onVideoSelect) {
@@ -951,15 +1027,26 @@ var GuideUIModule = class {
951
1027
  ReactEcs2.createElement(UiEntity2, {
952
1028
  uiTransform: { width: "100%", flexDirection: "column", padding: 10 },
953
1029
  children: [
954
- // Title
1030
+ // Title row
955
1031
  ReactEcs2.createElement(UiEntity2, {
956
- uiText: {
957
- value: "THE STATIC TV",
958
- fontSize: this.s(UI_DIMENSIONS.guide.sidebar.headerSize),
959
- color: THEME.colors.cyan,
960
- textAlign: "middle-center"
1032
+ uiTransform: {
1033
+ width: "100%",
1034
+ height: this.s(40),
1035
+ marginBottom: 10,
1036
+ flexDirection: "row",
1037
+ alignItems: "center"
961
1038
  },
962
- uiTransform: { height: this.s(40), marginBottom: 10 }
1039
+ children: [
1040
+ // Title
1041
+ ReactEcs2.createElement(UiEntity2, {
1042
+ uiText: {
1043
+ value: "THE STATIC TV",
1044
+ fontSize: this.s(UI_DIMENSIONS.guide.sidebar.headerSize),
1045
+ color: THEME.colors.cyan,
1046
+ textAlign: "middle-left"
1047
+ }
1048
+ })
1049
+ ]
963
1050
  }),
964
1051
  // Random Signal button
965
1052
  ReactEcs2.createElement(UiEntity2, {
@@ -1309,23 +1396,25 @@ var GuideUIModule = class {
1309
1396
  });
1310
1397
  }
1311
1398
  renderToggleButton() {
1399
+ const buttonText = this._isVisible ? "CLOSE" : "GUIDE";
1400
+ const buttonColor = this._isVisible ? Color43.create(0.2, 0.2, 0.28, 0.9) : Color43.create(0, 0.5, 0.5, 0.9);
1401
+ const buttonPos = 20 + this.s(100) + 10 + this.s(100) + 10;
1312
1402
  return ReactEcs2.createElement(UiEntity2, {
1313
1403
  uiTransform: {
1314
1404
  positionType: "absolute",
1315
- position: { right: 130, bottom: 10 },
1316
- // To the left of CHAT button
1405
+ position: { right: buttonPos, bottom: 10 },
1317
1406
  width: this.s(100),
1318
1407
  height: this.s(45),
1319
1408
  justifyContent: "center",
1320
1409
  alignItems: "center"
1321
1410
  },
1322
- uiBackground: { color: Color43.create(0, 0.5, 0.5, 0.9) },
1323
- onMouseDown: () => this.show(),
1411
+ uiBackground: { color: buttonColor },
1412
+ onMouseDown: () => this._isVisible ? this.hide() : this.show(),
1324
1413
  children: [
1325
1414
  ReactEcs2.createElement(UiEntity2, {
1326
1415
  uiText: {
1327
- value: "GUIDE",
1328
- fontSize: this.s(16),
1416
+ value: buttonText,
1417
+ fontSize: this.s(14),
1329
1418
  color: THEME.colors.white,
1330
1419
  textAlign: "middle-center"
1331
1420
  }
@@ -1367,7 +1456,6 @@ var ChatUIModule = class {
1367
1456
  this.chatScrollOffset = 0;
1368
1457
  // UI preferences
1369
1458
  this.position = "right";
1370
- this.fontScale = 1;
1371
1459
  // Timers
1372
1460
  this.chatTimerId = null;
1373
1461
  this.playerInfoTimerId = null;
@@ -1377,43 +1465,52 @@ var ChatUIModule = class {
1377
1465
  // =============================================================================
1378
1466
  /**
1379
1467
  * Get the chat UI component for rendering
1380
- * Returns toggle button when hidden, full panel when visible
1468
+ * Always renders toggle button, plus panel when visible
1381
1469
  */
1382
1470
  this.getComponent = () => {
1383
- if (!this._isVisible) {
1384
- return this.renderToggleButton();
1385
- }
1386
- const scaledTheme = scaleChatTheme(DEFAULT_CHAT_THEME, this.fontScale);
1471
+ const scaledTheme = scaleChatTheme(DEFAULT_CHAT_THEME, this.client.uiScale);
1387
1472
  const positionStyle = this.getPositionStyle();
1388
1473
  return ReactEcs3.createElement(UiEntity3, {
1389
1474
  uiTransform: {
1390
- width: UI_DIMENSIONS.chat.width,
1391
- height: UI_DIMENSIONS.chat.height,
1392
- positionType: "absolute",
1393
- position: positionStyle,
1394
- // Must be nested object, not spread!
1395
- flexDirection: "column",
1396
- border: { top: 2, bottom: 2, left: 2, right: 2 },
1397
- borderColor: THEME.colors.panelBorder
1475
+ width: "100%",
1476
+ height: "100%",
1477
+ positionType: "absolute"
1398
1478
  },
1399
- uiBackground: { color: THEME.colors.panel },
1400
1479
  children: [
1401
- this.renderHeader(),
1402
- this.renderChannelButton(scaledTheme),
1403
- this.renderMessagesArea(scaledTheme),
1404
- ReactEcs3.createElement(UiEntity3, {
1405
- uiTransform: { width: "100%", height: 1, flexShrink: 0 },
1406
- uiBackground: { color: THEME.colors.panelBorder }
1407
- }),
1408
- this.renderUserInfoBar(scaledTheme),
1409
- this.renderInputArea(scaledTheme),
1410
- this.renderChannelDropdown(scaledTheme)
1480
+ // Always render toggle button
1481
+ this.renderToggleButton(),
1482
+ // Render panel when visible - scaled sizes, fixed position
1483
+ // Key includes uiScale to force re-render when scale changes from other panels
1484
+ this._isVisible ? ReactEcs3.createElement(UiEntity3, {
1485
+ key: `chat-panel-${this.client.uiScale}`,
1486
+ uiTransform: {
1487
+ width: this.s(UI_DIMENSIONS.chat.width),
1488
+ height: this.s(UI_DIMENSIONS.chat.height),
1489
+ positionType: "absolute",
1490
+ position: { bottom: UI_DIMENSIONS.chat.bottom, right: UI_DIMENSIONS.chat.right },
1491
+ flexDirection: "column",
1492
+ border: { top: 2, bottom: 2, left: 2, right: 2 },
1493
+ borderColor: THEME.colors.panelBorder
1494
+ },
1495
+ uiBackground: { color: THEME.colors.panel },
1496
+ children: [
1497
+ this.renderHeader(),
1498
+ this.renderChannelButton(scaledTheme),
1499
+ this.renderMessagesArea(scaledTheme),
1500
+ ReactEcs3.createElement(UiEntity3, {
1501
+ uiTransform: { width: "100%", height: 1, flexShrink: 0 },
1502
+ uiBackground: { color: THEME.colors.panelBorder }
1503
+ }),
1504
+ this.renderUserInfoBar(scaledTheme),
1505
+ this.renderInputArea(scaledTheme),
1506
+ this.renderChannelDropdown(scaledTheme)
1507
+ ]
1508
+ }) : null
1411
1509
  ]
1412
1510
  });
1413
1511
  };
1414
1512
  this.client = client;
1415
1513
  this.config = config;
1416
- this.fontScale = config.fontScale || 1;
1417
1514
  }
1418
1515
  /**
1419
1516
  * Initialize the chat system
@@ -1432,6 +1529,7 @@ var ChatUIModule = class {
1432
1529
  * Show the chat UI
1433
1530
  */
1434
1531
  show() {
1532
+ if (this._isVisible) return;
1435
1533
  this._isVisible = true;
1436
1534
  this._unreadCount = 0;
1437
1535
  this.chatScrollOffset = 0;
@@ -1693,6 +1791,10 @@ var ChatUIModule = class {
1693
1791
  const d = new Date(input);
1694
1792
  if (!isNaN(d.getTime())) return d.getTime();
1695
1793
  }
1794
+ if (input instanceof Date) return input.getTime();
1795
+ if (typeof input === "object" && "seconds" in input) {
1796
+ return input.seconds * 1e3;
1797
+ }
1696
1798
  return 0;
1697
1799
  }
1698
1800
  formatTime(isoString) {
@@ -1705,6 +1807,10 @@ var ChatUIModule = class {
1705
1807
  return "";
1706
1808
  }
1707
1809
  }
1810
+ /** Scale a dimension by shared uiScale */
1811
+ s(value) {
1812
+ return Math.round(value * this.client.uiScale);
1813
+ }
1708
1814
  getPositionStyle() {
1709
1815
  return { bottom: 55, right: 20 };
1710
1816
  }
@@ -1712,15 +1818,9 @@ var ChatUIModule = class {
1712
1818
  return PanelHeader({
1713
1819
  title: "LIVE CHAT",
1714
1820
  fontSize: 14,
1715
- fontScale: this.fontScale,
1821
+ fontScale: 1,
1716
1822
  showPositionControls: false,
1717
- showFontControls: true,
1718
- onFontScaleUp: () => {
1719
- this.fontScale = Math.min(1.4, this.fontScale + 0.1);
1720
- },
1721
- onFontScaleDown: () => {
1722
- this.fontScale = Math.max(0.7, this.fontScale - 0.1);
1723
- },
1823
+ showFontControls: false,
1724
1824
  onClose: () => this.hide()
1725
1825
  });
1726
1826
  }
@@ -2095,23 +2195,24 @@ var ChatUIModule = class {
2095
2195
  }
2096
2196
  renderToggleButton() {
2097
2197
  const unreadBadge = this._unreadCount > 0 ? ` (${this._unreadCount})` : "";
2198
+ const buttonText = this._isVisible ? "CLOSE" : `CHAT${unreadBadge}`;
2199
+ const buttonColor = this._isVisible ? Color44.create(0.2, 0.2, 0.28, 0.9) : Color44.create(0.6, 0, 0.5, 0.9);
2098
2200
  return ReactEcs3.createElement(UiEntity3, {
2099
2201
  uiTransform: {
2100
2202
  positionType: "absolute",
2101
2203
  position: { right: 20, bottom: 10 },
2102
- // Far right corner
2103
- width: 100,
2104
- height: 45,
2204
+ width: this.s(100),
2205
+ height: this.s(45),
2105
2206
  justifyContent: "center",
2106
2207
  alignItems: "center"
2107
2208
  },
2108
- uiBackground: { color: Color44.create(0.6, 0, 0.5, 0.9) },
2109
- onMouseDown: () => this.show(),
2209
+ uiBackground: { color: buttonColor },
2210
+ onMouseDown: () => this._isVisible ? this.hide() : this.show(),
2110
2211
  children: [
2111
2212
  ReactEcs3.createElement(UiEntity3, {
2112
2213
  uiText: {
2113
- value: `CHAT${unreadBadge}`,
2114
- fontSize: 16,
2214
+ value: buttonText,
2215
+ fontSize: this.s(14),
2115
2216
  color: THEME.colors.white,
2116
2217
  textAlign: "middle-center"
2117
2218
  }
@@ -2121,6 +2222,1232 @@ var ChatUIModule = class {
2121
2222
  }
2122
2223
  };
2123
2224
 
2225
+ // src/ui/admin-panel-ui.tsx
2226
+ import ReactEcs4, { UiEntity as UiEntity4, Button as Button3, Label as Label4, Input as Input4 } from "@dcl/sdk/react-ecs";
2227
+ import { Color4 as Color45, Vector3 } from "@dcl/sdk/math";
2228
+ import { getPlayer as getPlayer2 } from "@dcl/sdk/players";
2229
+ import { movePlayerTo, openExternalUrl as openExternalUrl3 } from "~system/RestrictedActions";
2230
+ var BAN_KICK_POSITION = Vector3.create(16, -50, 16);
2231
+ var C = {
2232
+ bg: Color45.create(0.08, 0.08, 0.12, 0.98),
2233
+ header: Color45.create(0.9, 0.15, 0.15, 1),
2234
+ tabActive: Color45.create(0, 0.7, 0.7, 1),
2235
+ tabInactive: Color45.create(0.15, 0.15, 0.2, 1),
2236
+ section: Color45.create(0.12, 0.12, 0.18, 1),
2237
+ btn: Color45.create(0.2, 0.2, 0.28, 1),
2238
+ cyan: Color45.create(0, 0.8, 0.8, 1),
2239
+ magenta: Color45.create(0.85, 0.2, 0.55, 1),
2240
+ yellow: Color45.create(0.95, 0.75, 0.1, 1),
2241
+ green: Color45.create(0.2, 0.75, 0.3, 1),
2242
+ red: Color45.create(0.85, 0.2, 0.2, 1),
2243
+ purple: Color45.create(0.6, 0.3, 0.85, 1),
2244
+ orange: Color45.create(0.95, 0.5, 0.15, 1),
2245
+ text: Color45.White(),
2246
+ textDim: Color45.create(0.6, 0.6, 0.7, 1)
2247
+ };
2248
+ var AdminPanelUIModule = class {
2249
+ // UI scaling - uses shared client.uiScale
2250
+ constructor(client, config) {
2251
+ // State
2252
+ this.isAdmin = false;
2253
+ this.isOwner = false;
2254
+ this.panelOpen = false;
2255
+ this.activeTab = "video";
2256
+ this.playerWallet = "";
2257
+ // Video tab state
2258
+ this.customVideoUrl = "";
2259
+ this.streamData = null;
2260
+ this.streamFetched = false;
2261
+ this.videoState = null;
2262
+ this.videoStateFetched = false;
2263
+ this.channelCreating = false;
2264
+ this.channelCreateError = "";
2265
+ this.channelDeleting = false;
2266
+ this.channelDeleteError = "";
2267
+ this.keyRotating = false;
2268
+ this.keyRotateStatus = "";
2269
+ this.streamControlling = false;
2270
+ this.streamControlStatus = "";
2271
+ this.pollIntervalId = null;
2272
+ this.trialClaiming = false;
2273
+ this.trialClaimError = "";
2274
+ // Mod tab state
2275
+ this.sceneAdmins = [];
2276
+ this.bannedWallets = [];
2277
+ this.newAdminWallet = "";
2278
+ this.newBanWallet = "";
2279
+ this.broadcastText = "";
2280
+ this.modStatus = "";
2281
+ this.modsFetched = false;
2282
+ // --- UI Components ---
2283
+ this.SectionHead = ({ label, color }) => /* @__PURE__ */ ReactEcs4.createElement(
2284
+ UiEntity4,
2285
+ {
2286
+ uiTransform: { width: "100%", height: this.s(UI_DIMENSIONS.admin.sectionHeadHeight), margin: { bottom: 8 }, padding: { left: 10 }, alignItems: "center" },
2287
+ uiBackground: { color: Color45.create(color.r * 0.3, color.g * 0.3, color.b * 0.3, 0.5) }
2288
+ },
2289
+ /* @__PURE__ */ ReactEcs4.createElement(Label4, { value: label, fontSize: this.theme.sectionHead, color })
2290
+ );
2291
+ this.TabBtn = ({ label, tab }) => /* @__PURE__ */ ReactEcs4.createElement(
2292
+ Button3,
2293
+ {
2294
+ uiTransform: { flexGrow: 1, height: this.s(UI_DIMENSIONS.admin.tabHeight), justifyContent: "center", alignItems: "center" },
2295
+ uiBackground: { color: this.activeTab === tab ? C.tabActive : C.tabInactive },
2296
+ value: label,
2297
+ fontSize: this.theme.tabButton,
2298
+ color: this.activeTab === tab ? Color45.Black() : C.text,
2299
+ textAlign: "middle-center",
2300
+ onMouseDown: () => this.setActiveTab(tab)
2301
+ }
2302
+ );
2303
+ this.VideoTab = () => {
2304
+ if (!this.streamFetched) {
2305
+ this.fetchStreamData();
2306
+ }
2307
+ const t = this.theme;
2308
+ return /* @__PURE__ */ ReactEcs4.createElement(UiEntity4, { uiTransform: { flexDirection: "column", width: "100%", padding: 10 } }, !this.streamData?.hasChannel && /* @__PURE__ */ ReactEcs4.createElement(UiEntity4, { uiTransform: { flexDirection: "column", margin: { bottom: 14 } } }, /* @__PURE__ */ ReactEcs4.createElement(this.SectionHead, { label: "LIVE STREAM", color: C.btn }), /* @__PURE__ */ ReactEcs4.createElement(Label4, { value: "No streaming channel linked", fontSize: t.labelSmall, color: C.textDim, uiTransform: { margin: { bottom: 8 } } }), this.isOwner && /* @__PURE__ */ ReactEcs4.createElement(UiEntity4, { uiTransform: { flexDirection: "column" } }, this.streamData?.trialAvailable && /* @__PURE__ */ ReactEcs4.createElement(UiEntity4, { uiTransform: { flexDirection: "column", margin: { bottom: 10 } } }, /* @__PURE__ */ ReactEcs4.createElement(
2309
+ Button3,
2310
+ {
2311
+ uiTransform: { width: this.s(200), height: this.s(UI_DIMENSIONS.admin.buttonHeight), margin: { bottom: 6 } },
2312
+ uiBackground: { color: this.trialClaiming ? C.btn : C.green },
2313
+ value: this.trialClaiming ? "Claiming..." : "Start Free 4-Hour Trial",
2314
+ fontSize: t.button,
2315
+ color: C.text,
2316
+ onMouseDown: () => this.claimTrial()
2317
+ }
2318
+ ), this.trialClaimError && /* @__PURE__ */ ReactEcs4.createElement(Label4, { value: this.trialClaimError, fontSize: t.status, color: C.red }), /* @__PURE__ */ ReactEcs4.createElement(Label4, { value: "One-time trial \u2022 4 hours of streaming", fontSize: t.labelSmall, color: C.textDim, uiTransform: { margin: { top: 4 } } })), !this.streamData?.trialAvailable && /* @__PURE__ */ ReactEcs4.createElement(UiEntity4, { uiTransform: { flexDirection: "column" } }, /* @__PURE__ */ ReactEcs4.createElement(
2319
+ Button3,
2320
+ {
2321
+ uiTransform: { width: this.s(170), height: this.s(UI_DIMENSIONS.admin.buttonHeight), margin: { bottom: 6 } },
2322
+ uiBackground: { color: this.channelCreating ? C.btn : C.cyan },
2323
+ value: this.channelCreating ? "Creating..." : "+ Create Channel",
2324
+ fontSize: t.button,
2325
+ color: C.text,
2326
+ onMouseDown: () => this.createChannel()
2327
+ }
2328
+ ), this.channelCreateError && /* @__PURE__ */ ReactEcs4.createElement(Label4, { value: this.channelCreateError, fontSize: t.status, color: C.red }), /* @__PURE__ */ ReactEcs4.createElement(Label4, { 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__ */ ReactEcs4.createElement(UiEntity4, { uiTransform: { flexDirection: "column", margin: { bottom: 14 } } }, /* @__PURE__ */ ReactEcs4.createElement(this.SectionHead, { label: this.streamData.isLive ? "LIVE STREAM" : "STREAM SETTINGS", color: this.streamData.isLive ? C.red : C.yellow }), /* @__PURE__ */ ReactEcs4.createElement(UiEntity4, { uiTransform: { flexDirection: "row", alignItems: "center", margin: { bottom: 8 } } }, /* @__PURE__ */ ReactEcs4.createElement(
2329
+ Label4,
2330
+ {
2331
+ value: this.streamData.isLive ? `LIVE \u2022 ${this.streamData.currentViewers || 0} viewers` : "OFFLINE",
2332
+ fontSize: t.label,
2333
+ color: this.streamData.isLive ? C.red : C.textDim
2334
+ }
2335
+ ), /* @__PURE__ */ ReactEcs4.createElement(Label4, { value: ` \u2022 ${this.streamData.tier?.toUpperCase() || "RELAY"}`, fontSize: t.labelSmall, color: C.cyan }), /* @__PURE__ */ ReactEcs4.createElement(Label4, { value: ` \u2022 ${this.streamData.sparksBalance || 0} Sparks`, fontSize: t.labelSmall, color: C.textDim })), /* @__PURE__ */ ReactEcs4.createElement(
2336
+ Label4,
2337
+ {
2338
+ value: `Channel: ${this.streamData.channelName || this.streamData.channelId}`,
2339
+ fontSize: t.labelSmall,
2340
+ color: C.textDim,
2341
+ uiTransform: { margin: { bottom: 10 } }
2342
+ }
2343
+ ), /* @__PURE__ */ ReactEcs4.createElement(UiEntity4, { uiTransform: { flexDirection: "column", margin: { bottom: 6 } } }, /* @__PURE__ */ ReactEcs4.createElement(Label4, { value: "RTMP Server:", fontSize: t.labelSmall, color: C.textDim }), /* @__PURE__ */ ReactEcs4.createElement(
2344
+ Label4,
2345
+ {
2346
+ value: this.streamData.rtmpUrl || "Loading...",
2347
+ fontSize: t.label,
2348
+ color: this.streamData.rtmpUrl ? C.text : C.textDim,
2349
+ uiTransform: { margin: { left: 6 } }
2350
+ }
2351
+ )), /* @__PURE__ */ ReactEcs4.createElement(UiEntity4, { uiTransform: { flexDirection: "column", margin: { bottom: 6 } } }, /* @__PURE__ */ ReactEcs4.createElement(Label4, { value: "Stream Key:", fontSize: t.labelSmall, color: C.textDim }), /* @__PURE__ */ ReactEcs4.createElement(
2352
+ Label4,
2353
+ {
2354
+ value: this.streamData.streamKey || "Loading...",
2355
+ fontSize: t.label,
2356
+ color: this.streamData.streamKey ? C.cyan : C.textDim,
2357
+ uiTransform: { margin: { left: 6 } }
2358
+ }
2359
+ )), /* @__PURE__ */ ReactEcs4.createElement(UiEntity4, { uiTransform: { flexDirection: "column", margin: { bottom: 10 } } }, /* @__PURE__ */ ReactEcs4.createElement(Label4, { value: "HLS Playback:", fontSize: t.labelSmall, color: C.textDim }), /* @__PURE__ */ ReactEcs4.createElement(
2360
+ Label4,
2361
+ {
2362
+ value: this.streamData.hlsUrl || "Not available",
2363
+ fontSize: t.label,
2364
+ color: this.streamData.hlsUrl ? C.green : C.textDim,
2365
+ uiTransform: { margin: { left: 6 } }
2366
+ }
2367
+ )), /* @__PURE__ */ ReactEcs4.createElement(UiEntity4, { uiTransform: { flexDirection: "row", flexWrap: "wrap", margin: { bottom: 6 } } }, !this.streamData.isLive && /* @__PURE__ */ ReactEcs4.createElement(
2368
+ Button3,
2369
+ {
2370
+ uiTransform: { width: this.s(110), height: this.s(UI_DIMENSIONS.admin.buttonHeightSmall), margin: 4 },
2371
+ uiBackground: { color: this.streamControlling ? C.btn : C.green },
2372
+ value: this.streamControlling ? "Starting..." : "Start Stream",
2373
+ fontSize: t.buttonSmall,
2374
+ color: C.text,
2375
+ onMouseDown: () => this.startStream()
2376
+ }
2377
+ ), this.streamData.isLive && /* @__PURE__ */ ReactEcs4.createElement(
2378
+ Button3,
2379
+ {
2380
+ uiTransform: { width: this.s(110), height: this.s(UI_DIMENSIONS.admin.buttonHeightSmall), margin: 4 },
2381
+ uiBackground: { color: this.streamControlling ? C.btn : C.red },
2382
+ value: this.streamControlling ? "Stopping..." : "Stop Stream",
2383
+ fontSize: t.buttonSmall,
2384
+ color: C.text,
2385
+ onMouseDown: () => this.stopStream()
2386
+ }
2387
+ ), this.streamData.isLive && this.streamData.hlsUrl && /* @__PURE__ */ ReactEcs4.createElement(
2388
+ Button3,
2389
+ {
2390
+ uiTransform: { width: this.s(100), height: this.s(UI_DIMENSIONS.admin.buttonHeightSmall), margin: 4 },
2391
+ uiBackground: { color: C.cyan },
2392
+ value: "Play on Screen",
2393
+ fontSize: t.buttonSmall,
2394
+ color: C.text,
2395
+ onMouseDown: () => this.config.onVideoPlay?.(this.streamData.hlsUrl)
2396
+ }
2397
+ )), /* @__PURE__ */ ReactEcs4.createElement(UiEntity4, { uiTransform: { flexDirection: "row", flexWrap: "wrap", margin: { bottom: 6 } } }, /* @__PURE__ */ ReactEcs4.createElement(
2398
+ Button3,
2399
+ {
2400
+ uiTransform: { width: this.s(80), height: this.s(UI_DIMENSIONS.admin.buttonHeightSmall), margin: 4 },
2401
+ uiBackground: { color: C.btn },
2402
+ value: "Refresh",
2403
+ fontSize: t.buttonSmall,
2404
+ color: C.text,
2405
+ onMouseDown: () => this.refreshStreamStatus()
2406
+ }
2407
+ ), /* @__PURE__ */ ReactEcs4.createElement(
2408
+ Button3,
2409
+ {
2410
+ uiTransform: { width: this.s(95), height: this.s(UI_DIMENSIONS.admin.buttonHeightSmall), margin: 4 },
2411
+ uiBackground: { color: this.keyRotating ? C.btn : C.yellow },
2412
+ value: this.keyRotating ? "..." : "Rotate Key",
2413
+ fontSize: t.buttonSmall,
2414
+ color: C.text,
2415
+ onMouseDown: () => this.rotateStreamKey()
2416
+ }
2417
+ ), !this.streamData.isLive && /* @__PURE__ */ ReactEcs4.createElement(
2418
+ Button3,
2419
+ {
2420
+ uiTransform: { width: this.s(70), height: this.s(UI_DIMENSIONS.admin.buttonHeightSmall), margin: 4 },
2421
+ uiBackground: { color: this.channelDeleting ? C.btn : C.red },
2422
+ value: this.channelDeleting ? "..." : "Delete",
2423
+ fontSize: t.buttonSmall,
2424
+ color: C.text,
2425
+ onMouseDown: () => this.deleteChannel()
2426
+ }
2427
+ )), this.streamControlStatus === "started" && /* @__PURE__ */ ReactEcs4.createElement(Label4, { value: "Stream started - begin broadcasting in OBS", fontSize: t.status, color: C.green }), this.streamControlStatus === "stopped" && /* @__PURE__ */ ReactEcs4.createElement(Label4, { value: "Stream stopped", fontSize: t.status, color: C.textDim }), this.keyRotateStatus === "success" && /* @__PURE__ */ ReactEcs4.createElement(Label4, { value: "Key rotated! Update OBS", fontSize: t.status, color: C.green }), this.keyRotateStatus === "error" && /* @__PURE__ */ ReactEcs4.createElement(Label4, { value: "Failed to rotate key", fontSize: t.status, color: C.red }), this.channelDeleteError && /* @__PURE__ */ ReactEcs4.createElement(Label4, { value: this.channelDeleteError, fontSize: t.status, color: C.red }), this.streamControlStatus === "error" && /* @__PURE__ */ ReactEcs4.createElement(Label4, { value: "Stream control failed", fontSize: t.status, color: C.red })), /* @__PURE__ */ ReactEcs4.createElement(this.SectionHead, { label: "PLAY NOW", color: C.orange }), /* @__PURE__ */ ReactEcs4.createElement(UiEntity4, { uiTransform: { flexDirection: "row", alignItems: "center", margin: { bottom: 14 } } }, /* @__PURE__ */ ReactEcs4.createElement(
2428
+ Input4,
2429
+ {
2430
+ uiTransform: { width: this.s(230), height: this.s(UI_DIMENSIONS.admin.inputHeight) },
2431
+ uiBackground: { color: Color45.create(0.15, 0.15, 0.2, 1) },
2432
+ placeholder: "Video URL...",
2433
+ placeholderColor: C.textDim,
2434
+ color: C.text,
2435
+ fontSize: t.input,
2436
+ value: this.customVideoUrl,
2437
+ onChange: (val) => {
2438
+ this.customVideoUrl = val;
2439
+ }
2440
+ }
2441
+ ), /* @__PURE__ */ ReactEcs4.createElement(
2442
+ Button3,
2443
+ {
2444
+ uiTransform: { width: this.s(75), height: this.s(UI_DIMENSIONS.admin.inputHeight), margin: { left: 8 } },
2445
+ uiBackground: { color: C.green },
2446
+ value: "Play",
2447
+ fontSize: t.button,
2448
+ color: C.text,
2449
+ onMouseDown: () => {
2450
+ if (this.customVideoUrl) this.config.onVideoPlay?.(this.customVideoUrl);
2451
+ }
2452
+ }
2453
+ ), /* @__PURE__ */ ReactEcs4.createElement(
2454
+ Button3,
2455
+ {
2456
+ uiTransform: { width: this.s(65), height: this.s(UI_DIMENSIONS.admin.inputHeight), margin: { left: 6 } },
2457
+ uiBackground: { color: C.btn },
2458
+ value: "Clear",
2459
+ fontSize: t.button,
2460
+ color: C.text,
2461
+ onMouseDown: () => {
2462
+ this.customVideoUrl = "";
2463
+ }
2464
+ }
2465
+ )), /* @__PURE__ */ ReactEcs4.createElement(this.SectionHead, { label: "PLAYBACK", color: C.green }), /* @__PURE__ */ ReactEcs4.createElement(UiEntity4, { uiTransform: { flexDirection: "row", flexWrap: "wrap", margin: { bottom: 14 } } }, /* @__PURE__ */ ReactEcs4.createElement(
2466
+ Button3,
2467
+ {
2468
+ uiTransform: { width: this.s(75), height: this.s(UI_DIMENSIONS.admin.buttonHeightSmall), margin: 4 },
2469
+ uiBackground: { color: C.green },
2470
+ value: "Play",
2471
+ fontSize: t.button,
2472
+ color: C.text,
2473
+ onMouseDown: () => this.config.onCommand?.("videoPlay", { playing: true })
2474
+ }
2475
+ ), /* @__PURE__ */ ReactEcs4.createElement(
2476
+ Button3,
2477
+ {
2478
+ uiTransform: { width: this.s(75), height: this.s(UI_DIMENSIONS.admin.buttonHeightSmall), margin: 4 },
2479
+ uiBackground: { color: C.red },
2480
+ value: "Stop",
2481
+ fontSize: t.button,
2482
+ color: C.text,
2483
+ onMouseDown: () => this.config.onVideoStop?.()
2484
+ }
2485
+ ), /* @__PURE__ */ ReactEcs4.createElement(
2486
+ Button3,
2487
+ {
2488
+ uiTransform: { width: this.s(100), height: this.s(UI_DIMENSIONS.admin.buttonHeightSmall), margin: 4 },
2489
+ uiBackground: { color: C.btn },
2490
+ value: "Reset Default",
2491
+ fontSize: t.buttonSmall,
2492
+ color: C.text,
2493
+ onMouseDown: () => this.config.onCommand?.("videoClear", {})
2494
+ }
2495
+ )), /* @__PURE__ */ ReactEcs4.createElement(this.SectionHead, { label: "VIDEO SLOTS", color: C.cyan }), /* @__PURE__ */ ReactEcs4.createElement(UiEntity4, { uiTransform: { flexDirection: "row", flexWrap: "wrap", margin: { bottom: 10 } } }, /* @__PURE__ */ ReactEcs4.createElement(Button3, { 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__ */ ReactEcs4.createElement(Button3, { 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__ */ ReactEcs4.createElement(Button3, { 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__ */ ReactEcs4.createElement(Button3, { 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__ */ ReactEcs4.createElement(Button3, { 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__ */ ReactEcs4.createElement(
2496
+ Button3,
2497
+ {
2498
+ uiTransform: { height: this.s(24) },
2499
+ uiBackground: { color: Color45.create(0, 0, 0, 0) },
2500
+ value: "Edit slots at thestatic.tv \u2192",
2501
+ fontSize: t.labelSmall,
2502
+ color: C.cyan,
2503
+ onMouseDown: () => openExternalUrl3({ url: this.config.footerLink || `https://thestatic.tv/scene/${this.config.sceneId}` })
2504
+ }
2505
+ ));
2506
+ };
2507
+ this.ModTab = () => {
2508
+ const t = this.theme;
2509
+ return /* @__PURE__ */ ReactEcs4.createElement(UiEntity4, { uiTransform: { flexDirection: "column", width: "100%", padding: 10 } }, this.modStatus === "loading" && /* @__PURE__ */ ReactEcs4.createElement(Label4, { value: "Loading...", fontSize: t.status, color: C.yellow }), this.modStatus === "saved" && /* @__PURE__ */ ReactEcs4.createElement(Label4, { value: "Saved!", fontSize: t.status, color: C.green }), this.modStatus === "error" && /* @__PURE__ */ ReactEcs4.createElement(Label4, { value: "Error - check input", fontSize: t.status, color: C.red }), /* @__PURE__ */ ReactEcs4.createElement(this.SectionHead, { label: "BROADCAST", color: C.orange }), /* @__PURE__ */ ReactEcs4.createElement(UiEntity4, { uiTransform: { flexDirection: "row", alignItems: "center", margin: { bottom: 14 } } }, /* @__PURE__ */ ReactEcs4.createElement(
2510
+ Input4,
2511
+ {
2512
+ uiTransform: { width: this.s(230), height: this.s(UI_DIMENSIONS.admin.inputHeight) },
2513
+ uiBackground: { color: Color45.create(0.15, 0.15, 0.2, 1) },
2514
+ placeholder: "Message to all players...",
2515
+ placeholderColor: C.textDim,
2516
+ color: C.text,
2517
+ fontSize: t.input,
2518
+ value: this.broadcastText,
2519
+ onChange: (val) => {
2520
+ this.broadcastText = val;
2521
+ }
2522
+ }
2523
+ ), /* @__PURE__ */ ReactEcs4.createElement(
2524
+ Button3,
2525
+ {
2526
+ uiTransform: { width: this.s(65), height: this.s(UI_DIMENSIONS.admin.inputHeight), margin: { left: 8 } },
2527
+ uiBackground: { color: C.orange },
2528
+ value: "Send",
2529
+ fontSize: t.button,
2530
+ color: C.text,
2531
+ onMouseDown: () => this.sendBroadcast()
2532
+ }
2533
+ )), /* @__PURE__ */ ReactEcs4.createElement(this.SectionHead, { label: "CHAOS MODE", color: C.red }), /* @__PURE__ */ ReactEcs4.createElement(UiEntity4, { uiTransform: { flexDirection: "row", flexWrap: "wrap", margin: { bottom: 14 } } }, /* @__PURE__ */ ReactEcs4.createElement(
2534
+ Button3,
2535
+ {
2536
+ uiTransform: { width: this.s(130), height: this.s(UI_DIMENSIONS.admin.buttonHeight), margin: 4 },
2537
+ uiBackground: { color: C.red },
2538
+ value: "KICK ALL",
2539
+ fontSize: t.button,
2540
+ color: C.text,
2541
+ onMouseDown: () => {
2542
+ this.log("KICK ALL clicked");
2543
+ this.config.onCommand?.("kickAll", {});
2544
+ }
2545
+ }
2546
+ )), /* @__PURE__ */ ReactEcs4.createElement(this.SectionHead, { label: "SCENE ADMINS", color: C.purple }), /* @__PURE__ */ ReactEcs4.createElement(UiEntity4, { uiTransform: { flexDirection: "column", margin: { bottom: 6 } } }, this.sceneAdmins.length === 0 && this.modStatus !== "loading" && /* @__PURE__ */ ReactEcs4.createElement(Label4, { value: "No scene admins", fontSize: t.labelSmall, color: C.textDim }), this.sceneAdmins.map((wallet, i) => /* @__PURE__ */ ReactEcs4.createElement(
2547
+ UiEntity4,
2548
+ {
2549
+ key: `admin-${i}`,
2550
+ uiTransform: { flexDirection: "row", alignItems: "center", margin: { bottom: 5 }, width: "100%" }
2551
+ },
2552
+ /* @__PURE__ */ ReactEcs4.createElement(
2553
+ Label4,
2554
+ {
2555
+ value: `${wallet.slice(0, 6)}...${wallet.slice(-4)}`,
2556
+ fontSize: t.label,
2557
+ color: C.text,
2558
+ uiTransform: { width: this.s(130) }
2559
+ }
2560
+ ),
2561
+ /* @__PURE__ */ ReactEcs4.createElement(
2562
+ Button3,
2563
+ {
2564
+ uiTransform: { width: this.s(70), height: this.s(28), margin: { left: 8 } },
2565
+ uiBackground: { color: C.btn },
2566
+ value: "Remove",
2567
+ fontSize: t.buttonSmall,
2568
+ color: C.text,
2569
+ onMouseDown: () => this.removeSceneAdmin(wallet)
2570
+ }
2571
+ )
2572
+ ))), /* @__PURE__ */ ReactEcs4.createElement(UiEntity4, { uiTransform: { flexDirection: "row", alignItems: "center", margin: { bottom: 14 } } }, /* @__PURE__ */ ReactEcs4.createElement(
2573
+ Input4,
2574
+ {
2575
+ uiTransform: { width: this.s(230), height: this.s(UI_DIMENSIONS.admin.inputHeight) },
2576
+ uiBackground: { color: Color45.create(0.15, 0.15, 0.2, 1) },
2577
+ placeholder: "0x... add admin",
2578
+ placeholderColor: C.textDim,
2579
+ color: C.text,
2580
+ fontSize: t.input,
2581
+ value: this.newAdminWallet,
2582
+ onChange: (val) => {
2583
+ this.newAdminWallet = val;
2584
+ }
2585
+ }
2586
+ ), /* @__PURE__ */ ReactEcs4.createElement(
2587
+ Button3,
2588
+ {
2589
+ uiTransform: { width: this.s(60), height: this.s(UI_DIMENSIONS.admin.inputHeight), margin: { left: 8 } },
2590
+ uiBackground: { color: C.purple },
2591
+ value: "Add",
2592
+ fontSize: t.button,
2593
+ color: C.text,
2594
+ onMouseDown: () => {
2595
+ if (this.newAdminWallet) this.addSceneAdmin(this.newAdminWallet);
2596
+ }
2597
+ }
2598
+ )), /* @__PURE__ */ ReactEcs4.createElement(this.SectionHead, { label: "BANNED WALLETS", color: C.red }), /* @__PURE__ */ ReactEcs4.createElement(UiEntity4, { uiTransform: { flexDirection: "column", margin: { bottom: 6 } } }, this.bannedWallets.length === 0 && this.modStatus !== "loading" && /* @__PURE__ */ ReactEcs4.createElement(Label4, { value: "No banned wallets", fontSize: t.labelSmall, color: C.textDim }), this.bannedWallets.map((wallet, i) => /* @__PURE__ */ ReactEcs4.createElement(
2599
+ UiEntity4,
2600
+ {
2601
+ key: `ban-${i}`,
2602
+ uiTransform: { flexDirection: "row", alignItems: "center", margin: { bottom: 5 }, width: "100%" }
2603
+ },
2604
+ /* @__PURE__ */ ReactEcs4.createElement(
2605
+ Label4,
2606
+ {
2607
+ value: `${wallet.slice(0, 6)}...${wallet.slice(-4)}`,
2608
+ fontSize: t.label,
2609
+ color: C.red,
2610
+ uiTransform: { width: this.s(130) }
2611
+ }
2612
+ ),
2613
+ /* @__PURE__ */ ReactEcs4.createElement(
2614
+ Button3,
2615
+ {
2616
+ uiTransform: { width: this.s(70), height: this.s(28), margin: { left: 8 } },
2617
+ uiBackground: { color: C.green },
2618
+ value: "Unban",
2619
+ fontSize: t.buttonSmall,
2620
+ color: C.text,
2621
+ onMouseDown: () => this.unbanWallet(wallet)
2622
+ }
2623
+ )
2624
+ ))), /* @__PURE__ */ ReactEcs4.createElement(UiEntity4, { uiTransform: { flexDirection: "row", alignItems: "center", margin: { bottom: 10 } } }, /* @__PURE__ */ ReactEcs4.createElement(
2625
+ Input4,
2626
+ {
2627
+ uiTransform: { width: this.s(230), height: this.s(UI_DIMENSIONS.admin.inputHeight) },
2628
+ uiBackground: { color: Color45.create(0.15, 0.15, 0.2, 1) },
2629
+ placeholder: "0x... ban wallet",
2630
+ placeholderColor: C.textDim,
2631
+ color: C.text,
2632
+ fontSize: t.input,
2633
+ value: this.newBanWallet,
2634
+ onChange: (val) => {
2635
+ this.newBanWallet = val;
2636
+ }
2637
+ }
2638
+ ), /* @__PURE__ */ ReactEcs4.createElement(
2639
+ Button3,
2640
+ {
2641
+ uiTransform: { width: this.s(60), height: this.s(UI_DIMENSIONS.admin.inputHeight), margin: { left: 8 } },
2642
+ uiBackground: { color: C.red },
2643
+ value: "Ban",
2644
+ fontSize: t.button,
2645
+ color: C.text,
2646
+ onMouseDown: () => {
2647
+ if (this.newBanWallet) this.banWallet(this.newBanWallet);
2648
+ }
2649
+ }
2650
+ )), /* @__PURE__ */ ReactEcs4.createElement(
2651
+ Button3,
2652
+ {
2653
+ uiTransform: { height: this.s(24) },
2654
+ uiBackground: { color: Color45.create(0, 0, 0, 0) },
2655
+ value: "Manage at thestatic.tv \u2192",
2656
+ fontSize: t.labelSmall,
2657
+ color: C.cyan,
2658
+ onMouseDown: () => openExternalUrl3({ url: this.config.footerLink || `https://thestatic.tv/scene/${this.config.sceneId}` })
2659
+ }
2660
+ ));
2661
+ };
2662
+ /**
2663
+ * Get the React-ECS component for the admin panel
2664
+ */
2665
+ this.getComponent = () => {
2666
+ if (!this.isAdmin) return null;
2667
+ const t = this.theme;
2668
+ const tabs = [];
2669
+ if (this.config.sceneTabs && this.config.sceneTabs.length > 0) {
2670
+ this.config.sceneTabs.forEach((tab) => tabs.push({ label: tab.label, id: tab.id }));
2671
+ }
2672
+ if (this.config.showVideoTab !== false) {
2673
+ tabs.push({ label: "VIDEO", id: "video" });
2674
+ }
2675
+ if (this.config.showModTab !== false && this.isOwner) {
2676
+ tabs.push({ label: "MOD", id: "mod" });
2677
+ }
2678
+ if (!tabs.find((tab) => tab.id === this.activeTab) && tabs.length > 0) {
2679
+ this.activeTab = tabs[0].id;
2680
+ }
2681
+ return /* @__PURE__ */ ReactEcs4.createElement(
2682
+ UiEntity4,
2683
+ {
2684
+ uiTransform: {
2685
+ width: "100%",
2686
+ height: "100%",
2687
+ positionType: "absolute"
2688
+ }
2689
+ },
2690
+ /* @__PURE__ */ ReactEcs4.createElement(
2691
+ UiEntity4,
2692
+ {
2693
+ uiTransform: {
2694
+ position: { right: 20 + this.s(100) + 10, bottom: 10 },
2695
+ positionType: "absolute"
2696
+ }
2697
+ },
2698
+ /* @__PURE__ */ ReactEcs4.createElement(
2699
+ Button3,
2700
+ {
2701
+ uiTransform: { width: this.s(100), height: this.s(45) },
2702
+ uiBackground: { color: this.panelOpen ? C.btn : C.header },
2703
+ value: this.panelOpen ? "CLOSE" : "ADMIN",
2704
+ fontSize: this.s(14),
2705
+ color: C.text,
2706
+ onMouseDown: () => this.toggle()
2707
+ }
2708
+ )
2709
+ ),
2710
+ this.panelOpen && /* @__PURE__ */ ReactEcs4.createElement(
2711
+ UiEntity4,
2712
+ {
2713
+ key: `admin-panel-${this.client.uiScale}`,
2714
+ uiTransform: {
2715
+ width: this.s(UI_DIMENSIONS.admin.width),
2716
+ height: this.s(UI_DIMENSIONS.admin.height),
2717
+ maxHeight: this.s(UI_DIMENSIONS.admin.maxHeight),
2718
+ position: { right: UI_DIMENSIONS.chat.right + this.s(UI_DIMENSIONS.chat.width) + 10, bottom: UI_DIMENSIONS.admin.bottom },
2719
+ positionType: "absolute",
2720
+ flexDirection: "column"
2721
+ },
2722
+ uiBackground: { color: C.bg }
2723
+ },
2724
+ /* @__PURE__ */ ReactEcs4.createElement(
2725
+ UiEntity4,
2726
+ {
2727
+ uiTransform: {
2728
+ width: "100%",
2729
+ height: this.s(UI_DIMENSIONS.admin.headerHeight),
2730
+ justifyContent: "space-between",
2731
+ alignItems: "center",
2732
+ flexDirection: "row",
2733
+ padding: { left: 12, right: 8 }
2734
+ },
2735
+ uiBackground: { color: C.header }
2736
+ },
2737
+ /* @__PURE__ */ ReactEcs4.createElement(Label4, { value: this.config.title || "ADMIN PANEL", fontSize: t.header, color: C.text }),
2738
+ /* @__PURE__ */ ReactEcs4.createElement(
2739
+ Button3,
2740
+ {
2741
+ uiTransform: { width: this.s(28), height: this.s(28) },
2742
+ uiBackground: { color: Color45.create(0, 0, 0, 0.3) },
2743
+ value: "X",
2744
+ fontSize: t.header,
2745
+ color: THEME.colors.red,
2746
+ onMouseDown: () => this.hide()
2747
+ }
2748
+ )
2749
+ ),
2750
+ /* @__PURE__ */ ReactEcs4.createElement(UiEntity4, { uiTransform: { width: "100%", height: this.s(UI_DIMENSIONS.admin.tabHeight), flexDirection: "row" } }, tabs.map((tab) => /* @__PURE__ */ ReactEcs4.createElement(this.TabBtn, { label: tab.label, tab: tab.id }))),
2751
+ /* @__PURE__ */ ReactEcs4.createElement(
2752
+ UiEntity4,
2753
+ {
2754
+ uiTransform: {
2755
+ width: "100%",
2756
+ flexGrow: 1,
2757
+ overflow: "scroll",
2758
+ flexDirection: "column"
2759
+ }
2760
+ },
2761
+ this.activeTab === "video" && /* @__PURE__ */ ReactEcs4.createElement(this.VideoTab, null),
2762
+ this.activeTab === "mod" && this.isOwner && /* @__PURE__ */ ReactEcs4.createElement(this.ModTab, null),
2763
+ this.config.sceneTabs?.map((tab) => this.activeTab === tab.id && tab.render())
2764
+ ),
2765
+ /* @__PURE__ */ ReactEcs4.createElement(
2766
+ UiEntity4,
2767
+ {
2768
+ uiTransform: {
2769
+ width: "100%",
2770
+ height: this.s(UI_DIMENSIONS.admin.footerHeight),
2771
+ justifyContent: "center",
2772
+ alignItems: "center"
2773
+ },
2774
+ uiBackground: { color: C.section }
2775
+ },
2776
+ /* @__PURE__ */ ReactEcs4.createElement(
2777
+ Button3,
2778
+ {
2779
+ uiTransform: { height: this.s(24) },
2780
+ uiBackground: { color: Color45.create(0, 0, 0, 0) },
2781
+ value: `thestatic.tv/scene/${this.config.sceneId} \u2192`,
2782
+ fontSize: t.labelSmall,
2783
+ color: C.cyan,
2784
+ onMouseDown: () => openExternalUrl3({ url: this.config.footerLink || `https://thestatic.tv/scene/${this.config.sceneId}` })
2785
+ }
2786
+ )
2787
+ )
2788
+ )
2789
+ );
2790
+ };
2791
+ this.client = client;
2792
+ if (!config.sceneId) {
2793
+ throw new Error("[AdminPanel] sceneId is required");
2794
+ }
2795
+ this.config = {
2796
+ showVideoTab: true,
2797
+ showModTab: true,
2798
+ title: "ADMIN PANEL",
2799
+ debug: false,
2800
+ ...config,
2801
+ sceneId: config.sceneId
2802
+ // Ensure sceneId is set
2803
+ };
2804
+ this.baseUrl = client.getBaseUrl();
2805
+ if (config.headerColor) {
2806
+ C.header = Color45.create(
2807
+ config.headerColor.r,
2808
+ config.headerColor.g,
2809
+ config.headerColor.b,
2810
+ config.headerColor.a
2811
+ );
2812
+ }
2813
+ }
2814
+ log(msg, ...args) {
2815
+ if (this.config.debug) {
2816
+ console.log(`[AdminPanel] ${msg}`, ...args);
2817
+ }
2818
+ }
2819
+ /** Scale a dimension by shared uiScale */
2820
+ s(value) {
2821
+ return Math.round(value * this.client.uiScale);
2822
+ }
2823
+ /** Get scaled theme */
2824
+ get theme() {
2825
+ return scaleAdminTheme(DEFAULT_ADMIN_THEME, this.client.uiScale);
2826
+ }
2827
+ /**
2828
+ * Initialize the admin panel - checks admin status and fetches video state
2829
+ */
2830
+ async init() {
2831
+ await this.checkAdminStatus();
2832
+ await this.fetchVideoState();
2833
+ this.autoPlayDefault();
2834
+ this.log("Initialized");
2835
+ }
2836
+ /**
2837
+ * Check if current player is an admin for this scene
2838
+ */
2839
+ async checkAdminStatus() {
2840
+ const player = getPlayer2();
2841
+ if (!player?.userId) {
2842
+ this.log("No player data yet");
2843
+ return;
2844
+ }
2845
+ this.playerWallet = player.userId;
2846
+ if (this.config.forceAdmin) {
2847
+ this.isAdmin = true;
2848
+ this.isOwner = true;
2849
+ this.log("Admin status FORCED (forceAdmin: true)");
2850
+ return;
2851
+ }
2852
+ try {
2853
+ const res = await fetch(
2854
+ `${this.baseUrl}/scene/${this.config.sceneId}/admin-check?wallet=${player.userId}`
2855
+ );
2856
+ if (res.ok) {
2857
+ const data = await res.json();
2858
+ this.isAdmin = data.showButton ?? data.hasAccess;
2859
+ this.isOwner = data.isOwner || data.isSceneAdmin;
2860
+ if (data.isBanned) {
2861
+ this.config.onBroadcast?.("You have been banned from this scene.");
2862
+ this.banKickPlayer();
2863
+ this.log("Player is banned - kicking");
2864
+ }
2865
+ this.log("Admin status:", this.isAdmin, "Owner:", this.isOwner);
2866
+ }
2867
+ } catch (err) {
2868
+ this.log("Admin check error:", err);
2869
+ }
2870
+ }
2871
+ /**
2872
+ * Toggle the admin panel open/closed
2873
+ */
2874
+ toggle() {
2875
+ if (this.panelOpen) {
2876
+ this.hide();
2877
+ } else {
2878
+ this.show();
2879
+ }
2880
+ }
2881
+ /**
2882
+ * Show the admin panel
2883
+ */
2884
+ show() {
2885
+ if (this.panelOpen) return;
2886
+ this.client.closeOtherPanels("admin");
2887
+ this.panelOpen = true;
2888
+ if (this.activeTab === "video") {
2889
+ this.startStreamPolling();
2890
+ }
2891
+ }
2892
+ /**
2893
+ * Hide the admin panel
2894
+ */
2895
+ hide() {
2896
+ if (!this.panelOpen) return;
2897
+ this.panelOpen = false;
2898
+ this.stopStreamPolling();
2899
+ }
2900
+ /**
2901
+ * Check if the panel is currently open
2902
+ */
2903
+ get isOpen() {
2904
+ return this.panelOpen;
2905
+ }
2906
+ /**
2907
+ * Check if current user has admin access
2908
+ */
2909
+ get hasAccess() {
2910
+ return this.isAdmin;
2911
+ }
2912
+ /**
2913
+ * Register a custom scene tab (Pro tier)
2914
+ */
2915
+ registerSceneTab(tab) {
2916
+ if (!this.config.sceneTabs) {
2917
+ this.config.sceneTabs = [];
2918
+ }
2919
+ this.config.sceneTabs.push(tab);
2920
+ this.log("Registered scene tab:", tab.label);
2921
+ }
2922
+ // --- Stream Polling ---
2923
+ startStreamPolling() {
2924
+ if (this.pollIntervalId !== null) return;
2925
+ this.pollIntervalId = dclSetInterval(() => {
2926
+ if (this.activeTab === "video" && this.panelOpen && this.streamData?.hasChannel) {
2927
+ this.refreshStreamStatus();
2928
+ }
2929
+ }, 1e4);
2930
+ this.log("Stream polling started");
2931
+ }
2932
+ stopStreamPolling() {
2933
+ if (this.pollIntervalId !== null) {
2934
+ dclClearInterval(this.pollIntervalId);
2935
+ this.pollIntervalId = null;
2936
+ this.log("Stream polling stopped");
2937
+ }
2938
+ }
2939
+ // --- Stream API Calls ---
2940
+ async fetchStreamData() {
2941
+ if (this.streamFetched || !this.playerWallet) return;
2942
+ try {
2943
+ const res = await fetch(
2944
+ `${this.baseUrl}/scene/${this.config.sceneId}/stream?wallet=${this.playerWallet}`
2945
+ );
2946
+ if (res.ok) {
2947
+ this.streamData = await res.json();
2948
+ this.streamFetched = true;
2949
+ this.log("Stream data fetched:", this.streamData?.channelId || "no channel");
2950
+ }
2951
+ } catch (err) {
2952
+ this.log("Stream fetch error:", err);
2953
+ }
2954
+ }
2955
+ async refreshStreamStatus() {
2956
+ if (!this.playerWallet) return;
2957
+ try {
2958
+ const res = await fetch(
2959
+ `${this.baseUrl}/scene/${this.config.sceneId}/stream?wallet=${this.playerWallet}`
2960
+ );
2961
+ if (res.ok) {
2962
+ this.streamData = await res.json();
2963
+ }
2964
+ } catch (err) {
2965
+ }
2966
+ }
2967
+ // --- Video State (Slots) ---
2968
+ async fetchVideoState() {
2969
+ if (this.videoStateFetched) return;
2970
+ try {
2971
+ const res = await fetch(
2972
+ `${this.baseUrl}/scene/${this.config.sceneId}/video-state`
2973
+ );
2974
+ if (res.ok) {
2975
+ const data = await res.json();
2976
+ this.videoState = {
2977
+ defaultSlot: data.defaultSlot || null,
2978
+ videoSlots: data.videoSlots || {}
2979
+ };
2980
+ this.videoStateFetched = true;
2981
+ this.log("Video state fetched:", this.videoState.defaultSlot || "no default");
2982
+ }
2983
+ } catch (err) {
2984
+ this.log("Video state fetch error:", err);
2985
+ }
2986
+ }
2987
+ /**
2988
+ * Play a video slot by ID - looks up URL and calls onVideoPlay
2989
+ */
2990
+ playSlot(slotId) {
2991
+ const slot = this.videoState?.videoSlots?.[slotId];
2992
+ if (slot?.url) {
2993
+ this.log("Playing slot:", slotId, slot.url);
2994
+ this.config.onVideoPlay?.(slot.url);
2995
+ } else {
2996
+ this.log("Slot has no URL:", slotId);
2997
+ this.config.onVideoSlotPlay?.(slotId);
2998
+ }
2999
+ }
3000
+ /**
3001
+ * Auto-play the default slot if configured
3002
+ */
3003
+ autoPlayDefault() {
3004
+ if (!this.videoState?.defaultSlot) return;
3005
+ const defaultSlot = this.videoState.defaultSlot;
3006
+ const slot = this.videoState.videoSlots?.[defaultSlot];
3007
+ if (slot?.url) {
3008
+ this.log("Auto-playing default slot:", defaultSlot);
3009
+ this.config.onVideoPlay?.(slot.url);
3010
+ }
3011
+ }
3012
+ async createChannel() {
3013
+ if (!this.playerWallet || this.channelCreating) return;
3014
+ this.channelCreating = true;
3015
+ this.channelCreateError = "";
3016
+ try {
3017
+ this.log("Creating channel for scene...");
3018
+ const res = await fetch(
3019
+ `${this.baseUrl}/scene/${this.config.sceneId}/stream/create`,
3020
+ {
3021
+ method: "POST",
3022
+ headers: { "Content-Type": "application/json" },
3023
+ body: JSON.stringify({ wallet: this.playerWallet })
3024
+ }
3025
+ );
3026
+ const data = await res.json();
3027
+ if (res.ok) {
3028
+ this.log("Channel created:", data.channel);
3029
+ this.streamFetched = false;
3030
+ await this.fetchStreamData();
3031
+ this.config.onBroadcast?.(`Channel created: ${data.channel.channelId}`);
3032
+ } else {
3033
+ this.channelCreateError = data.error || "Failed to create channel";
3034
+ this.log("Channel creation failed:", data.error);
3035
+ }
3036
+ } catch (err) {
3037
+ this.channelCreateError = "Network error creating channel";
3038
+ this.log("Channel creation error:", err);
3039
+ }
3040
+ this.channelCreating = false;
3041
+ }
3042
+ async claimTrial() {
3043
+ if (!this.playerWallet || this.trialClaiming) return;
3044
+ this.trialClaiming = true;
3045
+ this.trialClaimError = "";
3046
+ try {
3047
+ this.log("Claiming streaming trial...");
3048
+ const res = await fetch(
3049
+ `${this.baseUrl}/scene/${this.config.sceneId}/stream/claim-trial`,
3050
+ {
3051
+ method: "POST",
3052
+ headers: { "Content-Type": "application/json" },
3053
+ body: JSON.stringify({ wallet: this.playerWallet })
3054
+ }
3055
+ );
3056
+ const data = await res.json();
3057
+ if (res.ok) {
3058
+ this.log("Trial claimed:", data.channel);
3059
+ this.streamFetched = false;
3060
+ await this.fetchStreamData();
3061
+ this.config.onBroadcast?.(`4-hour streaming trial activated!`);
3062
+ } else {
3063
+ this.trialClaimError = data.error || "Failed to claim trial";
3064
+ this.log("Trial claim failed:", data.error);
3065
+ }
3066
+ } catch (err) {
3067
+ this.trialClaimError = "Network error claiming trial";
3068
+ this.log("Trial claim error:", err);
3069
+ }
3070
+ this.trialClaiming = false;
3071
+ }
3072
+ async deleteChannel() {
3073
+ if (!this.playerWallet || this.channelDeleting || !this.streamData?.hasChannel) return;
3074
+ this.channelDeleting = true;
3075
+ this.channelDeleteError = "";
3076
+ try {
3077
+ this.log("Deleting channel for scene...");
3078
+ const res = await fetch(
3079
+ `${this.baseUrl}/scene/${this.config.sceneId}/stream/delete`,
3080
+ {
3081
+ method: "POST",
3082
+ headers: { "Content-Type": "application/json" },
3083
+ body: JSON.stringify({ wallet: this.playerWallet })
3084
+ }
3085
+ );
3086
+ const data = await res.json();
3087
+ if (res.ok) {
3088
+ this.log("Channel deleted:", data);
3089
+ this.streamData = null;
3090
+ this.streamFetched = false;
3091
+ this.config.onBroadcast?.("Channel deleted successfully");
3092
+ } else {
3093
+ this.channelDeleteError = data.error || "Failed to delete channel";
3094
+ this.log("Channel deletion failed:", data.error);
3095
+ }
3096
+ } catch (err) {
3097
+ this.channelDeleteError = "Network error deleting channel";
3098
+ this.log("Channel deletion error:", err);
3099
+ }
3100
+ this.channelDeleting = false;
3101
+ }
3102
+ async startStream() {
3103
+ if (!this.playerWallet || this.streamControlling || !this.streamData?.hasChannel || this.streamData.isLive) return;
3104
+ if ((this.streamData.sparksBalance || 0) <= 0) {
3105
+ this.streamControlStatus = "error";
3106
+ this.config.onBroadcast?.("No Sparks - cannot start stream");
3107
+ return;
3108
+ }
3109
+ this.streamControlling = true;
3110
+ this.streamControlStatus = "";
3111
+ try {
3112
+ this.log("Starting stream...");
3113
+ const res = await fetch(
3114
+ `${this.baseUrl}/scene/${this.config.sceneId}/stream`,
3115
+ {
3116
+ method: "POST",
3117
+ headers: { "Content-Type": "application/json" },
3118
+ body: JSON.stringify({ action: "start", wallet: this.playerWallet })
3119
+ }
3120
+ );
3121
+ const data = await res.json();
3122
+ if (res.ok) {
3123
+ this.streamControlStatus = "started";
3124
+ this.log("Stream started");
3125
+ this.config.onBroadcast?.("Stream started!");
3126
+ this.streamFetched = false;
3127
+ await this.fetchStreamData();
3128
+ } else {
3129
+ this.streamControlStatus = "error";
3130
+ this.log("Start stream failed:", data.error);
3131
+ this.config.onBroadcast?.(data.error || "Failed to start");
3132
+ }
3133
+ } catch (err) {
3134
+ this.streamControlStatus = "error";
3135
+ this.log("Start stream error:", err);
3136
+ }
3137
+ this.streamControlling = false;
3138
+ }
3139
+ async stopStream() {
3140
+ if (!this.playerWallet || this.streamControlling || !this.streamData?.hasChannel || !this.streamData.isLive) return;
3141
+ this.streamControlling = true;
3142
+ this.streamControlStatus = "";
3143
+ try {
3144
+ this.log("Stopping stream...");
3145
+ const res = await fetch(
3146
+ `${this.baseUrl}/scene/${this.config.sceneId}/stream`,
3147
+ {
3148
+ method: "POST",
3149
+ headers: { "Content-Type": "application/json" },
3150
+ body: JSON.stringify({ action: "stop", wallet: this.playerWallet })
3151
+ }
3152
+ );
3153
+ const data = await res.json();
3154
+ if (res.ok) {
3155
+ this.streamControlStatus = "stopped";
3156
+ this.log("Stream stopped");
3157
+ this.config.onBroadcast?.("Stream stopped");
3158
+ this.streamFetched = false;
3159
+ await this.fetchStreamData();
3160
+ } else {
3161
+ this.streamControlStatus = "error";
3162
+ this.log("Stop stream failed:", data.error);
3163
+ }
3164
+ } catch (err) {
3165
+ this.streamControlStatus = "error";
3166
+ this.log("Stop stream error:", err);
3167
+ }
3168
+ this.streamControlling = false;
3169
+ }
3170
+ async rotateStreamKey() {
3171
+ if (!this.playerWallet || this.keyRotating || !this.streamData?.hasChannel) return;
3172
+ this.keyRotating = true;
3173
+ this.keyRotateStatus = "";
3174
+ try {
3175
+ this.log("Rotating stream key...");
3176
+ const res = await fetch(
3177
+ `${this.baseUrl}/scene/${this.config.sceneId}/stream`,
3178
+ {
3179
+ method: "POST",
3180
+ headers: { "Content-Type": "application/json" },
3181
+ body: JSON.stringify({ action: "rotateKey", wallet: this.playerWallet })
3182
+ }
3183
+ );
3184
+ const data = await res.json();
3185
+ if (res.ok) {
3186
+ this.keyRotateStatus = "success";
3187
+ this.log("Key rotated:", data.message);
3188
+ this.config.onBroadcast?.("Stream key rotated - update OBS settings");
3189
+ } else {
3190
+ this.keyRotateStatus = "error";
3191
+ this.log("Key rotation failed:", data.error);
3192
+ }
3193
+ } catch (err) {
3194
+ this.keyRotateStatus = "error";
3195
+ this.log("Key rotation error:", err);
3196
+ }
3197
+ this.keyRotating = false;
3198
+ }
3199
+ // --- Mod Tab API Calls ---
3200
+ async fetchModData() {
3201
+ if (!this.playerWallet) return;
3202
+ this.modStatus = "loading";
3203
+ try {
3204
+ const res = await fetch(
3205
+ `${this.baseUrl}/scene/${this.config.sceneId}/config?wallet=${this.playerWallet}`,
3206
+ { headers: { "Content-Type": "application/json" } }
3207
+ );
3208
+ if (res.ok) {
3209
+ const data = await res.json();
3210
+ this.sceneAdmins = data.sceneAdmins || [];
3211
+ this.bannedWallets = data.bannedWallets || [];
3212
+ this.modsFetched = true;
3213
+ this.modStatus = "";
3214
+ this.log("Fetched mod data:", { sceneAdmins: this.sceneAdmins, bannedWallets: this.bannedWallets });
3215
+ } else {
3216
+ this.modStatus = "error";
3217
+ }
3218
+ } catch (err) {
3219
+ this.modStatus = "error";
3220
+ this.log("Fetch mod data error:", err);
3221
+ }
3222
+ }
3223
+ async addSceneAdmin(wallet) {
3224
+ if (!wallet || !this.playerWallet) return;
3225
+ const normalized = wallet.toLowerCase().trim();
3226
+ if (!/^0x[a-f0-9]{40}$/i.test(normalized)) {
3227
+ this.modStatus = "error";
3228
+ return;
3229
+ }
3230
+ if (this.sceneAdmins.includes(normalized)) return;
3231
+ this.modStatus = "loading";
3232
+ try {
3233
+ const res = await fetch(
3234
+ `${this.baseUrl}/scene/${this.config.sceneId}/config`,
3235
+ {
3236
+ method: "POST",
3237
+ headers: { "Content-Type": "application/json" },
3238
+ body: JSON.stringify({
3239
+ sceneAdmins: [...this.sceneAdmins, normalized],
3240
+ wallet: this.playerWallet
3241
+ })
3242
+ }
3243
+ );
3244
+ if (res.ok) {
3245
+ this.sceneAdmins = [...this.sceneAdmins, normalized];
3246
+ this.newAdminWallet = "";
3247
+ this.modStatus = "saved";
3248
+ this.log("Added scene admin:", normalized);
3249
+ } else {
3250
+ this.modStatus = "error";
3251
+ }
3252
+ } catch (err) {
3253
+ this.modStatus = "error";
3254
+ this.log("Add admin error:", err);
3255
+ }
3256
+ }
3257
+ async removeSceneAdmin(wallet) {
3258
+ if (!wallet || !this.playerWallet) return;
3259
+ this.modStatus = "loading";
3260
+ try {
3261
+ const res = await fetch(
3262
+ `${this.baseUrl}/scene/${this.config.sceneId}/config`,
3263
+ {
3264
+ method: "POST",
3265
+ headers: { "Content-Type": "application/json" },
3266
+ body: JSON.stringify({
3267
+ sceneAdmins: this.sceneAdmins.filter((w) => w !== wallet),
3268
+ wallet: this.playerWallet
3269
+ })
3270
+ }
3271
+ );
3272
+ if (res.ok) {
3273
+ this.sceneAdmins = this.sceneAdmins.filter((w) => w !== wallet);
3274
+ this.modStatus = "saved";
3275
+ this.log("Removed scene admin:", wallet);
3276
+ } else {
3277
+ this.modStatus = "error";
3278
+ }
3279
+ } catch (err) {
3280
+ this.modStatus = "error";
3281
+ this.log("Remove admin error:", err);
3282
+ }
3283
+ }
3284
+ async banWallet(wallet) {
3285
+ if (!wallet || !this.playerWallet) return;
3286
+ const normalized = wallet.toLowerCase().trim();
3287
+ if (!/^0x[a-f0-9]{40}$/i.test(normalized)) {
3288
+ this.modStatus = "error";
3289
+ return;
3290
+ }
3291
+ if (this.bannedWallets.includes(normalized)) return;
3292
+ this.modStatus = "loading";
3293
+ try {
3294
+ const res = await fetch(
3295
+ `${this.baseUrl}/scene/${this.config.sceneId}/config`,
3296
+ {
3297
+ method: "POST",
3298
+ headers: { "Content-Type": "application/json" },
3299
+ body: JSON.stringify({
3300
+ bannedWallets: [...this.bannedWallets, normalized],
3301
+ wallet: this.playerWallet
3302
+ })
3303
+ }
3304
+ );
3305
+ if (res.ok) {
3306
+ this.bannedWallets = [...this.bannedWallets, normalized];
3307
+ this.newBanWallet = "";
3308
+ this.modStatus = "saved";
3309
+ this.log("Banned wallet:", normalized);
3310
+ this.config.onCommand?.("kickBanned", { wallet: normalized });
3311
+ } else {
3312
+ this.modStatus = "error";
3313
+ }
3314
+ } catch (err) {
3315
+ this.modStatus = "error";
3316
+ this.log("Ban error:", err);
3317
+ }
3318
+ }
3319
+ async unbanWallet(wallet) {
3320
+ if (!wallet || !this.playerWallet) return;
3321
+ this.modStatus = "loading";
3322
+ try {
3323
+ const res = await fetch(
3324
+ `${this.baseUrl}/scene/${this.config.sceneId}/config`,
3325
+ {
3326
+ method: "POST",
3327
+ headers: { "Content-Type": "application/json" },
3328
+ body: JSON.stringify({
3329
+ bannedWallets: this.bannedWallets.filter((w) => w !== wallet),
3330
+ wallet: this.playerWallet
3331
+ })
3332
+ }
3333
+ );
3334
+ if (res.ok) {
3335
+ this.bannedWallets = this.bannedWallets.filter((w) => w !== wallet);
3336
+ this.modStatus = "saved";
3337
+ this.log("Unbanned wallet:", wallet);
3338
+ } else {
3339
+ this.modStatus = "error";
3340
+ }
3341
+ } catch (err) {
3342
+ this.modStatus = "error";
3343
+ this.log("Unban error:", err);
3344
+ }
3345
+ }
3346
+ banKickPlayer() {
3347
+ movePlayerTo({ newRelativePosition: BAN_KICK_POSITION });
3348
+ this.log("Player ban-kicked");
3349
+ }
3350
+ sendBroadcast() {
3351
+ if (!this.broadcastText.trim()) return;
3352
+ this.config.onBroadcast?.(this.broadcastText.trim());
3353
+ this.broadcastText = "";
3354
+ this.log("Broadcast sent");
3355
+ }
3356
+ setActiveTab(tab) {
3357
+ const previousTab = this.activeTab;
3358
+ this.activeTab = tab;
3359
+ if (tab === "mod" && !this.modsFetched && this.isOwner) {
3360
+ this.fetchModData();
3361
+ }
3362
+ if (tab === "video" && previousTab !== "video") {
3363
+ this.startStreamPolling();
3364
+ } else if (tab !== "video" && previousTab === "video") {
3365
+ this.stopStreamPolling();
3366
+ }
3367
+ }
3368
+ };
3369
+
3370
+ // src/ui/ui-renderer.tsx
3371
+ import ReactEcs5, { ReactEcsRenderer, UiEntity as UiEntity5, Label as Label5 } from "@dcl/sdk/react-ecs";
3372
+ import { Color4 as Color46 } from "@dcl/sdk/math";
3373
+ import { engine as engine2 } from "@dcl/sdk/ecs";
3374
+ var notificationText = "";
3375
+ var notificationVisible = false;
3376
+ var notificationEndTime = 0;
3377
+ var notificationInitialized = false;
3378
+ function showNotification(message, durationMs = 5e3) {
3379
+ notificationText = message;
3380
+ notificationVisible = true;
3381
+ notificationEndTime = Date.now() + durationMs;
3382
+ }
3383
+ function initNotificationSystem() {
3384
+ if (notificationInitialized) return;
3385
+ notificationInitialized = true;
3386
+ engine2.addSystem(() => {
3387
+ if (notificationVisible && Date.now() > notificationEndTime) {
3388
+ notificationVisible = false;
3389
+ }
3390
+ });
3391
+ }
3392
+ function NotificationBanner() {
3393
+ if (!notificationVisible) return null;
3394
+ return /* @__PURE__ */ ReactEcs5.createElement(
3395
+ UiEntity5,
3396
+ {
3397
+ uiTransform: {
3398
+ positionType: "absolute",
3399
+ position: { top: 80 },
3400
+ width: 500,
3401
+ height: 60,
3402
+ padding: 16,
3403
+ alignSelf: "center",
3404
+ justifyContent: "center",
3405
+ alignItems: "center"
3406
+ },
3407
+ uiBackground: { color: Color46.create(0.1, 0.1, 0.15, 0.95) }
3408
+ },
3409
+ /* @__PURE__ */ ReactEcs5.createElement(
3410
+ Label5,
3411
+ {
3412
+ value: notificationText,
3413
+ fontSize: 18,
3414
+ color: Color46.create(0, 1, 1, 1),
3415
+ textAlign: "middle-center"
3416
+ }
3417
+ )
3418
+ );
3419
+ }
3420
+ function createStaticUI(client) {
3421
+ initNotificationSystem();
3422
+ return function StaticUI() {
3423
+ const currentScale = client.uiScale;
3424
+ const guideComponent = client.guideUI?.getComponent() ?? null;
3425
+ const chatComponent = client.chatUI?.getComponent() ?? null;
3426
+ const adminComponent = client.adminPanel?.getComponent() ?? null;
3427
+ return /* @__PURE__ */ ReactEcs5.createElement(
3428
+ UiEntity5,
3429
+ {
3430
+ key: `static-ui-root-${currentScale}`,
3431
+ uiTransform: {
3432
+ width: "100%",
3433
+ height: "100%",
3434
+ positionType: "absolute",
3435
+ flexDirection: "column",
3436
+ alignItems: "center"
3437
+ }
3438
+ },
3439
+ /* @__PURE__ */ ReactEcs5.createElement(NotificationBanner, null),
3440
+ guideComponent,
3441
+ chatComponent,
3442
+ adminComponent
3443
+ );
3444
+ };
3445
+ }
3446
+ function setupStaticUI(client) {
3447
+ const StaticUI = createStaticUI(client);
3448
+ ReactEcsRenderer.setUiRenderer(StaticUI);
3449
+ }
3450
+
2124
3451
  // src/StaticTVClient.ts
2125
3452
  var DEFAULT_BASE_URL = "https://thestatic.tv/api/v1/dcl";
2126
3453
  var KEY_TYPE_CHANNEL = "channel";
@@ -2133,34 +3460,41 @@ var StaticTVClient = class {
2133
3460
  *
2134
3461
  * @example
2135
3462
  * ```typescript
2136
- * // Full access with channel key
2137
- * const staticTV = new StaticTVClient({
2138
- * apiKey: 'dclk_your_channel_key_here',
2139
- * debug: true
2140
- * });
3463
+ * let staticTV: StaticTVClient
2141
3464
  *
2142
- * // Lite mode with scene key (visitors only)
2143
- * const staticTV = new StaticTVClient({
2144
- * apiKey: 'dcls_your_scene_key_here'
2145
- * });
3465
+ * export function main() {
3466
+ * // All keys use dcls_ prefix - features determined by subscription
3467
+ * staticTV = new StaticTVClient({
3468
+ * apiKey: 'dcls_your_key_here'
3469
+ * })
3470
+ * // Session tracking starts automatically!
3471
+ * }
2146
3472
  * ```
2147
3473
  */
2148
3474
  constructor(config) {
2149
3475
  this._keyType = null;
3476
+ this._keyId = null;
2150
3477
  this._disabled = false;
2151
- this._fullFeaturesEnabled = false;
2152
- /** Guide module - fetch channel lineup (full SDK only) */
3478
+ this._tier = "free";
3479
+ this._standardFeaturesEnabled = false;
3480
+ this._proFeaturesEnabled = false;
3481
+ this._pendingProConfig = null;
3482
+ /** Guide module - fetch channel lineup (standard/pro tier) */
2153
3483
  this.guide = null;
2154
- /** Session module - track visitor sessions (all keys, null when disabled) */
3484
+ /** Session module - track visitor sessions (all tiers, null when disabled) */
2155
3485
  this.session = null;
2156
- /** Heartbeat module - track video watching (full SDK only) */
3486
+ /** Heartbeat module - track video watching (standard/pro tier) */
2157
3487
  this.heartbeat = null;
2158
- /** Interactions module - like/follow channels (full SDK only) */
3488
+ /** Interactions module - like/follow channels (standard/pro tier) */
2159
3489
  this.interactions = null;
2160
- /** Guide UI module - channel browser UI (full SDK only) */
3490
+ /** Guide UI module - channel browser UI (standard/pro tier) */
2161
3491
  this.guideUI = null;
2162
- /** Chat UI module - real-time chat UI (full SDK only) */
3492
+ /** Chat UI module - real-time chat UI (standard/pro tier) */
2163
3493
  this.chatUI = null;
3494
+ /** Admin Panel module - Video/Mod tabs (pro tier only) */
3495
+ this.adminPanel = null;
3496
+ /** UI scale - fixed at 1.0. DCL's UI system auto-scales based on viewport. */
3497
+ this.uiScale = 1;
2164
3498
  this.config = {
2165
3499
  autoStartSession: true,
2166
3500
  sessionHeartbeatInterval: 3e4,
@@ -2170,7 +3504,6 @@ var StaticTVClient = class {
2170
3504
  };
2171
3505
  this.baseUrl = config.baseUrl || DEFAULT_BASE_URL;
2172
3506
  if (!config.apiKey) {
2173
- console.log("[StaticTV] No apiKey provided - tracking disabled. Scene will load normally.");
2174
3507
  this._disabled = true;
2175
3508
  this._keyType = null;
2176
3509
  this.session = null;
@@ -2186,14 +3519,15 @@ var StaticTVClient = class {
2186
3519
  } else if (config.apiKey.startsWith("dcls_")) {
2187
3520
  this._keyType = KEY_TYPE_SCENE;
2188
3521
  } else {
2189
- console.log("[StaticTV] Invalid apiKey format (must start with dclk_ or dcls_) - tracking disabled. Scene will load normally.");
3522
+ console.warn("[TheStatic] Invalid API key format - get your key at thestatic.tv/dashboard");
2190
3523
  this._disabled = true;
2191
3524
  this._keyType = null;
2192
3525
  return;
2193
3526
  }
2194
3527
  this.session = new SessionModule(this);
2195
3528
  if (this._keyType === KEY_TYPE_CHANNEL) {
2196
- this._initFullModules();
3529
+ this._tier = "standard";
3530
+ this._initStandardModules();
2197
3531
  }
2198
3532
  if (this.config.autoStartSession) {
2199
3533
  fetchUserData().then(() => {
@@ -2209,6 +3543,10 @@ var StaticTVClient = class {
2209
3543
  }
2210
3544
  this.log(`StaticTVClient initialized (${this._keyType} mode)`);
2211
3545
  }
3546
+ /** Get the API base URL (for internal module use) */
3547
+ getBaseUrl() {
3548
+ return this.baseUrl;
3549
+ }
2212
3550
  /**
2213
3551
  * Get the key type (channel, scene, or null if disabled)
2214
3552
  */
@@ -2222,11 +3560,23 @@ var StaticTVClient = class {
2222
3560
  return this._disabled;
2223
3561
  }
2224
3562
  /**
2225
- * Check if this is a lite client (no full features)
2226
- * Returns true until session confirms sdkType is 'full'
3563
+ * Get the current SDK tier (free, standard, or pro)
3564
+ */
3565
+ get tier() {
3566
+ return this._tier;
3567
+ }
3568
+ /**
3569
+ * Check if this is a free tier client (session tracking only)
3570
+ * Returns true until session confirms a higher tier
3571
+ */
3572
+ get isFree() {
3573
+ return this._tier === "free";
3574
+ }
3575
+ /**
3576
+ * @deprecated Use `isFree` instead. Kept for backward compatibility.
2227
3577
  */
2228
3578
  get isLite() {
2229
- return !this._fullFeaturesEnabled;
3579
+ return this.isFree;
2230
3580
  }
2231
3581
  /**
2232
3582
  * Make an authenticated API request
@@ -2244,14 +3594,29 @@ var StaticTVClient = class {
2244
3594
  });
2245
3595
  }
2246
3596
  /**
2247
- * Log a message if debug is enabled
3597
+ * Log a debug message (only when debug: true)
2248
3598
  * @internal
2249
3599
  */
2250
3600
  log(message, ...args) {
2251
3601
  if (this.config.debug) {
2252
- console.log(`[StaticTV] ${message}`, ...args);
3602
+ console.log(`[TheStatic] ${message}`, ...args);
2253
3603
  }
2254
3604
  }
3605
+ /**
3606
+ * Log a warning (always shown)
3607
+ * @internal
3608
+ */
3609
+ warn(message) {
3610
+ console.warn(`[TheStatic] ${message}`);
3611
+ }
3612
+ /**
3613
+ * Log an error (always shown, user-friendly format)
3614
+ * @internal
3615
+ */
3616
+ error(message, err) {
3617
+ const errorDetail = err instanceof Error ? err.message : String(err || "");
3618
+ console.error(`[TheStatic] ${message}${errorDetail ? `: ${errorDetail}` : ""}`);
3619
+ }
2255
3620
  /**
2256
3621
  * Get the current configuration
2257
3622
  * @internal
@@ -2260,41 +3625,199 @@ var StaticTVClient = class {
2260
3625
  return this.config;
2261
3626
  }
2262
3627
  /**
2263
- * Initialize full feature modules (guide, heartbeat, interactions, UI)
3628
+ * Initialize standard feature modules (guide, heartbeat, interactions, UI)
2264
3629
  * @internal
2265
3630
  */
2266
- _initFullModules() {
2267
- if (this._fullFeaturesEnabled) return;
3631
+ _initStandardModules() {
3632
+ if (this._standardFeaturesEnabled) return;
2268
3633
  this.guide = new GuideModule(this);
2269
3634
  this.heartbeat = new HeartbeatModule(this);
2270
3635
  this.interactions = new InteractionsModule(this);
2271
3636
  this.guideUI = new GuideUIModule(this, this.config.guideUI);
2272
3637
  this.chatUI = new ChatUIModule(this, this.config.chatUI);
2273
- this._fullFeaturesEnabled = true;
3638
+ this._standardFeaturesEnabled = true;
2274
3639
  this.chatUI.init().catch((err) => {
2275
3640
  this.log(`Chat init failed: ${err}`);
2276
3641
  });
2277
- this.log("Full features enabled (guide, chat, heartbeat, interactions)");
3642
+ this.log("Standard features enabled (guide, chat, heartbeat, interactions)");
2278
3643
  }
2279
3644
  /**
2280
- * Called by SessionModule when server confirms sdkType is 'full'
2281
- * Enables guide, chat, heartbeat, and interactions modules
3645
+ * Initialize pro feature modules (admin panel)
3646
+ * @internal
3647
+ */
3648
+ _initProModules() {
3649
+ if (this._proFeaturesEnabled || !this._pendingProConfig) return;
3650
+ const configWithDefaults = {
3651
+ ...this._pendingProConfig,
3652
+ sceneId: this._pendingProConfig.sceneId || this._keyId || void 0
3653
+ };
3654
+ if (!configWithDefaults.sceneId) {
3655
+ this.log("Pro features: No sceneId and no keyId available - admin panel disabled");
3656
+ return;
3657
+ }
3658
+ this.adminPanel = new AdminPanelUIModule(this, configWithDefaults);
3659
+ this._proFeaturesEnabled = true;
3660
+ this.adminPanel.init().catch((err) => {
3661
+ this.log(`Admin panel init failed: ${err}`);
3662
+ });
3663
+ this.log(`Pro features enabled (admin panel) - sceneId: ${configWithDefaults.sceneId}`);
3664
+ }
3665
+ /**
3666
+ * Called by SessionModule when server returns the tier
3667
+ * Enables modules based on tier level
3668
+ * @internal
3669
+ */
3670
+ _enableFeaturesForTier(tier, keyId) {
3671
+ this._tier = tier;
3672
+ if (keyId) {
3673
+ this._keyId = keyId;
3674
+ }
3675
+ if (tier === "standard" || tier === "pro") {
3676
+ this._initStandardModules();
3677
+ }
3678
+ if (tier === "pro" && this._pendingProConfig) {
3679
+ this._initProModules();
3680
+ }
3681
+ }
3682
+ /**
3683
+ * @deprecated Use `_enableFeaturesForTier` instead
2282
3684
  * @internal
2283
3685
  */
2284
3686
  _enableFullFeatures() {
2285
- this._initFullModules();
3687
+ this._enableFeaturesForTier("standard");
2286
3688
  }
2287
3689
  /**
2288
- * Check if full features are enabled (server confirmed sdkType: 'full')
3690
+ * Check if standard features are enabled (guide, chat, etc.)
3691
+ */
3692
+ get hasStandardFeatures() {
3693
+ return this._standardFeaturesEnabled;
3694
+ }
3695
+ /**
3696
+ * @deprecated Use `hasStandardFeatures` instead
2289
3697
  */
2290
3698
  get hasFullFeatures() {
2291
- return this._fullFeaturesEnabled;
3699
+ return this._standardFeaturesEnabled;
3700
+ }
3701
+ /**
3702
+ * Check if pro features are enabled (admin panel)
3703
+ */
3704
+ get hasProFeatures() {
3705
+ return this._proFeaturesEnabled;
2292
3706
  }
2293
3707
  /**
2294
- * Get the SDK type (lite or full) - only available after session starts
3708
+ * @deprecated Use `tier` instead
2295
3709
  */
2296
3710
  get sdkType() {
2297
- return this.session?.sdkType || "lite";
3711
+ return this._tier === "free" ? "lite" : "full";
3712
+ }
3713
+ /**
3714
+ * Configure Pro features (Admin Panel with Video + Mod tabs)
3715
+ * Call this after creating the client to configure admin panel.
3716
+ * The panel will auto-enable when server confirms Pro tier.
3717
+ *
3718
+ * @param config Admin panel configuration (optional - defaults work for basic usage)
3719
+ *
3720
+ * @example
3721
+ * ```typescript
3722
+ * const staticTV = new StaticTVClient({ apiKey: 'dcls_...' })
3723
+ *
3724
+ * // Simplest usage - just enable with callbacks:
3725
+ * staticTV.enableProFeatures({
3726
+ * onVideoPlay: (url) => videoPlayer.play(url),
3727
+ * onVideoStop: () => videoPlayer.stop()
3728
+ * })
3729
+ *
3730
+ * // Advanced usage - custom sceneId and title:
3731
+ * staticTV.enableProFeatures({
3732
+ * sceneId: 'my-scene', // optional - defaults to API key ID
3733
+ * title: 'MY SCENE ADMIN',
3734
+ * onVideoPlay: (url) => videoPlayer.play(url),
3735
+ * onVideoStop: () => videoPlayer.stop(),
3736
+ * onBroadcast: (text) => showNotification(text)
3737
+ * })
3738
+ * ```
3739
+ */
3740
+ enableProFeatures(config = {}) {
3741
+ if (this._proFeaturesEnabled) {
3742
+ this.log("Pro features already enabled");
3743
+ return;
3744
+ }
3745
+ this._pendingProConfig = config;
3746
+ if (this._tier === "pro") {
3747
+ this._initProModules();
3748
+ } else {
3749
+ this.log("Pro features configured - will enable when Pro tier is confirmed");
3750
+ }
3751
+ }
3752
+ /**
3753
+ * Register a custom scene tab for the admin panel (Pro tier)
3754
+ * Must call enableProFeatures() first.
3755
+ *
3756
+ * @param tab The tab definition with label, id, and render function
3757
+ *
3758
+ * @example
3759
+ * ```typescript
3760
+ * staticTV.registerSceneTab({
3761
+ * label: 'LIGHTS',
3762
+ * id: 'lights',
3763
+ * render: () => <MyLightsControls />
3764
+ * })
3765
+ * ```
3766
+ */
3767
+ registerSceneTab(tab) {
3768
+ if (!this.adminPanel) {
3769
+ this.log("Cannot register scene tab - call enableProFeatures() first");
3770
+ return;
3771
+ }
3772
+ this.adminPanel.registerSceneTab(tab);
3773
+ }
3774
+ /**
3775
+ * Close Admin/Guide panels (they share the same screen space)
3776
+ * Chat is independent and stays open.
3777
+ * @param except The panel that should stay open: 'admin' | 'guide'
3778
+ */
3779
+ closeOtherPanels(except) {
3780
+ if (except !== "guide" && this.guideUI?.isVisible) {
3781
+ this.guideUI.hide();
3782
+ }
3783
+ if (except !== "admin" && this.adminPanel?.isOpen) {
3784
+ this.adminPanel.hide();
3785
+ }
3786
+ }
3787
+ /**
3788
+ * Set up the UI renderer for all SDK panels
3789
+ * Call this in your scene's main() function to render Guide, Chat, Admin panels.
3790
+ * No need to create your own ui.tsx - the SDK handles everything.
3791
+ *
3792
+ * @example
3793
+ * ```typescript
3794
+ * const staticTV = new StaticTVClient({ apiKey: 'dcls_...' })
3795
+ *
3796
+ * export function main() {
3797
+ * staticTV.setupUI()
3798
+ * // That's it! All panels will render automatically
3799
+ * }
3800
+ * ```
3801
+ */
3802
+ setupUI() {
3803
+ setupStaticUI(this);
3804
+ this.log("UI renderer initialized");
3805
+ }
3806
+ /**
3807
+ * Show a notification message on screen
3808
+ * Works with both SDK-rendered UI and custom UI setups.
3809
+ *
3810
+ * @param message The message to display
3811
+ * @param durationMs How long to show (default 5000ms)
3812
+ *
3813
+ * @example
3814
+ * ```typescript
3815
+ * staticTV.showNotification('Stream started!')
3816
+ * staticTV.showNotification('Custom message', 10000) // 10 seconds
3817
+ * ```
3818
+ */
3819
+ showNotification(message, durationMs = 5e3) {
3820
+ showNotification(message, durationMs);
2298
3821
  }
2299
3822
  /**
2300
3823
  * Cleanup when done (call before scene unload)
@@ -2317,6 +3840,7 @@ var StaticTVClient = class {
2317
3840
  }
2318
3841
  };
2319
3842
  export {
3843
+ AdminPanelUIModule,
2320
3844
  ChatUIModule,
2321
3845
  GuideModule,
2322
3846
  GuideUIModule,
@@ -2328,5 +3852,7 @@ export {
2328
3852
  StaticTVClient,
2329
3853
  fetchUserData,
2330
3854
  getPlayerDisplayName,
2331
- getPlayerWallet
3855
+ getPlayerWallet,
3856
+ setupStaticUI,
3857
+ showNotification
2332
3858
  };