vidply 1.0.5 → 1.0.6
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/LICENSE +22 -22
- package/README.md +593 -593
- package/dist/vidply.css +1807 -1807
- package/dist/vidply.esm.js +33 -0
- package/dist/vidply.esm.js.map +2 -2
- package/dist/vidply.esm.min.js +6 -6
- package/dist/vidply.esm.min.meta.json +17 -17
- package/dist/vidply.js +33 -0
- package/dist/vidply.js.map +2 -2
- package/dist/vidply.min.js +6 -6
- package/dist/vidply.min.meta.json +17 -17
- package/package.json +2 -2
- package/src/controls/CaptionManager.js +248 -248
- package/src/controls/ControlBar.js +2026 -2026
- package/src/controls/KeyboardManager.js +233 -233
- package/src/controls/SettingsDialog.js +417 -417
- package/src/controls/TranscriptManager.js +728 -728
- package/src/core/Player.js +1186 -1134
- package/src/i18n/i18n.js +66 -66
- package/src/i18n/translations.js +561 -561
- package/src/icons/Icons.js +183 -183
- package/src/index.js +95 -95
- package/src/renderers/HLSRenderer.js +302 -302
- package/src/renderers/HTML5Renderer.js +298 -298
- package/src/renderers/VimeoRenderer.js +257 -257
- package/src/renderers/YouTubeRenderer.js +274 -274
- package/src/styles/vidply.css +1807 -1807
- package/src/utils/DOMUtils.js +154 -154
- package/src/utils/EventEmitter.js +53 -53
- package/src/utils/TimeUtils.js +87 -87
package/src/utils/DOMUtils.js
CHANGED
|
@@ -1,154 +1,154 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* DOM manipulation utilities
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
export const DOMUtils = {
|
|
6
|
-
createElement(tag, options = {}) {
|
|
7
|
-
const element = document.createElement(tag);
|
|
8
|
-
|
|
9
|
-
if (options.className) {
|
|
10
|
-
element.className = options.className;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
if (options.attributes) {
|
|
14
|
-
Object.entries(options.attributes).forEach(([key, value]) => {
|
|
15
|
-
element.setAttribute(key, value);
|
|
16
|
-
});
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
if (options.innerHTML) {
|
|
20
|
-
element.innerHTML = options.innerHTML;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
if (options.textContent) {
|
|
24
|
-
element.textContent = options.textContent;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
if (options.style) {
|
|
28
|
-
Object.assign(element.style, options.style);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
if (options.children) {
|
|
32
|
-
options.children.forEach(child => {
|
|
33
|
-
if (child) element.appendChild(child);
|
|
34
|
-
});
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
return element;
|
|
38
|
-
},
|
|
39
|
-
|
|
40
|
-
addClass(element, className) {
|
|
41
|
-
if (element && className) {
|
|
42
|
-
element.classList.add(className);
|
|
43
|
-
}
|
|
44
|
-
},
|
|
45
|
-
|
|
46
|
-
removeClass(element, className) {
|
|
47
|
-
if (element && className) {
|
|
48
|
-
element.classList.remove(className);
|
|
49
|
-
}
|
|
50
|
-
},
|
|
51
|
-
|
|
52
|
-
toggleClass(element, className) {
|
|
53
|
-
if (element && className) {
|
|
54
|
-
element.classList.toggle(className);
|
|
55
|
-
}
|
|
56
|
-
},
|
|
57
|
-
|
|
58
|
-
hasClass(element, className) {
|
|
59
|
-
return element && element.classList.contains(className);
|
|
60
|
-
},
|
|
61
|
-
|
|
62
|
-
show(element) {
|
|
63
|
-
if (element) {
|
|
64
|
-
element.style.display = '';
|
|
65
|
-
}
|
|
66
|
-
},
|
|
67
|
-
|
|
68
|
-
hide(element) {
|
|
69
|
-
if (element) {
|
|
70
|
-
element.style.display = 'none';
|
|
71
|
-
}
|
|
72
|
-
},
|
|
73
|
-
|
|
74
|
-
fadeIn(element, duration = 300) {
|
|
75
|
-
if (!element) return;
|
|
76
|
-
|
|
77
|
-
element.style.opacity = '0';
|
|
78
|
-
element.style.display = '';
|
|
79
|
-
|
|
80
|
-
let start = null;
|
|
81
|
-
const animate = (timestamp) => {
|
|
82
|
-
if (!start) start = timestamp;
|
|
83
|
-
const progress = timestamp - start;
|
|
84
|
-
const opacity = Math.min(progress / duration, 1);
|
|
85
|
-
|
|
86
|
-
element.style.opacity = opacity;
|
|
87
|
-
|
|
88
|
-
if (progress < duration) {
|
|
89
|
-
requestAnimationFrame(animate);
|
|
90
|
-
}
|
|
91
|
-
};
|
|
92
|
-
|
|
93
|
-
requestAnimationFrame(animate);
|
|
94
|
-
},
|
|
95
|
-
|
|
96
|
-
fadeOut(element, duration = 300) {
|
|
97
|
-
if (!element) return;
|
|
98
|
-
|
|
99
|
-
const startOpacity = parseFloat(getComputedStyle(element).opacity) || 1;
|
|
100
|
-
let start = null;
|
|
101
|
-
|
|
102
|
-
const animate = (timestamp) => {
|
|
103
|
-
if (!start) start = timestamp;
|
|
104
|
-
const progress = timestamp - start;
|
|
105
|
-
const opacity = Math.max(startOpacity - (progress / duration), 0);
|
|
106
|
-
|
|
107
|
-
element.style.opacity = opacity;
|
|
108
|
-
|
|
109
|
-
if (progress < duration) {
|
|
110
|
-
requestAnimationFrame(animate);
|
|
111
|
-
} else {
|
|
112
|
-
element.style.display = 'none';
|
|
113
|
-
}
|
|
114
|
-
};
|
|
115
|
-
|
|
116
|
-
requestAnimationFrame(animate);
|
|
117
|
-
},
|
|
118
|
-
|
|
119
|
-
offset(element) {
|
|
120
|
-
if (!element) return { top: 0, left: 0 };
|
|
121
|
-
|
|
122
|
-
const rect = element.getBoundingClientRect();
|
|
123
|
-
return {
|
|
124
|
-
top: rect.top + window.pageYOffset,
|
|
125
|
-
left: rect.left + window.pageXOffset,
|
|
126
|
-
width: rect.width,
|
|
127
|
-
height: rect.height
|
|
128
|
-
};
|
|
129
|
-
},
|
|
130
|
-
|
|
131
|
-
escapeHTML(str) {
|
|
132
|
-
const div = document.createElement('div');
|
|
133
|
-
div.textContent = str;
|
|
134
|
-
return div.innerHTML;
|
|
135
|
-
},
|
|
136
|
-
|
|
137
|
-
sanitizeHTML(html) {
|
|
138
|
-
// Basic HTML sanitization - allow safe tags for VTT captions
|
|
139
|
-
// Since we control the HTML (from VTT parsing), we can safely allow these tags
|
|
140
|
-
const temp = document.createElement('div');
|
|
141
|
-
|
|
142
|
-
// Strip out any potentially dangerous tags/attributes
|
|
143
|
-
// Allow: strong, em, u, span, b, i with class and data-voice attributes
|
|
144
|
-
const safeHtml = html
|
|
145
|
-
.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
|
|
146
|
-
.replace(/<iframe\b[^<]*(?:(?!<\/iframe>)<[^<]*)*<\/iframe>/gi, '')
|
|
147
|
-
.replace(/on\w+\s*=/gi, '') // Remove event handlers
|
|
148
|
-
.replace(/javascript:/gi, ''); // Remove javascript: protocol
|
|
149
|
-
|
|
150
|
-
temp.innerHTML = safeHtml;
|
|
151
|
-
return temp.innerHTML;
|
|
152
|
-
}
|
|
153
|
-
};
|
|
154
|
-
|
|
1
|
+
/**
|
|
2
|
+
* DOM manipulation utilities
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export const DOMUtils = {
|
|
6
|
+
createElement(tag, options = {}) {
|
|
7
|
+
const element = document.createElement(tag);
|
|
8
|
+
|
|
9
|
+
if (options.className) {
|
|
10
|
+
element.className = options.className;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if (options.attributes) {
|
|
14
|
+
Object.entries(options.attributes).forEach(([key, value]) => {
|
|
15
|
+
element.setAttribute(key, value);
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (options.innerHTML) {
|
|
20
|
+
element.innerHTML = options.innerHTML;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (options.textContent) {
|
|
24
|
+
element.textContent = options.textContent;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (options.style) {
|
|
28
|
+
Object.assign(element.style, options.style);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (options.children) {
|
|
32
|
+
options.children.forEach(child => {
|
|
33
|
+
if (child) element.appendChild(child);
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return element;
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
addClass(element, className) {
|
|
41
|
+
if (element && className) {
|
|
42
|
+
element.classList.add(className);
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
removeClass(element, className) {
|
|
47
|
+
if (element && className) {
|
|
48
|
+
element.classList.remove(className);
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
toggleClass(element, className) {
|
|
53
|
+
if (element && className) {
|
|
54
|
+
element.classList.toggle(className);
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
hasClass(element, className) {
|
|
59
|
+
return element && element.classList.contains(className);
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
show(element) {
|
|
63
|
+
if (element) {
|
|
64
|
+
element.style.display = '';
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
hide(element) {
|
|
69
|
+
if (element) {
|
|
70
|
+
element.style.display = 'none';
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
fadeIn(element, duration = 300) {
|
|
75
|
+
if (!element) return;
|
|
76
|
+
|
|
77
|
+
element.style.opacity = '0';
|
|
78
|
+
element.style.display = '';
|
|
79
|
+
|
|
80
|
+
let start = null;
|
|
81
|
+
const animate = (timestamp) => {
|
|
82
|
+
if (!start) start = timestamp;
|
|
83
|
+
const progress = timestamp - start;
|
|
84
|
+
const opacity = Math.min(progress / duration, 1);
|
|
85
|
+
|
|
86
|
+
element.style.opacity = opacity;
|
|
87
|
+
|
|
88
|
+
if (progress < duration) {
|
|
89
|
+
requestAnimationFrame(animate);
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
requestAnimationFrame(animate);
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
fadeOut(element, duration = 300) {
|
|
97
|
+
if (!element) return;
|
|
98
|
+
|
|
99
|
+
const startOpacity = parseFloat(getComputedStyle(element).opacity) || 1;
|
|
100
|
+
let start = null;
|
|
101
|
+
|
|
102
|
+
const animate = (timestamp) => {
|
|
103
|
+
if (!start) start = timestamp;
|
|
104
|
+
const progress = timestamp - start;
|
|
105
|
+
const opacity = Math.max(startOpacity - (progress / duration), 0);
|
|
106
|
+
|
|
107
|
+
element.style.opacity = opacity;
|
|
108
|
+
|
|
109
|
+
if (progress < duration) {
|
|
110
|
+
requestAnimationFrame(animate);
|
|
111
|
+
} else {
|
|
112
|
+
element.style.display = 'none';
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
requestAnimationFrame(animate);
|
|
117
|
+
},
|
|
118
|
+
|
|
119
|
+
offset(element) {
|
|
120
|
+
if (!element) return { top: 0, left: 0 };
|
|
121
|
+
|
|
122
|
+
const rect = element.getBoundingClientRect();
|
|
123
|
+
return {
|
|
124
|
+
top: rect.top + window.pageYOffset,
|
|
125
|
+
left: rect.left + window.pageXOffset,
|
|
126
|
+
width: rect.width,
|
|
127
|
+
height: rect.height
|
|
128
|
+
};
|
|
129
|
+
},
|
|
130
|
+
|
|
131
|
+
escapeHTML(str) {
|
|
132
|
+
const div = document.createElement('div');
|
|
133
|
+
div.textContent = str;
|
|
134
|
+
return div.innerHTML;
|
|
135
|
+
},
|
|
136
|
+
|
|
137
|
+
sanitizeHTML(html) {
|
|
138
|
+
// Basic HTML sanitization - allow safe tags for VTT captions
|
|
139
|
+
// Since we control the HTML (from VTT parsing), we can safely allow these tags
|
|
140
|
+
const temp = document.createElement('div');
|
|
141
|
+
|
|
142
|
+
// Strip out any potentially dangerous tags/attributes
|
|
143
|
+
// Allow: strong, em, u, span, b, i with class and data-voice attributes
|
|
144
|
+
const safeHtml = html
|
|
145
|
+
.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
|
|
146
|
+
.replace(/<iframe\b[^<]*(?:(?!<\/iframe>)<[^<]*)*<\/iframe>/gi, '')
|
|
147
|
+
.replace(/on\w+\s*=/gi, '') // Remove event handlers
|
|
148
|
+
.replace(/javascript:/gi, ''); // Remove javascript: protocol
|
|
149
|
+
|
|
150
|
+
temp.innerHTML = safeHtml;
|
|
151
|
+
return temp.innerHTML;
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
|
|
@@ -1,53 +1,53 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Simple EventEmitter implementation
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
export class EventEmitter {
|
|
6
|
-
constructor() {
|
|
7
|
-
this.events = {};
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
on(event, listener) {
|
|
11
|
-
if (!this.events[event]) {
|
|
12
|
-
this.events[event] = [];
|
|
13
|
-
}
|
|
14
|
-
this.events[event].push(listener);
|
|
15
|
-
return this;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
once(event, listener) {
|
|
19
|
-
const onceListener = (...args) => {
|
|
20
|
-
listener(...args);
|
|
21
|
-
this.off(event, onceListener);
|
|
22
|
-
};
|
|
23
|
-
return this.on(event, onceListener);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
off(event, listener) {
|
|
27
|
-
if (!this.events[event]) return this;
|
|
28
|
-
|
|
29
|
-
if (!listener) {
|
|
30
|
-
delete this.events[event];
|
|
31
|
-
} else {
|
|
32
|
-
this.events[event] = this.events[event].filter(l => l !== listener);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
return this;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
emit(event, ...args) {
|
|
39
|
-
if (!this.events[event]) return this;
|
|
40
|
-
|
|
41
|
-
this.events[event].forEach(listener => {
|
|
42
|
-
listener(...args);
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
return this;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
removeAllListeners() {
|
|
49
|
-
this.events = {};
|
|
50
|
-
return this;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Simple EventEmitter implementation
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export class EventEmitter {
|
|
6
|
+
constructor() {
|
|
7
|
+
this.events = {};
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
on(event, listener) {
|
|
11
|
+
if (!this.events[event]) {
|
|
12
|
+
this.events[event] = [];
|
|
13
|
+
}
|
|
14
|
+
this.events[event].push(listener);
|
|
15
|
+
return this;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
once(event, listener) {
|
|
19
|
+
const onceListener = (...args) => {
|
|
20
|
+
listener(...args);
|
|
21
|
+
this.off(event, onceListener);
|
|
22
|
+
};
|
|
23
|
+
return this.on(event, onceListener);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
off(event, listener) {
|
|
27
|
+
if (!this.events[event]) return this;
|
|
28
|
+
|
|
29
|
+
if (!listener) {
|
|
30
|
+
delete this.events[event];
|
|
31
|
+
} else {
|
|
32
|
+
this.events[event] = this.events[event].filter(l => l !== listener);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return this;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
emit(event, ...args) {
|
|
39
|
+
if (!this.events[event]) return this;
|
|
40
|
+
|
|
41
|
+
this.events[event].forEach(listener => {
|
|
42
|
+
listener(...args);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
return this;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
removeAllListeners() {
|
|
49
|
+
this.events = {};
|
|
50
|
+
return this;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
package/src/utils/TimeUtils.js
CHANGED
|
@@ -1,87 +1,87 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Time formatting and conversion utilities
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import {i18n} from '../i18n/i18n.js';
|
|
6
|
-
|
|
7
|
-
export const TimeUtils = {
|
|
8
|
-
/**
|
|
9
|
-
* Format seconds to time string (HH:MM:SS or MM:SS)
|
|
10
|
-
*/
|
|
11
|
-
formatTime(seconds, alwaysShowHours = false) {
|
|
12
|
-
if (!isFinite(seconds) || seconds < 0) {
|
|
13
|
-
return alwaysShowHours ? '00:00:00' : '00:00';
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
const hours = Math.floor(seconds / 3600);
|
|
17
|
-
const minutes = Math.floor((seconds % 3600) / 60);
|
|
18
|
-
const secs = Math.floor(seconds % 60);
|
|
19
|
-
|
|
20
|
-
const pad = (num) => String(num).padStart(2, '0');
|
|
21
|
-
|
|
22
|
-
if (hours > 0 || alwaysShowHours) {
|
|
23
|
-
return `${pad(hours)}:${pad(minutes)}:${pad(secs)}`;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
return `${pad(minutes)}:${pad(secs)}`;
|
|
27
|
-
},
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Parse time string to seconds
|
|
31
|
-
*/
|
|
32
|
-
parseTime(timeString) {
|
|
33
|
-
const parts = timeString.split(':').map(p => parseInt(p, 10));
|
|
34
|
-
|
|
35
|
-
if (parts.length === 3) {
|
|
36
|
-
// HH:MM:SS
|
|
37
|
-
return parts[0] * 3600 + parts[1] * 60 + parts[2];
|
|
38
|
-
} else if (parts.length === 2) {
|
|
39
|
-
// MM:SS
|
|
40
|
-
return parts[0] * 60 + parts[1];
|
|
41
|
-
} else if (parts.length === 1) {
|
|
42
|
-
// SS
|
|
43
|
-
return parts[0];
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
return 0;
|
|
47
|
-
},
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Format seconds to readable duration
|
|
51
|
-
*/
|
|
52
|
-
formatDuration(seconds) {
|
|
53
|
-
if (!isFinite(seconds) || seconds < 0) {
|
|
54
|
-
return i18n.t('time.seconds', { count: 0 });
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const hours = Math.floor(seconds / 3600);
|
|
58
|
-
const minutes = Math.floor((seconds % 3600) / 60);
|
|
59
|
-
const secs = Math.floor(seconds % 60);
|
|
60
|
-
|
|
61
|
-
const parts = [];
|
|
62
|
-
|
|
63
|
-
if (hours > 0) {
|
|
64
|
-
const key = hours === 1 ? 'time.hour' : 'time.hours';
|
|
65
|
-
parts.push(i18n.t(key, { count: hours }));
|
|
66
|
-
}
|
|
67
|
-
if (minutes > 0) {
|
|
68
|
-
const key = minutes === 1 ? 'time.minute' : 'time.minutes';
|
|
69
|
-
parts.push(i18n.t(key, { count: minutes }));
|
|
70
|
-
}
|
|
71
|
-
if (secs > 0 || parts.length === 0) {
|
|
72
|
-
const key = secs === 1 ? 'time.second' : 'time.seconds';
|
|
73
|
-
parts.push(i18n.t(key, { count: secs }));
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
return parts.join(', ');
|
|
77
|
-
},
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Format percentage
|
|
81
|
-
*/
|
|
82
|
-
formatPercentage(value, total) {
|
|
83
|
-
if (total === 0) return 0;
|
|
84
|
-
return Math.round((value / total) * 100);
|
|
85
|
-
}
|
|
86
|
-
};
|
|
87
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Time formatting and conversion utilities
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import {i18n} from '../i18n/i18n.js';
|
|
6
|
+
|
|
7
|
+
export const TimeUtils = {
|
|
8
|
+
/**
|
|
9
|
+
* Format seconds to time string (HH:MM:SS or MM:SS)
|
|
10
|
+
*/
|
|
11
|
+
formatTime(seconds, alwaysShowHours = false) {
|
|
12
|
+
if (!isFinite(seconds) || seconds < 0) {
|
|
13
|
+
return alwaysShowHours ? '00:00:00' : '00:00';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const hours = Math.floor(seconds / 3600);
|
|
17
|
+
const minutes = Math.floor((seconds % 3600) / 60);
|
|
18
|
+
const secs = Math.floor(seconds % 60);
|
|
19
|
+
|
|
20
|
+
const pad = (num) => String(num).padStart(2, '0');
|
|
21
|
+
|
|
22
|
+
if (hours > 0 || alwaysShowHours) {
|
|
23
|
+
return `${pad(hours)}:${pad(minutes)}:${pad(secs)}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return `${pad(minutes)}:${pad(secs)}`;
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Parse time string to seconds
|
|
31
|
+
*/
|
|
32
|
+
parseTime(timeString) {
|
|
33
|
+
const parts = timeString.split(':').map(p => parseInt(p, 10));
|
|
34
|
+
|
|
35
|
+
if (parts.length === 3) {
|
|
36
|
+
// HH:MM:SS
|
|
37
|
+
return parts[0] * 3600 + parts[1] * 60 + parts[2];
|
|
38
|
+
} else if (parts.length === 2) {
|
|
39
|
+
// MM:SS
|
|
40
|
+
return parts[0] * 60 + parts[1];
|
|
41
|
+
} else if (parts.length === 1) {
|
|
42
|
+
// SS
|
|
43
|
+
return parts[0];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return 0;
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Format seconds to readable duration
|
|
51
|
+
*/
|
|
52
|
+
formatDuration(seconds) {
|
|
53
|
+
if (!isFinite(seconds) || seconds < 0) {
|
|
54
|
+
return i18n.t('time.seconds', { count: 0 });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const hours = Math.floor(seconds / 3600);
|
|
58
|
+
const minutes = Math.floor((seconds % 3600) / 60);
|
|
59
|
+
const secs = Math.floor(seconds % 60);
|
|
60
|
+
|
|
61
|
+
const parts = [];
|
|
62
|
+
|
|
63
|
+
if (hours > 0) {
|
|
64
|
+
const key = hours === 1 ? 'time.hour' : 'time.hours';
|
|
65
|
+
parts.push(i18n.t(key, { count: hours }));
|
|
66
|
+
}
|
|
67
|
+
if (minutes > 0) {
|
|
68
|
+
const key = minutes === 1 ? 'time.minute' : 'time.minutes';
|
|
69
|
+
parts.push(i18n.t(key, { count: minutes }));
|
|
70
|
+
}
|
|
71
|
+
if (secs > 0 || parts.length === 0) {
|
|
72
|
+
const key = secs === 1 ? 'time.second' : 'time.seconds';
|
|
73
|
+
parts.push(i18n.t(key, { count: secs }));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return parts.join(', ');
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Format percentage
|
|
81
|
+
*/
|
|
82
|
+
formatPercentage(value, total) {
|
|
83
|
+
if (total === 0) return 0;
|
|
84
|
+
return Math.round((value / total) * 100);
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
|