PluginProbe
WpStream – Live Streaming, Video on Demand, Pay Per View / trunk
WpStream – Live Streaming, Video on Demand, Pay Per View vtrunk
4.14.1 4.14.0 4.13.2 4.13.1 4.13 4.12.5 4.12.4 4.12.3 4.12.2 4.12.1 4.12 4.4.4 4.4.5 4.4.6 4.4.7 4.4.8 4.4.9 4.5 4.5.1 4.5.11 4.5.11.1 4.5.11.2 4.5.11.4 4.5.11.5 4.5.11.6 All 181 releases
wpstream / admin / js / admin_control.js

admin_control.js in WpStream – Live Streaming, Video on Demand, Pay Per View trunk, at admin/js/admin_control.js

937 lines 36.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /*global $, jQuery, */
2 /*
3 * WpStream admin control — product edit + storage upload behaviours.
4 *
5 * Loaded on the WooCommerce product edit and WpStream storage admin screens.
6 * Responsibilities:
7 * - Dismissing WpStream admin notices via AJAX.
8 * - The direct-to-cloud file upload flow, including chunked S3-style multipart
9 * upload with retry for files over the 5GB standard-upload limit.
10 * - Toggling product-type-specific metabox fields (live stream / VOD / bundle).
11 * - WordPress media-library pickers for category images, external/recorded VOD
12 * sources, VTT captions, and the player logo.
13 * Server strings and endpoints come from the localized `wpstream_admin_control_vars`.
14 */
15 // Multipart chunk size: 128MB per part (grows for very large files, see MAX_PARTS).
16 const CHUNK_SIZE = 128 * 1024 * 1024; // 128MB in bytes
17 // The storage backend assembles at most this many parts per upload, so the
18 // chunk size scales up once a file would need more parts than this.
19 const MAX_PARTS = 1000;
20 // Files larger than this (5GB) must use the multipart upload path.
21 const MAX_STANDARD_UPLOAD_SIZE = 5 * 1000000000; // 5GB in bytes
22 // Maximum number of retry attempts for a failed chunk.
23 const MAX_RETRIES = 3;
24 // Delay (ms) between chunk retry attempts.
25 const RETRY_DELAY = 5000;
26
27 // Main admin bootstrap: runs once the DOM is ready.
28 jQuery(document).ready(function ($) {
29 "use strict";
30
31
32 // Wire up the shared download/delete link handlers for stored recordings.
33 WpStreamUtils.generate_download_link();
34 WpStreamUtils.generate_delete_link();
35 // Bind the recorded/external video and caption media pickers.
36 wpstream_handle_video_selection();
37 wpstream_handle_caption_selection();
38 // Bind category featured-image uploader and the player-logo uploader.
39 wpstream_upload_images_in_wpadmin();
40
41 wpstream_upload_player_logo();
42
43
44 /**
45 * Toggle a social-media settings sub-panel based on its checkbox.
46 *
47 * @param {string} social_class - Base CSS class for the checkbox/container pair.
48 * @return {void}
49 */
50 function social_media_toggle(social_class){
51
52 // When the checkbox for this social class changes, slide its container.
53 jQuery('.'+social_class).on('change',function(){
54
55 // Checked: reveal the matching container; unchecked: hide it.
56 if( $(this).prop('checked') ){
57 jQuery($(this).parent().parent().find( '.'+social_class+'_container' )).slideDown('100');
58 }else{
59 jQuery($(this).parent().parent().find( '.'+social_class+'_container' )).slideUp('100');
60 }
61
62 });
63 }
64
65
66
67 // Dismiss a WpStream admin notice: persist the dismissal server-side.
68 jQuery('.wpstream_notices .notice-dismiss').on('click',function(){
69
70 // Build the admin-ajax endpoint URL.
71 var ajaxurl = wpstream_admin_control_vars.admin_url + 'admin-ajax.php';
72 // Which notice was dismissed (from the parent's data attribute).
73 var notice_type = $(this).parent().attr('data-notice-type');
74 // Security nonce for the dismissal request.
75 var nonce = $('#wpstream_notice_nonce').val();
76
77
78 // Tell the server to remember this notice was dismissed.
79 jQuery.ajax({
80 type: 'POST',
81 url: ajaxurl,
82 data: {
83 'action' : 'wpstream_update_cache_notice',
84 'notice_type' : notice_type,
85 'security' : nonce
86 },
87 success: function (data) {
88 // No UI action needed on success.
89
90 },
91 error: function (errorThrown) {
92 // Errors are silently ignored.
93
94 }
95 });
96 });
97
98 // Custom styled file inputs: reflect the chosen file name in the label.
99 $( '.inputfile' ).each( function(){
100 // Cache the input, its label, and the label's original markup.
101 var $input = $( this ),
102 $label = $input.next( 'label' ),
103 labelVal = $label.html();
104
105 // On file selection, compute the display name.
106 $input.on( 'change', function( e )
107 {
108 var fileName = '';
109
110 // Multiple files: use the data-multiple-caption template with the count.
111 if( this.files && this.files.length > 1 )
112 fileName = ( this.getAttribute( 'data-multiple-caption' ) || '' ).replace( '{count}', this.files.length );
113 // Single file: take the base name from the input's value path.
114 else if( e.target.value )
115 fileName = e.target.value.split( '\\' ).pop();
116
117 // Show the file name, or restore the original label if none.
118 if( fileName )
119 $label.find( 'span' ).html( fileName );
120 else
121 $label.html( labelVal );
122 });
123
124 // Firefox bug fix
125 // Track focus state on the input so it can be styled while focused.
126 $input
127 .on( 'focus', function(){ $input.addClass( 'has-focus' ); })
128 .on( 'blur', function(){ $input.removeClass( 'has-focus' ); });
129 });
130
131
132
133 /*
134 *
135 * File Upload
136 *
137 */
138
139
140 // The direct-upload form and multipart upload state shared across handlers.
141 var form = $('.direct-upload');
142 // Holds multipart part URLs and the chunk size once a multipart upload is
143 // initiated (null = standard upload).
144 var multipartUploadData = null;
145 // Server-side handle identifying the in-progress multipart upload.
146 var handle = null;
147 // Accumulates the parts that have been uploaded successfully.
148 var currentUploadedParts = [];
149
150 // Configure the jQuery File Upload plugin on the direct-upload form.
151 form.fileupload({
152 url: form.attr('action'),
153 type: form.attr('method'),
154
155 datatype: 'xml',
156 // Called when a file is selected/added; validates and starts the upload.
157 add: function (event, data) {
158
159
160 // Reject anything that is not MP4 or QuickTime video.
161 if( data.files[0].type!=='video/mp4' && data.files[0].type!=='video/quicktime'){
162 jQuery('#wpstream_uploaded_mes').empty().html(wpstream_admin_control_vars.not_accepted);
163 jQuery('#wpstream_label_action').text(wpstream_admin_control_vars.choose_a_file);
164 return;
165 }
166
167 // Get file info
168 var file = data.files[0];
169 var fileSizeInBytes = file.size;
170 // File size in megabytes for quota comparison.
171 var file_size = (parseInt(fileSizeInBytes, 10))/1000000;
172 // Quota check: either against streaming-hours or storage/bandwidth allowance.
173 if ( wpstream_admin_control_vars.use_streaming_hours ) {
174 // Streaming-hours model: block upload when the user has no hours left.
175 var user_storage_hours = jQuery('#wpstream_storage_hours').val();
176 if ( parseFloat(user_storage_hours) <= 0 ) {
177 jQuery('#wpstream_uploaded_mes').empty().html(wpstream_admin_control_vars.no_streaming_hours);
178 return;
179 }
180 } else {
181 // Storage/bandwidth model: read the remaining allowances.
182 var user_storage = jQuery('#wpstream_storage').val();
183 var user_band = jQuery('#wpstream_band').val();
184
185 // Block the upload if the file exceeds storage or bandwidth.
186 if(file_size > user_storage || file_size > user_band){
187 jQuery('#wpstream_uploaded_mes').empty().html(wpstream_admin_control_vars.no_band_no_store);
188 return;
189 }
190 }
191
192 // Update UI
193 // Switch the label to the "uploading" state and disable the button.
194 $('#wpstream_label_action').text(wpstream_admin_control_vars.uploading);
195 $('#wpstream_upload').prop('disabled', true);
196 $('label[for="wpstream_upload"]')
197 .css('cursor','not-allowed')
198 .css('background-color','#8c8f94');
199
200 // Clear any previous status message.
201 jQuery('#wpstream_uploaded_mes').empty();
202
203 // Show warning message if leaving page during upload
204 window.onbeforeunload = function () {
205 return 'You have unsaved changes.';
206 };
207
208 // Set content headers
209 // Populate the form's content-type/length fields for the upload.
210 form.find('input[name="Content-Type"]').val(file.type);
211 form.find('input[name="Content-Length"]').val(file.size);
212
213 // Show the progress bar
214 // Build a progress bar keyed by file size so progress handlers can find it.
215 var bar = $('<div class="progress" data-mod="'+file.size+'"><div class="bar"></div></div>');
216 $('.progress-bar-area').append(bar);
217 bar.slideDown('fast');
218
219 // Check if file size exceeds 5GB and requires multipart upload
220 if (fileSizeInBytes > MAX_STANDARD_UPLOAD_SIZE) {
221 // Show multipart upload message
222 jQuery('#wpstream_uploaded_mes').html(wpstream_admin_control_vars.exceeding_limit);
223 // Initiate multipart upload
224 initiateMultipartUpload(file, data);
225 } else {
226 // Standard upload for files under 5GB
227 data.submit();
228 }
229 },
230 // Progress callback: updates the bar during a standard (non-multipart) upload.
231 progress: function (e, data) {
232 // Standard upload progress
233 if (!multipartUploadData) {
234 // Compute and render the percentage complete.
235 var percent = Math.round((data.loaded / data.total) * 100);
236 $('.progress[data-mod="'+data.files[0].size+'"] .bar').css('width', percent + '%').html(percent+'%');
237 }
238 },
239
240 // Upload failure callback.
241 fail: function () {
242 handleUploadFailure();
243 },
244
245 // Upload error callback.
246 error: function () {
247 handleUploadFailure();
248 },
249 // Upload done callback: finalize a completed standard upload.
250 done: function (event, data) {
251 if (!multipartUploadData) {
252 // Handle standard upload completion
253 handleUploadSuccess(data.files[0]);
254 }
255 }
256 });
257
258 /**
259 * Initiate a multipart (chunked) upload for a large file.
260 *
261 * Requests presigned part URLs and an upload handle from the server, then
262 * begins uploading chunks. Falls back to failure handling on any error.
263 *
264 * @param {File} file - The selected file to upload.
265 * @param {Object} data - jQuery File Upload data object for this file.
266 * @return {void}
267 */
268 // Function to initiate multipart upload
269 function initiateMultipartUpload(file, data) {
270 // AJAX endpoint plus file metadata for the request.
271 var ajaxurl = wpstream_admin_control_vars.admin_url + 'admin-ajax.php';
272 var fileName = file.name;
273 var fileSize = file.size;
274
275 // Calculate the chunk size and number of parts. The chunk grows beyond
276 // CHUNK_SIZE when the file would otherwise exceed MAX_PARTS parts.
277 var chunkSize = Math.max(CHUNK_SIZE, Math.ceil(fileSize / MAX_PARTS));
278 var numParts = Math.ceil(fileSize / chunkSize);
279
280 // Inform the user we are preparing the multipart upload.
281 jQuery('#wpstream_uploaded_mes').html(wpstream_admin_control_vars.preparing_multipart);
282
283 // Ask the server to initiate the multipart upload and return part URLs.
284 jQuery.ajax({
285 type: 'POST',
286 url: ajaxurl,
287 dataType: 'json',
288 data: {
289 'action': 'wpstream_initiate_multipart_upload',
290 'security': wpstream_admin_control_vars.multipart_upload_nonce,
291 'file_name': fileName,
292 'file_size': fileSize,
293 'content_type': file.type,
294 'parts': numParts
295 },
296 success: function(response) {
297 if (response.success) {
298 // Validate required data exists in response
299 if (!response.data ||
300 !response.data.multipart ||
301 !response.data.parts ||
302 !response.data.handle
303 ) {
304 // Missing fields: abort with an invalid-response message.
305 handleUploadFailure(wpstream_admin_control_vars.invalid_response);
306 return;
307 }
308
309 // Store the part URLs and chunk size, and reset per-upload state.
310 multipartUploadData = {
311 parts: response.data.parts,
312 chunkSize: chunkSize
313 };
314 currentUploadedParts = [];
315 handle = response.data.handle;
316
317 // Start uploading chunks
318 uploadNextChunk(file, 0, numParts);
319 } else {
320 // Server refused the upload: show its reason
321 // (wp_send_json_error puts the string under `data`).
322 handleUploadFailure(typeof response.data === 'string' ? response.data : '');
323 }
324 },
325 error: function() {
326 // Transport error initiating the multipart upload.
327 handleUploadFailure();
328 }
329 });
330 }
331
332 /**
333 * Upload a single chunk of the file, then recurse to the next chunk.
334 *
335 * PUTs the chunk to its presigned URL, updates the progress bar, and on
336 * success advances to the next part; on network error it defers to the
337 * retry handler. When all parts are done it completes the upload.
338 *
339 * @param {File} file - The file being uploaded.
340 * @param {number} partIndex - Zero-based index of the current chunk.
341 * @param {number} totalParts - Total number of chunks.
342 * @param {number} [retryCount=0] - Current retry attempt for this chunk.
343 * @return {void}
344 */
345 // Function to upload a chunk of the file
346 function uploadNextChunk(
347 file,
348 partIndex,
349 totalParts,
350 retryCount = 0
351 ) {
352 // Base case: every part uploaded, so finalize the multipart upload.
353 if (partIndex >= totalParts) {
354 // All parts uploaded, complete the multipart upload
355 completeMultipartUpload(file, totalParts);
356 return;
357 }
358
359 // Compute this chunk's byte range (using the chunk size fixed at
360 // initiation) and slice it out of the file.
361 var chunkSize = multipartUploadData.chunkSize;
362 var start = partIndex * chunkSize;
363 var end = Math.min((partIndex + 1) * chunkSize, file.size);
364 var chunk = file.slice(start, end);
365 // Part numbers are 1-based for the storage API.
366 var partNumber = partIndex + 1;
367
368 // Show which part is currently uploading.
369 jQuery('#wpstream_uploaded_mes').html(wpstream_admin_control_vars.uploading_part.replace('{part}', partNumber).replace('{total}', totalParts));
370
371 // Update progress bar to show overall progress
372 var overallProgress = Math.round((partIndex / totalParts) * 100);
373 jQuery('.progress[data-mod="'+file.size+'"] .bar').css('width', overallProgress + '%').html(overallProgress+'%');
374
375 // Upload the chunk
376 // PUT the chunk directly to its presigned part URL.
377 var xhr = new XMLHttpRequest();
378 // xhr.open('POST', 'https://s3.amazonaws.com/' + partData.bucket, true);
379 xhr.open('PUT', multipartUploadData.parts[partIndex], true);
380
381 // On completion, check the HTTP status to decide success vs failure.
382 xhr.onload = function() {
383 // 200/204 indicate the part stored successfully.
384 if (xhr.status === 204 || xhr.status === 200) {
385 // Record the completed part.
386 currentUploadedParts.push({
387 PartNumber: partNumber,
388 });
389
390 // Upload next chunk
391 uploadNextChunk(file, partIndex + 1, totalParts);
392 } else {
393 // Non-success status: log details and abort the upload.
394 var errorInfo = {
395 status: xhr.status,
396 statusText: xhr.statusText,
397 response: xhr.responseText,
398 headers: xhr.getAllResponseHeaders()
399 };
400 console.error('Part Upload Failed:', errorInfo);
401
402 jQuery('#wpstream_uploaded_mes').html(wpstream_admin_control_vars.upload_failed_part.replace('{part}', partNumber));
403 handleUploadFailure();
404 }
405 };
406
407 // On network error, hand off to the retry logic.
408 xhr.onerror = function() {
409 handleChunkError(file, partIndex, totalParts, retryCount, xhr);
410 // jQuery('#wpstream_uploaded_mes').html(wpstream_admin_control_vars.upload_failed_part.replace('{part}', partNumber));
411 // handleUploadFailure();
412 };
413
414 // Per-chunk progress: blend chunk progress into the overall bar.
415 xhr.upload.onprogress = function(e) {
416 if (e.lengthComputable) {
417 // Calculate chunk progress and overall progress
418 var chunkProgress = (e.loaded / e.total) * 100;
419 var overallProgress = Math.round((partIndex / totalParts * 100) + (chunkProgress / totalParts));
420 jQuery('.progress[data-mod="'+file.size+'"] .bar').css('width', overallProgress + '%').html(overallProgress+'%');
421 }
422 };
423
424 // Send the chunk body.
425 xhr.send(chunk);
426 }
427
428 /**
429 * Retry a failed chunk after a delay, up to MAX_RETRIES times.
430 *
431 * @param {File} file - The file being uploaded.
432 * @param {number} partIndex - Zero-based index of the failed chunk.
433 * @param {number} totalParts - Total number of chunks.
434 * @param {number} retryCount - Number of retries already attempted.
435 * @param {XMLHttpRequest} xhr - The failed request (unused; kept for context).
436 * @return {void}
437 */
438 // Function to retry uploading when failing
439 // Adding a delay of RETRY_DELAY seconds before retrying
440 function handleChunkError(file, partIndex, totalParts, retryCount, xhr) {
441 // 1-based part number for user-facing messages.
442 var partNumber = partIndex + 1;
443
444 // Retry while under the cap; otherwise give up.
445 if ( retryCount < MAX_RETRIES ) {
446 // Notify the user this part is being retried.
447 jQuery('#wpstream_uploaded_mes').html(wpstream_admin_control_vars.upload_failed_part_retry.replace('{part}', partNumber).replace('{times}', retryCount + 1));
448 // Re-attempt the same chunk after RETRY_DELAY with an incremented count.
449 setTimeout(function() {
450 uploadNextChunk(file, partIndex, totalParts, retryCount + 1);
451 }, RETRY_DELAY);
452 } else {
453 // Retries exhausted: abort the whole upload.
454 handleUploadFailure();
455 }
456
457 }
458
459 /**
460 * Finalize a multipart upload once all parts are uploaded.
461 *
462 * Tells the server to assemble the parts into the final object, then routes
463 * to success or failure handling based on the response.
464 *
465 * @param {File} file - The uploaded file.
466 * @param {number} totalParts - Total number of parts uploaded.
467 * @return {void}
468 */
469 // Function to complete multipart upload
470 function completeMultipartUpload(file, totalParts) {
471 // AJAX endpoint for the completion request.
472 var ajaxurl = wpstream_admin_control_vars.admin_url + 'admin-ajax.php';
473
474 // Inform the user the upload is being finalized.
475 jQuery('#wpstream_uploaded_mes').html(wpstream_admin_control_vars.completing_upload);
476 // Ask the server to complete/assemble the multipart upload.
477 jQuery.ajax({
478 type: 'POST',
479 url: ajaxurl,
480 dataType: 'json',
481 data: {
482 'action': 'wpstream_complete_multipart_upload',
483 'security': wpstream_admin_control_vars.multipart_upload_nonce,
484 'parts': totalParts,
485 'file_name': file.name,
486 'handle': handle,
487 },
488 success: function(response) {
489 if (response.success) {
490 // Reset multipart data
491 multipartUploadData = null;
492 currentUploadedParts = [];
493
494 // Handle success
495 handleUploadSuccess(file);
496 } else {
497 // Server could not complete the upload: show its reason
498 // (wp_send_json_error puts the string under `data`).
499 handleUploadFailure(typeof response.data === 'string' ? response.data : '');
500 }
501 },
502 error: function(e) {
503 // Transport error during completion.
504 handleUploadFailure();
505 }
506 });
507 }
508
509 /**
510 * Reset the UI and upload state after any upload failure.
511 *
512 * @param {string} [message] - Server-supplied reason; shown as plain text
513 * in place of the generic failure message.
514 * @return {void}
515 */
516 // Handle upload failure
517 function handleUploadFailure(message) {
518 // Remove the unsaved-changes guard and the progress bar.
519 window.onbeforeunload = null;
520 jQuery('.bar').remove();
521 // Show the failure messages. A server reason is inserted as text so
522 // nothing from the response is ever interpreted as HTML.
523 var messageBox = jQuery('#wpstream_uploaded_mes').empty();
524 if (message) {
525 messageBox.text(message);
526 } else {
527 messageBox.html(wpstream_admin_control_vars.upload_failed);
528 }
529 jQuery('#wpstream_label_action').empty().html(wpstream_admin_control_vars.upload_failed2);
530 // Re-enable the upload button and restore its default styling.
531 jQuery('#wpstream_upload').prop('disabled', false);
532 jQuery('label[for="wpstream_upload"]')
533 .css('cursor','')
534 .css('background-color','');
535
536 // Reset multipart upload data
537 multipartUploadData = null;
538 currentUploadedParts = [];
539 }
540
541 /**
542 * Handle a successful upload: reset UI and insert a pending video row.
543 *
544 * Sanitizes the file name to mirror the server's stored name, injects a new
545 * "processing" video entry into the list, and starts pending-status polling.
546 *
547 * @param {File} file - The successfully uploaded file.
548 * @return {void}
549 */
550 // Handle upload success
551 function handleUploadSuccess(file) {
552 // Remove the unsaved-changes guard and the progress bar.
553 window.onbeforeunload = null;
554 jQuery('.bar').remove();
555 // Show the completion messages.
556 jQuery('#wpstream_uploaded_mes').empty().html(wpstream_admin_control_vars.upload_complete);
557 jQuery('#wpstream_label_action').text(wpstream_admin_control_vars.upload_complete2);
558 // Re-enable the upload button and restore its default styling.
559 jQuery('#wpstream_upload').prop('disabled', false);
560 jQuery('label[for="wpstream_upload"]')
561 .css('cursor','')
562 .css('background-color','');
563
564 // Capture the original name and size (in MB) of the file.
565 var new_file_name = file.name;
566 var new_file_size = Math.floor(file.size / 1048576);
567
568 // Sanitize the base name (spaces to underscores, strip non-word chars)
569 // and re-attach the extension, matching the server's stored filename.
570 var new_file_name_array = new_file_name.split(".");
571 var temp_file_name = new_file_name_array[0].split(' ').join('_');
572 temp_file_name = temp_file_name.replace(/\W/g, '');
573 new_file_name = temp_file_name+'.'+new_file_name_array[new_file_name_array.length-1];
574
575 // Build the new video wrapper markup, ending with a "pending" placeholder.
576 var to_insert='<div class="wpstream_video_wrapper"><div class="wpstream_video_title"><div class="wpstream_video_notice"></div></div>';
577 to_insert += `<div class="wpstream_video_title"><strong class="storage_file_name">${wpstream_admin_control_vars.file_name_text}</strong><span class="storage_file_name_real">`+new_file_name+`</span><span class="storage_file_size">` + new_file_size + ` MB</span></div>`;
578 to_insert += `<div class="wpstream_video_pending">${wpstream_admin_control_vars.video_processing}</div>`;
579
580 // Insert the new entry after the list heading.
581 jQuery('#video_management_title').after(to_insert);
582
583 // Begin polling so the pending entry updates once processing completes.
584 WpStreamUtils.checkPendingVideos();
585 }
586
587 // When the WooCommerce product type changes, reveal the "sold individually"
588 // field for WpStream product types.
589 jQuery('#product-type').on('change',function(){
590
591 // Read the newly selected product type.
592 var product_type= jQuery('#product-type').val();
593 // Show the field only for WpStream product types.
594 if(product_type==='live_stream' || product_type==='video_on_demand' || product_type==='wpstream_bundle' ){
595 jQuery('._sold_individually_field').show();
596 }
597
598 });
599
600 // Deep-link support: if arriving with a new VOD name, preselect VOD.
601 if(wpstream_findGetParameter('new_video_name')!=='' && wpstream_findGetParameter('new_video_name')!=null ){
602 jQuery('#product-type').val('video_on_demand').trigger('change');
603 }
604
605 // Deep-link support: if arriving with a new stream flag, preselect live stream.
606 if(wpstream_findGetParameter('new_stream')!=='' && wpstream_findGetParameter('new_stream')!=null ){
607 jQuery('#product-type').val('live_stream').trigger('change');
608 }
609
610 // Reveal the product-type-specific metabox sections for the current type.
611 var product_type= jQuery('#product-type').val();
612 if ( product_type === 'video_on_demand' ) {
613 // VOD-only fields.
614 jQuery('.show_if_video_on_demand' ).show();
615 }else if ( product_type === 'live_stream' ) {
616 // Live-stream-only fields.
617 jQuery( '.show_if_live_stream' ).show();
618 } else if ( product_type === 'wpstream_bundle' ) {
619 // Bundle-only fields, plus force the General tab open.
620 jQuery( '.show_if_wpstream_bundle' ).show();
621 // Trigger clicks to activate the General product data tab.
622 jQuery('.general_tab').trigger('click');
623 $('a[href="#general_product_data"]').click();
624 $('.product_data_tabs .tab.general_tab').click();
625 }
626
627
628
629
630
631 /**
632 * Read a query-string parameter from the current URL.
633 *
634 * @param {string} parameterName - The parameter name to look up.
635 * @return {string|null} The decoded value, or null if not present.
636 */
637 function wpstream_findGetParameter(parameterName) {
638 // Accumulator for the result and a scratch array for each pair.
639 var result = null,
640 tmp = [];
641 // Split the query string into key=value pairs and scan for a match.
642 location.search
643 .substr(1)
644 .split("&")
645 .forEach(function (item) {
646 tmp = item.split("=");
647 // Store the decoded value when the key matches.
648 if (tmp[0] === parameterName) result = decodeURIComponent(tmp[1]);
649 });
650 return result;
651 }
652
653
654 // Toggle the movie-URL field based on the subscription-event selector.
655 jQuery('#_subscript_live_event').change(function(){
656 //alert('move it'+product_type);
657 // For WpStream product types this control is not applicable; do nothing.
658 if ( product_type === 'video_on_demand' || product_type === 'live_stream' || product_type === 'wpstream_bundle' ) {
659
660 }else{
661 // Otherwise show the movie-URL field only when subscription is "no".
662 var value= jQuery(this).val();
663 if(value==="no"){
664 jQuery("._movie_url_field").parent().removeClass("hide_if_subscription").show();
665 }else{
666 jQuery("._movie_url_field").parent().addClass("hide_if_subscription").hide();
667 }
668 }
669 });
670
671 // Apply the subscription-event visibility on initial load.
672 jQuery('#_subscript_live_event').trigger('change');
673
674
675 // Toggle VOD source fields based on the WpStream product-type selector.
676 $('#wpstream_product_type').change(function(){
677 // Hide all source-specific fields first.
678 jQuery('.video_free').hide();
679 jQuery('.video_free_external').hide();
680
681 jQuery('.wpstream_option_vod_source').hide();
682
683 // Value "2": recorded/free video source.
684 if( jQuery('#wpstream_product_type').val()=== "2"){
685 jQuery('.video_free').show();
686 jQuery('.wpstream_show_recording').show();
687 }
688 // Value "3": external video source.
689 if( jQuery('#wpstream_product_type').val()=== "3"){
690 jQuery('.video_free_external').show();
691 jQuery('.wpstream_show_external').show();
692 }
693 });
694 // Apply the product-type visibility on initial load.
695 $('#wpstream_product_type').trigger('change');
696
697
698
699
700
701
702 });
703
704
705
706 /*
707 * Upload images in admin
708 *
709 */
710 /**
711 * Bind WordPress media-library pickers to admin image upload buttons.
712 *
713 * For each configured button id, opens the media picker on click and writes the
714 * chosen image's URL and id into the sibling hidden fields.
715 *
716 * @return {void}
717 */
718 function wpstream_upload_images_in_wpadmin(){
719 // List of upload button ids to wire up.
720 var idList = ["category_featured_image_button"];
721
722 // Attach a click handler to each button in the list.
723 for (var i = 0; i < idList.length; i++) {
724 var currentId = idList[i];
725 jQuery('#'+currentId).on( 'click', function(event) {
726 // Cache the button's parent so we can find its fields.
727 var parent=jQuery(this).parent();
728 // Open the media picker, then store the selected image's url and id.
729 wpstream_admin_return_uploaded_image().then(function(image) {
730 parent.find('.wpstream_landing_upload').val(image.url);
731 parent.find('.wpstream_landing_upload_id').val(image.id);
732
733 });
734
735 });
736
737 }
738 }
739
740
741 /*
742 * return uploaded image
743 *
744 */
745 /**
746 * Open the WordPress media library and resolve with the inserted image.
747 *
748 * @return {Promise<Object>} Resolves with the selected attachment's JSON.
749 */
750 function wpstream_admin_return_uploaded_image(){
751
752 // Wrap the media picker in a promise resolved on image insert.
753 return new Promise(function(resolve, reject) {
754 // Create a media frame for inserting a single image.
755 var mediaUploader = wp.media({
756 frame: "post",
757 state: "insert",
758 multiple: false
759 });
760
761 // On insert, resolve with the chosen attachment's data.
762 mediaUploader.on("insert", function(){
763 var image = mediaUploader.state().get("selection").first().toJSON();
764 resolve(image);
765 });
766
767 // Open the media frame.
768 mediaUploader.open();
769 });
770 }
771
772
773
774
775
776 /*
777 * handle video selection for recording
778 *
779 */
780
781 /**
782 * Bind the media picker to the external/recorded free-video button.
783 *
784 * On click, opens the media library and writes the chosen URL into the
785 * external free-video field.
786 *
787 * @return {void}
788 */
789 function wpstream_handle_video_selection(){
790
791 // When the external free-video button is clicked, pick a media URL.
792 jQuery('#wpstream_free_video_external_button').on( 'click', function(event) {
793 // Cache the button's parent for locating the target field.
794 var parent=jQuery(this).parent();
795 // Open the media picker and store the selected image's URL.
796 wpstream_admin_return_uploaded_image().then(function(image) {
797 parent.find('#wpstream_free_video_external').val(image.url);
798 });
799 });
800 }
801
802 /*
803 * handle caption selection for recording
804 */
805 /**
806 * Bind VTT caption selection and removal for the VOD captions field.
807 *
808 * Opens a media picker restricted to text/vtt files, stores the chosen file's
809 * URL/name, and wires an add/remove toggle for the caption.
810 *
811 * @return {void}
812 */
813 function wpstream_handle_caption_selection(){
814 // Open a VTT-only media picker when the captions button is clicked.
815 jQuery('#wpstream_vod_captions_url_button').on( 'click', function(event) {
816 // Prevent the default button action.
817 event.preventDefault();
818 // Cache the parent container and the button itself.
819 var parent = jQuery(this).parent();
820 var button = jQuery(this);
821
822 // Create a media frame limited to VTT caption files.
823 var mediaUploader = wp.media({
824 title: wpstream_admin_control_vars.select_caption_file,
825 button: {
826 text: 'Select'
827 },
828 multiple: false,
829 library: {
830 type: 'text/vtt'
831 }
832 });
833
834 // On selection, store the caption file and update the display.
835 mediaUploader.on("select", function(){
836 // Grab the chosen attachment's data.
837 var attachment = mediaUploader.state().get("selection").first().toJSON();
838 parent.find('#wpstream_closed_captions_file').val(attachment.url);
839 parent.find('.wpstream_caption_file_display').text(attachment.filename);
840
841 // Hide the select button now that a caption is chosen.
842 button.hide();
843
844 // Add a "remove caption" button if one is not already present.
845 if( parent.find('.wpstream_remove_caption').length === 0 ){
846 parent.append('<input type="button" class="button wpstream_remove_caption" value="' + wpstream_admin_control_vars.remove_button + '" style="margin-left: 5px;" />');
847 }
848 });
849
850 // Open the media frame.
851 mediaUploader.open();
852 });
853
854 // Delegated handler: remove a selected caption and restore the picker button.
855 jQuery(document).on('click', '.wpstream_remove_caption', function(e){
856 // Prevent the default button action.
857 e.preventDefault();
858 // Clear the stored caption URL and display text.
859 var parent = jQuery(this).parent();
860 parent.find('#wpstream_closed_captions_file').val('');
861 parent.find('.wpstream_caption_file_display').text('');
862
863 // Show the caption picker button again.
864 parent.find('#wpstream_vod_captions_url_button').show();
865
866 // Remove this "remove" button.
867 jQuery(this).remove();
868 });
869 }
870
871 /**
872 * Wire the player-logo image upload and remove buttons.
873 *
874 * The upload button opens a (lazily-created) media picker and shows a preview;
875 * the remove button clears the stored value and hides the preview.
876 *
877 * @return {void}
878 */
879 function wpstream_upload_player_logo(){
880 // Shared media frame instance, created on first use.
881 var mediaUploader;
882
883 // Handle upload button click
884 jQuery('.wpstream-upload-image').on('click', function(e) {
885 // Prevent the default button action.
886 e.preventDefault();
887
888 // Resolve the wrapper and its related fields/preview/remove button.
889 var button = jQuery(this);
890 var wrapper = button.closest('.wpstream-image-upload-wrapper');
891 var inputField = wrapper.find('input[type="hidden"]');
892 var previewArea = wrapper.find('.wpstream-image-preview');
893 var removeButton = wrapper.find('.wpstream-remove-image');
894
895 // Create media uploader instance if not already created
896 if (!mediaUploader) {
897 mediaUploader = wp.media({
898 title: wpstream_settings_vars.choose_image_text || 'Choose Image',
899 button: {
900 text: wpstream_settings_vars.select_image_text || 'Select Image'
901 },
902 multiple: false
903 });
904
905 // When image is selected in the media uploader
906 mediaUploader.on('select', function() {
907 // Store the selected image URL and populate the preview.
908 var attachment = mediaUploader.state().get('selection').first().toJSON();
909 inputField.val(attachment.url);
910
911 previewArea.find('img').attr('src', attachment.url);
912 previewArea.show();
913 removeButton.show();
914 });
915 }
916
917 // Open the media uploader
918 mediaUploader.open();
919 });
920
921 // Handle remove button click
922 jQuery('.wpstream-remove-image').on('click', function(e) {
923 // Prevent the default button action.
924 e.preventDefault();
925
926 // Resolve the wrapper and its field/preview.
927 var button = jQuery(this);
928 var wrapper = button.closest('.wpstream-image-upload-wrapper');
929 var inputField = wrapper.find('input[type="hidden"]');
930 var previewArea = wrapper.find('.wpstream-image-preview');
931
932 // Clear the stored logo and hide the preview/remove button.
933 inputField.val('');
934 previewArea.hide();
935 button.hide();
936 });
937 }