unified-video-framework 1.4.437 → 1.4.439
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/package.json +1 -1
- package/packages/core/dist/version.d.ts +1 -1
- package/packages/core/dist/version.js +1 -1
- package/packages/core/src/version.ts +1 -1
- package/packages/web/dist/WebPlayer.d.ts +28 -0
- package/packages/web/dist/WebPlayer.d.ts.map +1 -1
- package/packages/web/dist/WebPlayer.js +679 -8
- package/packages/web/dist/WebPlayer.js.map +1 -1
- package/packages/web/dist/index.d.ts +1 -1
- package/packages/web/dist/index.d.ts.map +1 -1
- package/packages/web/dist/index.js.map +1 -1
- package/packages/web/dist/react/types/FlashNewsTickerTypes.d.ts +61 -0
- package/packages/web/dist/react/types/FlashNewsTickerTypes.d.ts.map +1 -1
- package/packages/web/src/WebPlayer.ts +912 -9
- package/packages/web/src/index.ts +10 -1
- package/packages/web/src/react/types/FlashNewsTickerTypes.ts +214 -1
|
@@ -20,7 +20,19 @@ import {
|
|
|
20
20
|
VideoSegment,
|
|
21
21
|
ChapterEvents
|
|
22
22
|
} from './chapters/types/ChapterTypes';
|
|
23
|
-
import type {
|
|
23
|
+
import type {
|
|
24
|
+
FlashNewsTickerConfig,
|
|
25
|
+
FlashNewsTickerItem,
|
|
26
|
+
BroadcastStyleConfig,
|
|
27
|
+
BroadcastTheme,
|
|
28
|
+
TickerStyleVariant,
|
|
29
|
+
TickerLayoutStyle,
|
|
30
|
+
IntroAnimationType,
|
|
31
|
+
TwoLineDisplayConfig,
|
|
32
|
+
DecorativeShapesConfig,
|
|
33
|
+
ItemCyclingConfig,
|
|
34
|
+
SeparatorType
|
|
35
|
+
} from './react/types/FlashNewsTickerTypes';
|
|
24
36
|
import YouTubeExtractor from './utils/YouTubeExtractor';
|
|
25
37
|
import { DRMManager, DRMErrorHandler } from './drm';
|
|
26
38
|
import type { DRMInitResult, DRMError } from './drm';
|
|
@@ -71,6 +83,19 @@ export class WebPlayer extends BasePlayer {
|
|
|
71
83
|
private flashTickerTopElement: HTMLDivElement | null = null;
|
|
72
84
|
private flashTickerBottomElement: HTMLDivElement | null = null;
|
|
73
85
|
|
|
86
|
+
// Professional Ticker State (for item cycling and intro animations)
|
|
87
|
+
private tickerCurrentItemIndex: number = 0;
|
|
88
|
+
private tickerCycleTimer: number | null = null;
|
|
89
|
+
private tickerConfig: FlashNewsTickerConfig | null = null;
|
|
90
|
+
private tickerHeadlineElement: HTMLDivElement | null = null;
|
|
91
|
+
private tickerDetailElement: HTMLDivElement | null = null;
|
|
92
|
+
private tickerIntroOverlay: HTMLDivElement | null = null;
|
|
93
|
+
private tickerProgressBar: HTMLDivElement | null = null;
|
|
94
|
+
private tickerProgressFill: HTMLDivElement | null = null;
|
|
95
|
+
private tickerIsPaused: boolean = false;
|
|
96
|
+
private tickerPauseStartTime: number = 0;
|
|
97
|
+
private tickerRemainingTime: number = 0;
|
|
98
|
+
|
|
74
99
|
// Free preview gate state
|
|
75
100
|
private previewGateHit: boolean = false;
|
|
76
101
|
private paymentSuccessTime: number = 0;
|
|
@@ -172,6 +197,8 @@ export class WebPlayer extends BasePlayer {
|
|
|
172
197
|
private currentRetryAttempt: number = 0;
|
|
173
198
|
private lastFailedUrl: string = ''; // Track last failed URL to avoid duplicate error handling
|
|
174
199
|
private isFallbackPosterMode: boolean = false; // True when showing fallback poster (no playable sources)
|
|
200
|
+
private hlsErrorRetryCount: number = 0; // Track HLS error recovery attempts
|
|
201
|
+
private readonly MAX_HLS_ERROR_RETRIES: number = 3; // Max HLS recovery attempts before fallback
|
|
175
202
|
|
|
176
203
|
// Live stream detection
|
|
177
204
|
private lastDuration: number = 0; // Track duration changes to detect live streams
|
|
@@ -727,6 +754,7 @@ export class WebPlayer extends BasePlayer {
|
|
|
727
754
|
this.currentRetryAttempt = 0;
|
|
728
755
|
this.lastFailedUrl = '';
|
|
729
756
|
this.isFallbackPosterMode = false; // Reset fallback poster mode
|
|
757
|
+
this.hlsErrorRetryCount = 0; // Reset HLS error retry count
|
|
730
758
|
|
|
731
759
|
// Clean up previous instances
|
|
732
760
|
await this.cleanup();
|
|
@@ -1122,6 +1150,9 @@ export class WebPlayer extends BasePlayer {
|
|
|
1122
1150
|
this.hls.attachMedia(this.video);
|
|
1123
1151
|
|
|
1124
1152
|
this.hls.on(window.Hls.Events.MANIFEST_PARSED, (event: any, data: any) => {
|
|
1153
|
+
// Reset HLS error retry count on successful manifest load
|
|
1154
|
+
this.hlsErrorRetryCount = 0;
|
|
1155
|
+
|
|
1125
1156
|
// Extract quality levels
|
|
1126
1157
|
this.qualities = data.levels.map((level: any, index: number) => ({
|
|
1127
1158
|
height: level.height,
|
|
@@ -1164,27 +1195,101 @@ export class WebPlayer extends BasePlayer {
|
|
|
1164
1195
|
}
|
|
1165
1196
|
}
|
|
1166
1197
|
|
|
1167
|
-
private handleHLSError(data: any): void {
|
|
1198
|
+
private async handleHLSError(data: any): Promise<void> {
|
|
1168
1199
|
const Hls = window.Hls;
|
|
1200
|
+
|
|
1201
|
+
this.debugLog(`🔴 HLS Error: type=${data.type}, details=${data.details}, fatal=${data.fatal}`);
|
|
1202
|
+
|
|
1203
|
+
// Check if we've exceeded max retry attempts
|
|
1204
|
+
if (this.hlsErrorRetryCount >= this.MAX_HLS_ERROR_RETRIES) {
|
|
1205
|
+
this.debugLog(`🔴 HLS max retries (${this.MAX_HLS_ERROR_RETRIES}) exceeded, triggering fallback`);
|
|
1206
|
+
this.hls?.destroy();
|
|
1207
|
+
this.hls = null;
|
|
1208
|
+
|
|
1209
|
+
// Try fallback sources
|
|
1210
|
+
const fallbackLoaded = await this.tryFallbackSource({
|
|
1211
|
+
code: 'HLS_ERROR',
|
|
1212
|
+
message: data.details,
|
|
1213
|
+
type: data.type,
|
|
1214
|
+
fatal: true,
|
|
1215
|
+
details: data
|
|
1216
|
+
});
|
|
1217
|
+
|
|
1218
|
+
if (!fallbackLoaded) {
|
|
1219
|
+
this.handleError({
|
|
1220
|
+
code: 'HLS_ERROR',
|
|
1221
|
+
message: `HLS stream failed after ${this.MAX_HLS_ERROR_RETRIES} retries: ${data.details}`,
|
|
1222
|
+
type: 'media',
|
|
1223
|
+
fatal: true,
|
|
1224
|
+
details: data
|
|
1225
|
+
});
|
|
1226
|
+
}
|
|
1227
|
+
return;
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1169
1230
|
switch (data.type) {
|
|
1170
1231
|
case Hls.ErrorTypes.NETWORK_ERROR:
|
|
1171
|
-
|
|
1172
|
-
this.
|
|
1232
|
+
this.hlsErrorRetryCount++;
|
|
1233
|
+
this.debugLog(`🔴 Fatal network error (attempt ${this.hlsErrorRetryCount}/${this.MAX_HLS_ERROR_RETRIES}), trying to recover`);
|
|
1234
|
+
|
|
1235
|
+
// For manifest errors (like 404), recovery won't help - go straight to fallback
|
|
1236
|
+
if (data.details === 'manifestLoadError' || data.details === 'manifestParsingError') {
|
|
1237
|
+
this.debugLog(`🔴 Manifest error detected (${data.details}), skipping recovery - triggering fallback`);
|
|
1238
|
+
this.hls?.destroy();
|
|
1239
|
+
this.hls = null;
|
|
1240
|
+
|
|
1241
|
+
const fallbackLoaded = await this.tryFallbackSource({
|
|
1242
|
+
code: 'HLS_MANIFEST_ERROR',
|
|
1243
|
+
message: data.details,
|
|
1244
|
+
type: 'network',
|
|
1245
|
+
fatal: true,
|
|
1246
|
+
details: data
|
|
1247
|
+
});
|
|
1248
|
+
|
|
1249
|
+
if (!fallbackLoaded) {
|
|
1250
|
+
this.handleError({
|
|
1251
|
+
code: 'HLS_MANIFEST_ERROR',
|
|
1252
|
+
message: `Failed to load HLS manifest: ${data.details}`,
|
|
1253
|
+
type: 'media',
|
|
1254
|
+
fatal: true,
|
|
1255
|
+
details: data
|
|
1256
|
+
});
|
|
1257
|
+
}
|
|
1258
|
+
} else {
|
|
1259
|
+
// For other network errors, try recovery
|
|
1260
|
+
this.hls?.startLoad();
|
|
1261
|
+
}
|
|
1173
1262
|
break;
|
|
1263
|
+
|
|
1174
1264
|
case Hls.ErrorTypes.MEDIA_ERROR:
|
|
1175
|
-
|
|
1176
|
-
this.
|
|
1265
|
+
this.hlsErrorRetryCount++;
|
|
1266
|
+
this.debugLog(`🔴 Fatal media error (attempt ${this.hlsErrorRetryCount}/${this.MAX_HLS_ERROR_RETRIES}), trying to recover`);
|
|
1267
|
+
this.hls?.recoverMediaError();
|
|
1177
1268
|
break;
|
|
1269
|
+
|
|
1178
1270
|
default:
|
|
1179
|
-
|
|
1180
|
-
this.
|
|
1271
|
+
this.debugLog(`🔴 Fatal unrecoverable HLS error: ${data.details}`);
|
|
1272
|
+
this.hls?.destroy();
|
|
1273
|
+
this.hls = null;
|
|
1274
|
+
|
|
1275
|
+
// Try fallback sources for unrecoverable errors
|
|
1276
|
+
const fallbackLoaded = await this.tryFallbackSource({
|
|
1181
1277
|
code: 'HLS_ERROR',
|
|
1182
1278
|
message: data.details,
|
|
1183
1279
|
type: 'media',
|
|
1184
1280
|
fatal: true,
|
|
1185
1281
|
details: data
|
|
1186
1282
|
});
|
|
1187
|
-
|
|
1283
|
+
|
|
1284
|
+
if (!fallbackLoaded) {
|
|
1285
|
+
this.handleError({
|
|
1286
|
+
code: 'HLS_ERROR',
|
|
1287
|
+
message: data.details,
|
|
1288
|
+
type: 'media',
|
|
1289
|
+
fatal: true,
|
|
1290
|
+
details: data
|
|
1291
|
+
});
|
|
1292
|
+
}
|
|
1188
1293
|
break;
|
|
1189
1294
|
}
|
|
1190
1295
|
}
|
|
@@ -2470,6 +2575,11 @@ export class WebPlayer extends BasePlayer {
|
|
|
2470
2575
|
private createTickerElement(config: FlashNewsTickerConfig, position: 'top' | 'bottom'): HTMLDivElement {
|
|
2471
2576
|
// Route to broadcast style if configured
|
|
2472
2577
|
if (config.styleVariant === 'broadcast') {
|
|
2578
|
+
const layoutStyle = config.broadcastStyle?.layoutStyle || 'broadcast';
|
|
2579
|
+
// Route to professional ticker for two-line or professional layouts
|
|
2580
|
+
if (layoutStyle === 'two-line' || layoutStyle === 'professional') {
|
|
2581
|
+
return this.createProfessionalTickerElement(config, position);
|
|
2582
|
+
}
|
|
2473
2583
|
return this.createBroadcastTickerElement(config, position);
|
|
2474
2584
|
}
|
|
2475
2585
|
|
|
@@ -2837,14 +2947,19 @@ export class WebPlayer extends BasePlayer {
|
|
|
2837
2947
|
const style = document.createElement('style');
|
|
2838
2948
|
style.id = 'uvf-ticker-animation';
|
|
2839
2949
|
style.textContent = `
|
|
2950
|
+
/* Basic ticker scroll */
|
|
2840
2951
|
@keyframes ticker-scroll {
|
|
2841
2952
|
0% { transform: translateX(0%); }
|
|
2842
2953
|
100% { transform: translateX(-100%); }
|
|
2843
2954
|
}
|
|
2955
|
+
|
|
2956
|
+
/* Globe rotation */
|
|
2844
2957
|
@keyframes globe-rotate {
|
|
2845
2958
|
0% { transform: rotate(0deg); }
|
|
2846
2959
|
100% { transform: rotate(360deg); }
|
|
2847
2960
|
}
|
|
2961
|
+
|
|
2962
|
+
/* LIVE badge animations */
|
|
2848
2963
|
@keyframes live-pulse {
|
|
2849
2964
|
0%, 100% { opacity: 1; transform: scale(1); }
|
|
2850
2965
|
50% { opacity: 0.85; transform: scale(1.02); }
|
|
@@ -2853,6 +2968,85 @@ export class WebPlayer extends BasePlayer {
|
|
|
2853
2968
|
0%, 100% { opacity: 1; }
|
|
2854
2969
|
50% { opacity: 0.3; }
|
|
2855
2970
|
}
|
|
2971
|
+
|
|
2972
|
+
/* Intro Animations */
|
|
2973
|
+
@keyframes intro-slide-in {
|
|
2974
|
+
0% { transform: translateX(-100%); opacity: 0; }
|
|
2975
|
+
20% { transform: translateX(0); opacity: 1; }
|
|
2976
|
+
80% { transform: translateX(0); opacity: 1; }
|
|
2977
|
+
100% { transform: translateX(100%); opacity: 0; }
|
|
2978
|
+
}
|
|
2979
|
+
|
|
2980
|
+
@keyframes intro-flash {
|
|
2981
|
+
0%, 100% { opacity: 0; }
|
|
2982
|
+
10%, 30%, 50%, 70%, 90% { opacity: 1; background: rgba(255,255,255,0.3); }
|
|
2983
|
+
20%, 40%, 60%, 80% { opacity: 1; background: transparent; }
|
|
2984
|
+
}
|
|
2985
|
+
|
|
2986
|
+
@keyframes intro-scale {
|
|
2987
|
+
0% { transform: scale(0); opacity: 0; }
|
|
2988
|
+
20% { transform: scale(1.2); opacity: 1; }
|
|
2989
|
+
30% { transform: scale(1); }
|
|
2990
|
+
80% { transform: scale(1); opacity: 1; }
|
|
2991
|
+
100% { transform: scale(0); opacity: 0; }
|
|
2992
|
+
}
|
|
2993
|
+
|
|
2994
|
+
@keyframes intro-pulse {
|
|
2995
|
+
0% { transform: scale(0.8); opacity: 0; }
|
|
2996
|
+
25% { transform: scale(1.1); opacity: 1; }
|
|
2997
|
+
50% { transform: scale(1); opacity: 1; }
|
|
2998
|
+
75% { transform: scale(1); opacity: 1; }
|
|
2999
|
+
100% { transform: scale(0.8); opacity: 0; }
|
|
3000
|
+
}
|
|
3001
|
+
|
|
3002
|
+
@keyframes intro-shake {
|
|
3003
|
+
0% { transform: translateX(0); opacity: 0; }
|
|
3004
|
+
10% { opacity: 1; }
|
|
3005
|
+
15%, 35%, 55%, 75% { transform: translateX(-5px); }
|
|
3006
|
+
25%, 45%, 65%, 85% { transform: translateX(5px); }
|
|
3007
|
+
90% { transform: translateX(0); opacity: 1; }
|
|
3008
|
+
100% { transform: translateX(0); opacity: 0; }
|
|
3009
|
+
}
|
|
3010
|
+
|
|
3011
|
+
@keyframes intro-none {
|
|
3012
|
+
0% { opacity: 1; }
|
|
3013
|
+
80% { opacity: 1; }
|
|
3014
|
+
100% { opacity: 0; }
|
|
3015
|
+
}
|
|
3016
|
+
|
|
3017
|
+
/* Item Transitions */
|
|
3018
|
+
@keyframes item-fade-out {
|
|
3019
|
+
from { opacity: 1; }
|
|
3020
|
+
to { opacity: 0; }
|
|
3021
|
+
}
|
|
3022
|
+
@keyframes item-fade-in {
|
|
3023
|
+
from { opacity: 0; }
|
|
3024
|
+
to { opacity: 1; }
|
|
3025
|
+
}
|
|
3026
|
+
@keyframes item-slide-out {
|
|
3027
|
+
from { transform: translateY(0); opacity: 1; }
|
|
3028
|
+
to { transform: translateY(-100%); opacity: 0; }
|
|
3029
|
+
}
|
|
3030
|
+
@keyframes item-slide-in {
|
|
3031
|
+
from { transform: translateY(100%); opacity: 0; }
|
|
3032
|
+
to { transform: translateY(0); opacity: 1; }
|
|
3033
|
+
}
|
|
3034
|
+
|
|
3035
|
+
/* Decorative Separator Animations */
|
|
3036
|
+
@keyframes separator-pulse {
|
|
3037
|
+
0%, 100% { opacity: 0.8; transform: scaleY(1); }
|
|
3038
|
+
50% { opacity: 1; transform: scaleY(1.1); }
|
|
3039
|
+
}
|
|
3040
|
+
|
|
3041
|
+
@keyframes chevron-pulse {
|
|
3042
|
+
0%, 100% { transform: translateX(0); opacity: 0.8; }
|
|
3043
|
+
50% { transform: translateX(3px); opacity: 1; }
|
|
3044
|
+
}
|
|
3045
|
+
|
|
3046
|
+
@keyframes diamond-spin {
|
|
3047
|
+
from { transform: rotate(0deg); }
|
|
3048
|
+
to { transform: rotate(360deg); }
|
|
3049
|
+
}
|
|
2856
3050
|
`;
|
|
2857
3051
|
document.head.appendChild(style);
|
|
2858
3052
|
}
|
|
@@ -2871,6 +3065,715 @@ export class WebPlayer extends BasePlayer {
|
|
|
2871
3065
|
return Math.max(totalWidth / speed, 15); // Minimum 15s for smooth scrolling
|
|
2872
3066
|
}
|
|
2873
3067
|
|
|
3068
|
+
/**
|
|
3069
|
+
* Create professional TV-style ticker with two-line display and item cycling
|
|
3070
|
+
*/
|
|
3071
|
+
private createProfessionalTickerElement(config: FlashNewsTickerConfig, position: 'top' | 'bottom'): HTMLDivElement {
|
|
3072
|
+
const broadcastStyle = config.broadcastStyle || {};
|
|
3073
|
+
const theme = broadcastStyle.theme || 'breaking-red';
|
|
3074
|
+
const twoLineConfig = broadcastStyle.twoLineDisplay || {};
|
|
3075
|
+
const itemCycling = config.itemCycling || {};
|
|
3076
|
+
|
|
3077
|
+
// Store config for item cycling
|
|
3078
|
+
this.tickerConfig = config;
|
|
3079
|
+
this.tickerCurrentItemIndex = 0;
|
|
3080
|
+
|
|
3081
|
+
// Get theme colors
|
|
3082
|
+
const themeColors = this.getBroadcastThemeColors(theme, broadcastStyle);
|
|
3083
|
+
|
|
3084
|
+
const ticker = document.createElement('div');
|
|
3085
|
+
ticker.className = `uvf-flash-ticker ticker-${position} ticker-professional`;
|
|
3086
|
+
|
|
3087
|
+
const bottomOffset = config.bottomOffset || 0;
|
|
3088
|
+
const topOffset = config.topOffset || 0;
|
|
3089
|
+
const headerHeight = broadcastStyle.headerHeight || 28;
|
|
3090
|
+
|
|
3091
|
+
// Calculate body height based on two-line config
|
|
3092
|
+
const topLineConfig = twoLineConfig.topLine || {};
|
|
3093
|
+
const bottomLineConfig = twoLineConfig.bottomLine || {};
|
|
3094
|
+
const topLineHeight = topLineConfig.minHeight || 32;
|
|
3095
|
+
const bottomLineHeight = bottomLineConfig.height || 28;
|
|
3096
|
+
const bodyHeight = twoLineConfig.enabled !== false ? topLineHeight + bottomLineHeight : (config.height || 36);
|
|
3097
|
+
const progressHeight = itemCycling.showProgress ? (itemCycling.progressHeight || 3) : 0;
|
|
3098
|
+
const totalHeight = headerHeight + bodyHeight + progressHeight;
|
|
3099
|
+
|
|
3100
|
+
ticker.style.cssText = `
|
|
3101
|
+
position: absolute;
|
|
3102
|
+
left: 0;
|
|
3103
|
+
right: 0;
|
|
3104
|
+
height: ${totalHeight}px;
|
|
3105
|
+
${position === 'top' ? `top: ${topOffset}px;` : `bottom: ${bottomOffset}px;`}
|
|
3106
|
+
overflow: hidden;
|
|
3107
|
+
pointer-events: ${itemCycling.pauseOnHover !== false ? 'auto' : 'none'};
|
|
3108
|
+
display: flex;
|
|
3109
|
+
flex-direction: column;
|
|
3110
|
+
`;
|
|
3111
|
+
|
|
3112
|
+
// Create header row
|
|
3113
|
+
const header = this.createProfessionalHeader(broadcastStyle, themeColors, headerHeight);
|
|
3114
|
+
ticker.appendChild(header);
|
|
3115
|
+
|
|
3116
|
+
// Create body with two-line display
|
|
3117
|
+
const body = this.createTwoLineBody(config, twoLineConfig, themeColors.bodyBg, bodyHeight);
|
|
3118
|
+
ticker.appendChild(body);
|
|
3119
|
+
|
|
3120
|
+
// Create progress bar if enabled
|
|
3121
|
+
if (itemCycling.showProgress) {
|
|
3122
|
+
const progressBar = this.createProgressBar(itemCycling);
|
|
3123
|
+
ticker.appendChild(progressBar);
|
|
3124
|
+
}
|
|
3125
|
+
|
|
3126
|
+
// Create intro overlay (hidden by default)
|
|
3127
|
+
const introOverlay = this.createIntroOverlay(broadcastStyle);
|
|
3128
|
+
ticker.appendChild(introOverlay);
|
|
3129
|
+
this.tickerIntroOverlay = introOverlay;
|
|
3130
|
+
|
|
3131
|
+
// Add hover pause functionality
|
|
3132
|
+
if (itemCycling.pauseOnHover !== false && itemCycling.enabled) {
|
|
3133
|
+
ticker.addEventListener('mouseenter', () => this.pauseItemCycling());
|
|
3134
|
+
ticker.addEventListener('mouseleave', () => this.resumeItemCycling());
|
|
3135
|
+
}
|
|
3136
|
+
|
|
3137
|
+
// Add all animation keyframes
|
|
3138
|
+
this.ensureTickerAnimations();
|
|
3139
|
+
|
|
3140
|
+
// Start item cycling if enabled
|
|
3141
|
+
if (itemCycling.enabled && config.items && config.items.length > 1) {
|
|
3142
|
+
// Initial display with intro animation if configured
|
|
3143
|
+
const firstItem = config.items[0];
|
|
3144
|
+
if (firstItem.showIntro) {
|
|
3145
|
+
this.playIntroAnimation(firstItem).then(() => {
|
|
3146
|
+
this.startItemCycling();
|
|
3147
|
+
});
|
|
3148
|
+
} else {
|
|
3149
|
+
this.startItemCycling();
|
|
3150
|
+
}
|
|
3151
|
+
}
|
|
3152
|
+
|
|
3153
|
+
return ticker;
|
|
3154
|
+
}
|
|
3155
|
+
|
|
3156
|
+
/**
|
|
3157
|
+
* Create professional header row with globe, separator, text, and LIVE badge
|
|
3158
|
+
*/
|
|
3159
|
+
private createProfessionalHeader(
|
|
3160
|
+
broadcastStyle: BroadcastStyleConfig,
|
|
3161
|
+
themeColors: { headerBg: string; bodyBg: string },
|
|
3162
|
+
headerHeight: number
|
|
3163
|
+
): HTMLDivElement {
|
|
3164
|
+
const header = document.createElement('div');
|
|
3165
|
+
header.className = 'uvf-ticker-header-row';
|
|
3166
|
+
header.style.cssText = `
|
|
3167
|
+
display: flex;
|
|
3168
|
+
align-items: center;
|
|
3169
|
+
height: ${headerHeight}px;
|
|
3170
|
+
background: ${themeColors.headerBg};
|
|
3171
|
+
padding: 0 12px;
|
|
3172
|
+
position: relative;
|
|
3173
|
+
`;
|
|
3174
|
+
|
|
3175
|
+
// Add globe graphic if enabled
|
|
3176
|
+
if (broadcastStyle.showGlobe !== false) {
|
|
3177
|
+
const globe = this.createGlobeElement(broadcastStyle.animateGlobe !== false);
|
|
3178
|
+
header.appendChild(globe);
|
|
3179
|
+
}
|
|
3180
|
+
|
|
3181
|
+
// Add decorative separator between globe and text
|
|
3182
|
+
if (broadcastStyle.decorativeShapes?.headerSeparator) {
|
|
3183
|
+
const separator = this.createDecorativeSeparator(broadcastStyle.decorativeShapes.headerSeparator);
|
|
3184
|
+
header.appendChild(separator);
|
|
3185
|
+
} else if (broadcastStyle.showGlobe !== false) {
|
|
3186
|
+
// Default simple separator
|
|
3187
|
+
const defaultSeparator = document.createElement('div');
|
|
3188
|
+
defaultSeparator.className = 'uvf-ticker-separator';
|
|
3189
|
+
defaultSeparator.style.cssText = `
|
|
3190
|
+
width: 2px;
|
|
3191
|
+
height: 18px;
|
|
3192
|
+
background: rgba(255,255,255,0.6);
|
|
3193
|
+
margin: 0 10px;
|
|
3194
|
+
`;
|
|
3195
|
+
header.appendChild(defaultSeparator);
|
|
3196
|
+
}
|
|
3197
|
+
|
|
3198
|
+
// Add header text (BREAKING NEWS)
|
|
3199
|
+
const headerText = document.createElement('span');
|
|
3200
|
+
headerText.className = 'uvf-ticker-header-text';
|
|
3201
|
+
headerText.textContent = broadcastStyle.headerText || 'BREAKING NEWS';
|
|
3202
|
+
headerText.style.cssText = `
|
|
3203
|
+
color: ${broadcastStyle.headerTextColor || '#ffffff'};
|
|
3204
|
+
font-size: ${broadcastStyle.headerFontSize || 16}px;
|
|
3205
|
+
font-weight: 800;
|
|
3206
|
+
text-transform: uppercase;
|
|
3207
|
+
letter-spacing: 1px;
|
|
3208
|
+
text-shadow: 1px 1px 2px rgba(0,0,0,0.3);
|
|
3209
|
+
`;
|
|
3210
|
+
header.appendChild(headerText);
|
|
3211
|
+
|
|
3212
|
+
// Add LIVE badge if enabled
|
|
3213
|
+
if (broadcastStyle.showLiveBadge !== false) {
|
|
3214
|
+
const liveBadge = this.createLiveBadgeElement(broadcastStyle.pulseLiveBadge !== false);
|
|
3215
|
+
header.appendChild(liveBadge);
|
|
3216
|
+
}
|
|
3217
|
+
|
|
3218
|
+
return header;
|
|
3219
|
+
}
|
|
3220
|
+
|
|
3221
|
+
/**
|
|
3222
|
+
* Create decorative separator element
|
|
3223
|
+
*/
|
|
3224
|
+
private createDecorativeSeparator(config: NonNullable<DecorativeShapesConfig['headerSeparator']>): HTMLDivElement {
|
|
3225
|
+
const separator = document.createElement('div');
|
|
3226
|
+
separator.className = 'uvf-ticker-separator';
|
|
3227
|
+
|
|
3228
|
+
const width = config.width || 2;
|
|
3229
|
+
const height = config.height || 20;
|
|
3230
|
+
const color = config.color || '#ffffff';
|
|
3231
|
+
const animated = config.animated !== false;
|
|
3232
|
+
const type = config.type || 'line';
|
|
3233
|
+
|
|
3234
|
+
let animationStyle = '';
|
|
3235
|
+
let content = '';
|
|
3236
|
+
|
|
3237
|
+
switch (type) {
|
|
3238
|
+
case 'pulse-line':
|
|
3239
|
+
animationStyle = animated ? 'animation: separator-pulse 1.5s ease-in-out infinite;' : '';
|
|
3240
|
+
break;
|
|
3241
|
+
case 'chevron':
|
|
3242
|
+
content = '›';
|
|
3243
|
+
animationStyle = animated ? 'animation: chevron-pulse 1s ease-in-out infinite;' : '';
|
|
3244
|
+
break;
|
|
3245
|
+
case 'diamond':
|
|
3246
|
+
content = '◆';
|
|
3247
|
+
animationStyle = animated ? 'animation: diamond-spin 3s linear infinite;' : '';
|
|
3248
|
+
break;
|
|
3249
|
+
case 'dot':
|
|
3250
|
+
content = '•';
|
|
3251
|
+
animationStyle = animated ? 'animation: dot-blink 1s ease-in-out infinite;' : '';
|
|
3252
|
+
break;
|
|
3253
|
+
default:
|
|
3254
|
+
// Simple line
|
|
3255
|
+
break;
|
|
3256
|
+
}
|
|
3257
|
+
|
|
3258
|
+
if (type === 'line' || type === 'pulse-line') {
|
|
3259
|
+
separator.style.cssText = `
|
|
3260
|
+
width: ${width}px;
|
|
3261
|
+
height: ${height}px;
|
|
3262
|
+
background: ${color};
|
|
3263
|
+
margin: 0 10px;
|
|
3264
|
+
opacity: 0.8;
|
|
3265
|
+
${animationStyle}
|
|
3266
|
+
`;
|
|
3267
|
+
} else {
|
|
3268
|
+
separator.textContent = content;
|
|
3269
|
+
separator.style.cssText = `
|
|
3270
|
+
color: ${color};
|
|
3271
|
+
font-size: ${height}px;
|
|
3272
|
+
margin: 0 10px;
|
|
3273
|
+
opacity: 0.8;
|
|
3274
|
+
display: flex;
|
|
3275
|
+
align-items: center;
|
|
3276
|
+
justify-content: center;
|
|
3277
|
+
${animationStyle}
|
|
3278
|
+
`;
|
|
3279
|
+
}
|
|
3280
|
+
|
|
3281
|
+
return separator;
|
|
3282
|
+
}
|
|
3283
|
+
|
|
3284
|
+
/**
|
|
3285
|
+
* Create two-line body with static headline and scrolling detail
|
|
3286
|
+
*/
|
|
3287
|
+
private createTwoLineBody(
|
|
3288
|
+
config: FlashNewsTickerConfig,
|
|
3289
|
+
twoLineConfig: TwoLineDisplayConfig,
|
|
3290
|
+
bodyBg: string,
|
|
3291
|
+
bodyHeight: number
|
|
3292
|
+
): HTMLDivElement {
|
|
3293
|
+
const body = document.createElement('div');
|
|
3294
|
+
body.className = 'uvf-ticker-body-row';
|
|
3295
|
+
body.style.cssText = `
|
|
3296
|
+
display: flex;
|
|
3297
|
+
flex-direction: column;
|
|
3298
|
+
height: ${bodyHeight}px;
|
|
3299
|
+
background: ${bodyBg};
|
|
3300
|
+
overflow: hidden;
|
|
3301
|
+
position: relative;
|
|
3302
|
+
`;
|
|
3303
|
+
|
|
3304
|
+
const topLineConfig = twoLineConfig.topLine || {};
|
|
3305
|
+
const bottomLineConfig = twoLineConfig.bottomLine || {};
|
|
3306
|
+
|
|
3307
|
+
// Get first item for initial display
|
|
3308
|
+
const firstItem = config.items?.[0];
|
|
3309
|
+
|
|
3310
|
+
// Create headline line (top - static)
|
|
3311
|
+
const headlineLine = document.createElement('div');
|
|
3312
|
+
headlineLine.className = 'uvf-ticker-headline-line';
|
|
3313
|
+
this.tickerHeadlineElement = headlineLine;
|
|
3314
|
+
|
|
3315
|
+
const topLineHeight = topLineConfig.minHeight || 32;
|
|
3316
|
+
const topLineFontSize = topLineConfig.fontSize || 16;
|
|
3317
|
+
const topLineLineHeight = topLineConfig.lineHeight || 1.3;
|
|
3318
|
+
const topLineMultiLine = topLineConfig.multiLine !== false;
|
|
3319
|
+
const topLineMaxLines = topLineConfig.maxLines || 2;
|
|
3320
|
+
|
|
3321
|
+
headlineLine.style.cssText = `
|
|
3322
|
+
display: flex;
|
|
3323
|
+
align-items: center;
|
|
3324
|
+
min-height: ${topLineHeight}px;
|
|
3325
|
+
padding: ${topLineConfig.padding || 8}px 12px;
|
|
3326
|
+
background: ${topLineConfig.backgroundColor || 'transparent'};
|
|
3327
|
+
overflow: hidden;
|
|
3328
|
+
${topLineMultiLine ? '' : 'white-space: nowrap;'}
|
|
3329
|
+
`;
|
|
3330
|
+
|
|
3331
|
+
const headlineText = document.createElement('span');
|
|
3332
|
+
headlineText.className = 'uvf-ticker-headline-text';
|
|
3333
|
+
|
|
3334
|
+
// Support multi-line text with CSS
|
|
3335
|
+
headlineText.style.cssText = `
|
|
3336
|
+
color: ${topLineConfig.textColor || config.textColor || '#ffffff'};
|
|
3337
|
+
font-size: ${topLineFontSize}px;
|
|
3338
|
+
font-weight: 700;
|
|
3339
|
+
line-height: ${topLineLineHeight};
|
|
3340
|
+
${topLineMultiLine ? `
|
|
3341
|
+
display: -webkit-box;
|
|
3342
|
+
-webkit-line-clamp: ${topLineMaxLines};
|
|
3343
|
+
-webkit-box-orient: vertical;
|
|
3344
|
+
overflow: hidden;
|
|
3345
|
+
text-overflow: ellipsis;
|
|
3346
|
+
white-space: normal;
|
|
3347
|
+
word-wrap: break-word;
|
|
3348
|
+
` : `
|
|
3349
|
+
white-space: nowrap;
|
|
3350
|
+
overflow: hidden;
|
|
3351
|
+
text-overflow: ellipsis;
|
|
3352
|
+
`}
|
|
3353
|
+
`;
|
|
3354
|
+
|
|
3355
|
+
// Set initial headline content
|
|
3356
|
+
if (firstItem) {
|
|
3357
|
+
if (firstItem.headlineHtml) {
|
|
3358
|
+
headlineText.innerHTML = firstItem.headlineHtml;
|
|
3359
|
+
} else if (firstItem.headline) {
|
|
3360
|
+
headlineText.textContent = firstItem.headline;
|
|
3361
|
+
} else {
|
|
3362
|
+
// Fallback to text if no headline
|
|
3363
|
+
headlineText.textContent = firstItem.text;
|
|
3364
|
+
}
|
|
3365
|
+
}
|
|
3366
|
+
|
|
3367
|
+
headlineLine.appendChild(headlineText);
|
|
3368
|
+
body.appendChild(headlineLine);
|
|
3369
|
+
|
|
3370
|
+
// Add separator line between headline and detail
|
|
3371
|
+
if (twoLineConfig.showSeparator !== false) {
|
|
3372
|
+
const separatorLine = document.createElement('div');
|
|
3373
|
+
separatorLine.style.cssText = `
|
|
3374
|
+
height: 1px;
|
|
3375
|
+
background: ${twoLineConfig.separatorColor || 'rgba(255,255,255,0.2)'};
|
|
3376
|
+
margin: 0 12px;
|
|
3377
|
+
`;
|
|
3378
|
+
body.appendChild(separatorLine);
|
|
3379
|
+
}
|
|
3380
|
+
|
|
3381
|
+
// Create detail line (bottom - scrolling)
|
|
3382
|
+
const detailLine = document.createElement('div');
|
|
3383
|
+
detailLine.className = 'uvf-ticker-detail-line';
|
|
3384
|
+
this.tickerDetailElement = detailLine;
|
|
3385
|
+
|
|
3386
|
+
const bottomLineHeight = bottomLineConfig.height || 28;
|
|
3387
|
+
const bottomLineFontSize = bottomLineConfig.fontSize || 14;
|
|
3388
|
+
const bottomLineSpeed = bottomLineConfig.speed || 80;
|
|
3389
|
+
|
|
3390
|
+
detailLine.style.cssText = `
|
|
3391
|
+
display: flex;
|
|
3392
|
+
align-items: center;
|
|
3393
|
+
height: ${bottomLineHeight}px;
|
|
3394
|
+
background: ${bottomLineConfig.backgroundColor || 'transparent'};
|
|
3395
|
+
overflow: hidden;
|
|
3396
|
+
position: relative;
|
|
3397
|
+
`;
|
|
3398
|
+
|
|
3399
|
+
// Create scrolling track for detail
|
|
3400
|
+
const track = document.createElement('div');
|
|
3401
|
+
track.className = 'uvf-ticker-track';
|
|
3402
|
+
|
|
3403
|
+
// Calculate scroll duration based on content
|
|
3404
|
+
const detailText = firstItem?.text || '';
|
|
3405
|
+
const textWidth = detailText.length * 10; // Approximate
|
|
3406
|
+
const duration = Math.max(textWidth / bottomLineSpeed, 10);
|
|
3407
|
+
|
|
3408
|
+
track.style.cssText = `
|
|
3409
|
+
display: flex;
|
|
3410
|
+
white-space: nowrap;
|
|
3411
|
+
animation: ticker-scroll ${duration}s linear infinite;
|
|
3412
|
+
will-change: transform;
|
|
3413
|
+
padding-left: 100%;
|
|
3414
|
+
`;
|
|
3415
|
+
|
|
3416
|
+
// Render detail text multiple times for seamless loop
|
|
3417
|
+
const renderDetail = (text: string, html?: string) => {
|
|
3418
|
+
for (let i = 0; i < 10; i++) {
|
|
3419
|
+
const span = document.createElement('span');
|
|
3420
|
+
if (html) {
|
|
3421
|
+
span.innerHTML = html;
|
|
3422
|
+
} else {
|
|
3423
|
+
span.textContent = text;
|
|
3424
|
+
}
|
|
3425
|
+
span.style.cssText = `
|
|
3426
|
+
color: ${bottomLineConfig.textColor || config.textColor || '#ffffff'};
|
|
3427
|
+
font-size: ${bottomLineFontSize}px;
|
|
3428
|
+
font-weight: 500;
|
|
3429
|
+
margin-right: 100px;
|
|
3430
|
+
display: inline-flex;
|
|
3431
|
+
align-items: center;
|
|
3432
|
+
`;
|
|
3433
|
+
track.appendChild(span);
|
|
3434
|
+
}
|
|
3435
|
+
};
|
|
3436
|
+
|
|
3437
|
+
if (firstItem) {
|
|
3438
|
+
renderDetail(firstItem.text, firstItem.html);
|
|
3439
|
+
}
|
|
3440
|
+
|
|
3441
|
+
detailLine.appendChild(track);
|
|
3442
|
+
body.appendChild(detailLine);
|
|
3443
|
+
|
|
3444
|
+
return body;
|
|
3445
|
+
}
|
|
3446
|
+
|
|
3447
|
+
/**
|
|
3448
|
+
* Create progress bar for item cycling
|
|
3449
|
+
*/
|
|
3450
|
+
private createProgressBar(itemCycling: ItemCyclingConfig): HTMLDivElement {
|
|
3451
|
+
const progressBar = document.createElement('div');
|
|
3452
|
+
progressBar.className = 'uvf-ticker-progress';
|
|
3453
|
+
this.tickerProgressBar = progressBar;
|
|
3454
|
+
|
|
3455
|
+
const height = itemCycling.progressHeight || 3;
|
|
3456
|
+
progressBar.style.cssText = `
|
|
3457
|
+
height: ${height}px;
|
|
3458
|
+
background: rgba(0,0,0,0.3);
|
|
3459
|
+
position: relative;
|
|
3460
|
+
overflow: hidden;
|
|
3461
|
+
`;
|
|
3462
|
+
|
|
3463
|
+
const progressFill = document.createElement('div');
|
|
3464
|
+
progressFill.className = 'uvf-progress-fill';
|
|
3465
|
+
this.tickerProgressFill = progressFill;
|
|
3466
|
+
|
|
3467
|
+
progressFill.style.cssText = `
|
|
3468
|
+
height: 100%;
|
|
3469
|
+
width: 0%;
|
|
3470
|
+
background: ${itemCycling.progressColor || '#ffffff'};
|
|
3471
|
+
transition: width 0.1s linear;
|
|
3472
|
+
`;
|
|
3473
|
+
|
|
3474
|
+
progressBar.appendChild(progressFill);
|
|
3475
|
+
return progressBar;
|
|
3476
|
+
}
|
|
3477
|
+
|
|
3478
|
+
/**
|
|
3479
|
+
* Create intro overlay for "JUST IN" / "BREAKING" animations
|
|
3480
|
+
*/
|
|
3481
|
+
private createIntroOverlay(broadcastStyle: BroadcastStyleConfig): HTMLDivElement {
|
|
3482
|
+
const overlay = document.createElement('div');
|
|
3483
|
+
overlay.className = 'uvf-ticker-intro-overlay';
|
|
3484
|
+
overlay.style.cssText = `
|
|
3485
|
+
position: absolute;
|
|
3486
|
+
top: 0;
|
|
3487
|
+
left: 0;
|
|
3488
|
+
right: 0;
|
|
3489
|
+
bottom: 0;
|
|
3490
|
+
display: none;
|
|
3491
|
+
align-items: center;
|
|
3492
|
+
justify-content: center;
|
|
3493
|
+
background: rgba(200, 0, 0, 0.95);
|
|
3494
|
+
z-index: 10;
|
|
3495
|
+
`;
|
|
3496
|
+
|
|
3497
|
+
const introText = document.createElement('span');
|
|
3498
|
+
introText.className = 'uvf-intro-text';
|
|
3499
|
+
introText.style.cssText = `
|
|
3500
|
+
color: #ffffff;
|
|
3501
|
+
font-size: 24px;
|
|
3502
|
+
font-weight: 900;
|
|
3503
|
+
text-transform: uppercase;
|
|
3504
|
+
letter-spacing: 3px;
|
|
3505
|
+
text-shadow: 2px 2px 4px rgba(0,0,0,0.5);
|
|
3506
|
+
`;
|
|
3507
|
+
introText.textContent = 'BREAKING';
|
|
3508
|
+
|
|
3509
|
+
overlay.appendChild(introText);
|
|
3510
|
+
return overlay;
|
|
3511
|
+
}
|
|
3512
|
+
|
|
3513
|
+
/**
|
|
3514
|
+
* Play intro animation for a news item
|
|
3515
|
+
*/
|
|
3516
|
+
private playIntroAnimation(item: FlashNewsTickerItem): Promise<void> {
|
|
3517
|
+
return new Promise((resolve) => {
|
|
3518
|
+
if (!this.tickerIntroOverlay || !item.showIntro) {
|
|
3519
|
+
resolve();
|
|
3520
|
+
return;
|
|
3521
|
+
}
|
|
3522
|
+
|
|
3523
|
+
const broadcastStyle = this.tickerConfig?.broadcastStyle || {};
|
|
3524
|
+
const animation = item.introAnimation || broadcastStyle.defaultIntroAnimation || 'slide-in';
|
|
3525
|
+
const duration = item.introDuration || broadcastStyle.defaultIntroDuration || 2000;
|
|
3526
|
+
const introText = item.introText || 'BREAKING';
|
|
3527
|
+
|
|
3528
|
+
// Update intro text
|
|
3529
|
+
const textElement = this.tickerIntroOverlay.querySelector('.uvf-intro-text') as HTMLSpanElement;
|
|
3530
|
+
if (textElement) {
|
|
3531
|
+
textElement.textContent = introText;
|
|
3532
|
+
}
|
|
3533
|
+
|
|
3534
|
+
// Apply animation
|
|
3535
|
+
this.tickerIntroOverlay.style.display = 'flex';
|
|
3536
|
+
this.tickerIntroOverlay.style.animation = `intro-${animation} ${duration}ms ease-out forwards`;
|
|
3537
|
+
|
|
3538
|
+
// Hide after animation completes
|
|
3539
|
+
setTimeout(() => {
|
|
3540
|
+
if (this.tickerIntroOverlay) {
|
|
3541
|
+
this.tickerIntroOverlay.style.display = 'none';
|
|
3542
|
+
this.tickerIntroOverlay.style.animation = '';
|
|
3543
|
+
}
|
|
3544
|
+
resolve();
|
|
3545
|
+
}, duration);
|
|
3546
|
+
});
|
|
3547
|
+
}
|
|
3548
|
+
|
|
3549
|
+
/**
|
|
3550
|
+
* Start automatic item cycling
|
|
3551
|
+
*/
|
|
3552
|
+
private startItemCycling(): void {
|
|
3553
|
+
if (!this.tickerConfig?.items || this.tickerConfig.items.length <= 1) return;
|
|
3554
|
+
|
|
3555
|
+
const itemCycling = this.tickerConfig.itemCycling || {};
|
|
3556
|
+
if (!itemCycling.enabled) return;
|
|
3557
|
+
|
|
3558
|
+
const currentItem = this.tickerConfig.items[this.tickerCurrentItemIndex];
|
|
3559
|
+
const duration = (currentItem.duration || itemCycling.defaultDuration || 10) * 1000;
|
|
3560
|
+
|
|
3561
|
+
// Start progress animation
|
|
3562
|
+
this.animateProgress(duration);
|
|
3563
|
+
|
|
3564
|
+
// Set timer for next item
|
|
3565
|
+
this.tickerCycleTimer = window.setTimeout(() => {
|
|
3566
|
+
this.transitionToNextItem();
|
|
3567
|
+
}, duration);
|
|
3568
|
+
}
|
|
3569
|
+
|
|
3570
|
+
/**
|
|
3571
|
+
* Stop item cycling
|
|
3572
|
+
*/
|
|
3573
|
+
private stopItemCycling(): void {
|
|
3574
|
+
if (this.tickerCycleTimer) {
|
|
3575
|
+
clearTimeout(this.tickerCycleTimer);
|
|
3576
|
+
this.tickerCycleTimer = null;
|
|
3577
|
+
}
|
|
3578
|
+
}
|
|
3579
|
+
|
|
3580
|
+
/**
|
|
3581
|
+
* Pause item cycling (for hover)
|
|
3582
|
+
*/
|
|
3583
|
+
private pauseItemCycling(): void {
|
|
3584
|
+
if (!this.tickerCycleTimer || this.tickerIsPaused) return;
|
|
3585
|
+
|
|
3586
|
+
this.tickerIsPaused = true;
|
|
3587
|
+
this.tickerPauseStartTime = Date.now();
|
|
3588
|
+
|
|
3589
|
+
// Calculate remaining time
|
|
3590
|
+
const itemCycling = this.tickerConfig?.itemCycling || {};
|
|
3591
|
+
const currentItem = this.tickerConfig?.items?.[this.tickerCurrentItemIndex];
|
|
3592
|
+
const totalDuration = (currentItem?.duration || itemCycling.defaultDuration || 10) * 1000;
|
|
3593
|
+
|
|
3594
|
+
// Stop the timer
|
|
3595
|
+
clearTimeout(this.tickerCycleTimer);
|
|
3596
|
+
this.tickerCycleTimer = null;
|
|
3597
|
+
|
|
3598
|
+
// Pause progress bar animation
|
|
3599
|
+
if (this.tickerProgressFill) {
|
|
3600
|
+
const computedWidth = window.getComputedStyle(this.tickerProgressFill).width;
|
|
3601
|
+
this.tickerProgressFill.style.transition = 'none';
|
|
3602
|
+
this.tickerProgressFill.style.width = computedWidth;
|
|
3603
|
+
}
|
|
3604
|
+
}
|
|
3605
|
+
|
|
3606
|
+
/**
|
|
3607
|
+
* Resume item cycling (after hover)
|
|
3608
|
+
*/
|
|
3609
|
+
private resumeItemCycling(): void {
|
|
3610
|
+
if (!this.tickerIsPaused) return;
|
|
3611
|
+
|
|
3612
|
+
this.tickerIsPaused = false;
|
|
3613
|
+
|
|
3614
|
+
// Calculate remaining time based on progress
|
|
3615
|
+
const itemCycling = this.tickerConfig?.itemCycling || {};
|
|
3616
|
+
const currentItem = this.tickerConfig?.items?.[this.tickerCurrentItemIndex];
|
|
3617
|
+
const totalDuration = (currentItem?.duration || itemCycling.defaultDuration || 10) * 1000;
|
|
3618
|
+
|
|
3619
|
+
// Get current progress percentage
|
|
3620
|
+
let remainingTime = totalDuration;
|
|
3621
|
+
if (this.tickerProgressFill) {
|
|
3622
|
+
const currentWidth = parseFloat(this.tickerProgressFill.style.width) || 0;
|
|
3623
|
+
remainingTime = totalDuration * (1 - currentWidth / 100);
|
|
3624
|
+
}
|
|
3625
|
+
|
|
3626
|
+
// Resume progress animation
|
|
3627
|
+
this.animateProgress(remainingTime);
|
|
3628
|
+
|
|
3629
|
+
// Set timer for remaining time
|
|
3630
|
+
this.tickerCycleTimer = window.setTimeout(() => {
|
|
3631
|
+
this.transitionToNextItem();
|
|
3632
|
+
}, remainingTime);
|
|
3633
|
+
}
|
|
3634
|
+
|
|
3635
|
+
/**
|
|
3636
|
+
* Animate progress bar
|
|
3637
|
+
*/
|
|
3638
|
+
private animateProgress(duration: number): void {
|
|
3639
|
+
if (!this.tickerProgressFill) return;
|
|
3640
|
+
|
|
3641
|
+
// Reset to start
|
|
3642
|
+
this.tickerProgressFill.style.transition = 'none';
|
|
3643
|
+
this.tickerProgressFill.style.width = '0%';
|
|
3644
|
+
|
|
3645
|
+
// Force reflow
|
|
3646
|
+
this.tickerProgressFill.offsetHeight;
|
|
3647
|
+
|
|
3648
|
+
// Animate to 100%
|
|
3649
|
+
this.tickerProgressFill.style.transition = `width ${duration}ms linear`;
|
|
3650
|
+
this.tickerProgressFill.style.width = '100%';
|
|
3651
|
+
}
|
|
3652
|
+
|
|
3653
|
+
/**
|
|
3654
|
+
* Transition to the next news item
|
|
3655
|
+
*/
|
|
3656
|
+
private async transitionToNextItem(): Promise<void> {
|
|
3657
|
+
if (!this.tickerConfig?.items || this.tickerConfig.items.length <= 1) return;
|
|
3658
|
+
|
|
3659
|
+
const itemCycling = this.tickerConfig.itemCycling || {};
|
|
3660
|
+
const transitionType = itemCycling.transitionType || 'fade';
|
|
3661
|
+
const transitionDuration = itemCycling.transitionDuration || 500;
|
|
3662
|
+
|
|
3663
|
+
// Move to next item
|
|
3664
|
+
this.tickerCurrentItemIndex = (this.tickerCurrentItemIndex + 1) % this.tickerConfig.items.length;
|
|
3665
|
+
const nextItem = this.tickerConfig.items[this.tickerCurrentItemIndex];
|
|
3666
|
+
|
|
3667
|
+
// Play intro animation if configured
|
|
3668
|
+
if (nextItem.showIntro) {
|
|
3669
|
+
await this.playIntroAnimation(nextItem);
|
|
3670
|
+
}
|
|
3671
|
+
|
|
3672
|
+
// Apply transition animation
|
|
3673
|
+
await this.applyItemTransition(transitionType, transitionDuration, nextItem);
|
|
3674
|
+
|
|
3675
|
+
// Continue cycling
|
|
3676
|
+
this.startItemCycling();
|
|
3677
|
+
}
|
|
3678
|
+
|
|
3679
|
+
/**
|
|
3680
|
+
* Apply transition animation between items
|
|
3681
|
+
*/
|
|
3682
|
+
private applyItemTransition(
|
|
3683
|
+
transitionType: string,
|
|
3684
|
+
duration: number,
|
|
3685
|
+
item: FlashNewsTickerItem
|
|
3686
|
+
): Promise<void> {
|
|
3687
|
+
return new Promise((resolve) => {
|
|
3688
|
+
const headline = this.tickerHeadlineElement?.querySelector('.uvf-ticker-headline-text') as HTMLSpanElement;
|
|
3689
|
+
const detailTrack = this.tickerDetailElement?.querySelector('.uvf-ticker-track') as HTMLDivElement;
|
|
3690
|
+
|
|
3691
|
+
if (!headline || !detailTrack) {
|
|
3692
|
+
this.updateItemContent(item);
|
|
3693
|
+
resolve();
|
|
3694
|
+
return;
|
|
3695
|
+
}
|
|
3696
|
+
|
|
3697
|
+
if (transitionType === 'none') {
|
|
3698
|
+
this.updateItemContent(item);
|
|
3699
|
+
resolve();
|
|
3700
|
+
return;
|
|
3701
|
+
}
|
|
3702
|
+
|
|
3703
|
+
// Apply fade or slide out animation
|
|
3704
|
+
const outAnimation = transitionType === 'slide' ? 'item-slide-out' : 'item-fade-out';
|
|
3705
|
+
const inAnimation = transitionType === 'slide' ? 'item-slide-in' : 'item-fade-in';
|
|
3706
|
+
|
|
3707
|
+
headline.style.animation = `${outAnimation} ${duration / 2}ms ease-out forwards`;
|
|
3708
|
+
|
|
3709
|
+
setTimeout(() => {
|
|
3710
|
+
// Update content
|
|
3711
|
+
this.updateItemContent(item);
|
|
3712
|
+
|
|
3713
|
+
// Apply fade or slide in animation
|
|
3714
|
+
headline.style.animation = `${inAnimation} ${duration / 2}ms ease-out forwards`;
|
|
3715
|
+
|
|
3716
|
+
setTimeout(() => {
|
|
3717
|
+
headline.style.animation = '';
|
|
3718
|
+
resolve();
|
|
3719
|
+
}, duration / 2);
|
|
3720
|
+
}, duration / 2);
|
|
3721
|
+
});
|
|
3722
|
+
}
|
|
3723
|
+
|
|
3724
|
+
/**
|
|
3725
|
+
* Update headline and detail content for current item
|
|
3726
|
+
*/
|
|
3727
|
+
private updateItemContent(item: FlashNewsTickerItem): void {
|
|
3728
|
+
// Update headline
|
|
3729
|
+
const headline = this.tickerHeadlineElement?.querySelector('.uvf-ticker-headline-text') as HTMLSpanElement;
|
|
3730
|
+
if (headline) {
|
|
3731
|
+
if (item.headlineHtml) {
|
|
3732
|
+
headline.innerHTML = item.headlineHtml;
|
|
3733
|
+
} else if (item.headline) {
|
|
3734
|
+
headline.textContent = item.headline;
|
|
3735
|
+
} else {
|
|
3736
|
+
headline.textContent = item.text;
|
|
3737
|
+
}
|
|
3738
|
+
}
|
|
3739
|
+
|
|
3740
|
+
// Update detail track
|
|
3741
|
+
const detailTrack = this.tickerDetailElement?.querySelector('.uvf-ticker-track') as HTMLDivElement;
|
|
3742
|
+
if (detailTrack) {
|
|
3743
|
+
// Clear existing content
|
|
3744
|
+
detailTrack.innerHTML = '';
|
|
3745
|
+
|
|
3746
|
+
// Get style config
|
|
3747
|
+
const bottomLineConfig = this.tickerConfig?.broadcastStyle?.twoLineDisplay?.bottomLine || {};
|
|
3748
|
+
const bottomLineFontSize = bottomLineConfig.fontSize || 14;
|
|
3749
|
+
|
|
3750
|
+
// Re-render detail text
|
|
3751
|
+
for (let i = 0; i < 10; i++) {
|
|
3752
|
+
const span = document.createElement('span');
|
|
3753
|
+
if (item.html) {
|
|
3754
|
+
span.innerHTML = item.html;
|
|
3755
|
+
} else {
|
|
3756
|
+
span.textContent = item.text;
|
|
3757
|
+
}
|
|
3758
|
+
span.style.cssText = `
|
|
3759
|
+
color: ${bottomLineConfig.textColor || this.tickerConfig?.textColor || '#ffffff'};
|
|
3760
|
+
font-size: ${bottomLineFontSize}px;
|
|
3761
|
+
font-weight: 500;
|
|
3762
|
+
margin-right: 100px;
|
|
3763
|
+
display: inline-flex;
|
|
3764
|
+
align-items: center;
|
|
3765
|
+
`;
|
|
3766
|
+
detailTrack.appendChild(span);
|
|
3767
|
+
}
|
|
3768
|
+
|
|
3769
|
+
// Update scroll speed
|
|
3770
|
+
const bottomLineSpeed = bottomLineConfig.speed || 80;
|
|
3771
|
+
const textWidth = item.text.length * 10;
|
|
3772
|
+
const duration = Math.max(textWidth / bottomLineSpeed, 10);
|
|
3773
|
+
detailTrack.style.animation = `ticker-scroll ${duration}s linear infinite`;
|
|
3774
|
+
}
|
|
3775
|
+
}
|
|
3776
|
+
|
|
2874
3777
|
setAutoQuality(enabled: boolean): void {
|
|
2875
3778
|
this.autoQuality = enabled;
|
|
2876
3779
|
|