@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/README.md +216 -66
- package/dist/index.d.mts +450 -38
- package/dist/index.d.ts +450 -38
- package/dist/index.js +1671 -142
- package/dist/index.mjs +1667 -141
- package/package.json +8 -3
package/dist/index.mjs
CHANGED
|
@@ -96,8 +96,8 @@ function getPlayerWallet() {
|
|
|
96
96
|
return cachedWallet;
|
|
97
97
|
}
|
|
98
98
|
try {
|
|
99
|
-
const { getPlayer:
|
|
100
|
-
const player =
|
|
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:
|
|
112
|
-
const player =
|
|
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("[
|
|
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("[
|
|
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.
|
|
219
|
+
this._tier = "free";
|
|
213
220
|
this.client = client;
|
|
214
221
|
}
|
|
215
222
|
/**
|
|
216
|
-
* Get the
|
|
217
|
-
|
|
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.
|
|
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.
|
|
290
|
+
this._tier = normalizeTier(response.sdkType);
|
|
269
291
|
this.startHeartbeat();
|
|
270
|
-
this.client.log(`Session started: ${this.sessionId},
|
|
271
|
-
|
|
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:
|
|
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 -
|
|
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:
|
|
635
|
+
header: 16,
|
|
597
636
|
channelButton: 14,
|
|
598
637
|
channelDropdown: 14,
|
|
599
|
-
systemMessage:
|
|
600
|
-
chatUsername:
|
|
638
|
+
systemMessage: 13,
|
|
639
|
+
chatUsername: 14,
|
|
601
640
|
chatTimestamp: 11,
|
|
602
|
-
chatMessage:
|
|
603
|
-
input:
|
|
641
|
+
chatMessage: 14,
|
|
642
|
+
input: 14,
|
|
604
643
|
sendButton: 14,
|
|
605
|
-
userInfo:
|
|
644
|
+
userInfo: 13,
|
|
606
645
|
authStatus: 12,
|
|
607
|
-
notification:
|
|
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
|
-
*
|
|
804
|
+
* Always renders toggle button, plus panel when visible
|
|
743
805
|
*/
|
|
744
806
|
this.getComponent = () => {
|
|
745
|
-
|
|
746
|
-
|
|
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:
|
|
752
|
-
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
|
-
|
|
762
|
-
this.
|
|
763
|
-
|
|
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
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
1032
|
+
uiTransform: {
|
|
1033
|
+
width: "100%",
|
|
1034
|
+
height: this.s(40),
|
|
1035
|
+
marginBottom: 10,
|
|
1036
|
+
flexDirection: "row",
|
|
1037
|
+
alignItems: "center"
|
|
961
1038
|
},
|
|
962
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
1328
|
-
fontSize: this.s(
|
|
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
|
-
*
|
|
1468
|
+
* Always renders toggle button, plus panel when visible
|
|
1381
1469
|
*/
|
|
1382
1470
|
this.getComponent = () => {
|
|
1383
|
-
|
|
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:
|
|
1391
|
-
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
|
-
|
|
1402
|
-
this.
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
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:
|
|
1821
|
+
fontScale: 1,
|
|
1716
1822
|
showPositionControls: false,
|
|
1717
|
-
showFontControls:
|
|
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
|
-
|
|
2103
|
-
|
|
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:
|
|
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:
|
|
2114
|
-
fontSize:
|
|
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
|
-
*
|
|
2137
|
-
* const staticTV = new StaticTVClient({
|
|
2138
|
-
* apiKey: 'dclk_your_channel_key_here',
|
|
2139
|
-
* debug: true
|
|
2140
|
-
* });
|
|
3463
|
+
* let staticTV: StaticTVClient
|
|
2141
3464
|
*
|
|
2142
|
-
*
|
|
2143
|
-
*
|
|
2144
|
-
*
|
|
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.
|
|
2152
|
-
|
|
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
|
|
3484
|
+
/** Session module - track visitor sessions (all tiers, null when disabled) */
|
|
2155
3485
|
this.session = null;
|
|
2156
|
-
/** Heartbeat module - track video watching (
|
|
3486
|
+
/** Heartbeat module - track video watching (standard/pro tier) */
|
|
2157
3487
|
this.heartbeat = null;
|
|
2158
|
-
/** Interactions module - like/follow channels (
|
|
3488
|
+
/** Interactions module - like/follow channels (standard/pro tier) */
|
|
2159
3489
|
this.interactions = null;
|
|
2160
|
-
/** Guide UI module - channel browser UI (
|
|
3490
|
+
/** Guide UI module - channel browser UI (standard/pro tier) */
|
|
2161
3491
|
this.guideUI = null;
|
|
2162
|
-
/** Chat UI module - real-time chat UI (
|
|
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.
|
|
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.
|
|
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
|
-
*
|
|
2226
|
-
|
|
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
|
|
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
|
|
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(`[
|
|
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
|
|
3628
|
+
* Initialize standard feature modules (guide, heartbeat, interactions, UI)
|
|
2264
3629
|
* @internal
|
|
2265
3630
|
*/
|
|
2266
|
-
|
|
2267
|
-
if (this.
|
|
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.
|
|
3638
|
+
this._standardFeaturesEnabled = true;
|
|
2274
3639
|
this.chatUI.init().catch((err) => {
|
|
2275
3640
|
this.log(`Chat init failed: ${err}`);
|
|
2276
3641
|
});
|
|
2277
|
-
this.log("
|
|
3642
|
+
this.log("Standard features enabled (guide, chat, heartbeat, interactions)");
|
|
2278
3643
|
}
|
|
2279
3644
|
/**
|
|
2280
|
-
*
|
|
2281
|
-
*
|
|
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.
|
|
3687
|
+
this._enableFeaturesForTier("standard");
|
|
2286
3688
|
}
|
|
2287
3689
|
/**
|
|
2288
|
-
* Check if
|
|
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.
|
|
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
|
-
*
|
|
3708
|
+
* @deprecated Use `tier` instead
|
|
2295
3709
|
*/
|
|
2296
3710
|
get sdkType() {
|
|
2297
|
-
return this.
|
|
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
|
};
|