// ---- Variables ---- const oAuthBtn = document.getElementById('oauth_btn'); let currentAuthorizationToken = null; let mdbListUI; const servicesRequiringCredentials = ['pikpak',]; const servicesRequiringUrl = ['stremthru']; const servicesNotNeedingDebridProxy = ['stremthru']; const providerSignupLinks = { pikpak: 'https://mypikpak.com/drive/activity/invited?invitation-code=52875535', seedr: 'https://www.seedr.cc/?r=2726511', offcloud: 'https://offcloud.com/?=9932cd9f', realdebrid: ['http://real-debrid.com/?id=9490816', 'http://real-debrid.com/?id=3351376'], debridlink: 'https://debrid-link.com/id/kHgZs', alldebrid: 'https://alldebrid.com/?uid=3ndha&lang=en', torbox: ['https://torbox.app/subscription?referral=339b923e-fb23-40e7-8031-4af39c212e3c', 'https://torbox.app/subscription?referral=e2a28977-99ed-43cd-ba2c-e90dc398c49c', 'https://torbox.app/subscription?referral=38f1c266-8a6c-40b2-a6d2-2148e77dafc9'], easydebrid: 'https://paradise-cloud.com/products/easydebrid', premiumize: 'https://www.premiumize.me', qbittorrent: 'https://github.com/mhdzumair/MediaFusion/tree/main/streaming_providers/qbittorrent#qbittorrent-webdav-setup-options-with-mediafusion', stremthru: 'https://github.com/MunifTanjim/stremthru?tab=readme-ov-file#configuration', }; const providerTokenTooltip = { '*': `Enter Encoded Token previously generated or Click 'Authorize' to generate a new token or Provide your Private Token.`, stremthru: `Enter Credential ('store_name:store_token' or base64 encoded basic token from 'STREMTHRU_PROXY_AUTH' config)`, }; // ---- OAuth-related Functions ---- function generateUniqueToken() { return Date.now().toString() + Math.random().toString(); } async function initiateOAuthFlow(getDeviceCodeUrl, authorizeUrl) { const provider = document.getElementById('provider_service').value; currentAuthorizationToken = generateUniqueToken(); setOAuthBtnTextContent(provider); try { const response = await fetch(getDeviceCodeUrl); const data = await response.json(); if (data.device_code) { document.getElementById('device_code_display').textContent = data.user_code; if (data.verification_url) { const verificationLinkElement = document.getElementById('verification_link'); verificationLinkElement.href = data.verification_url; verificationLinkElement.textContent = data.verification_url; } setElementDisplay('device_code_section', 'block'); checkAuthorization(data.device_code, authorizeUrl); } else { // Reset the button's text if there's an error or if the device code isn't returned setOAuthBtnTextContent(provider); } } catch (error) { console.error('Error fetching device code:', error); alert('An error occurred. Please try again.'); setOAuthBtnTextContent(provider) } } function checkAuthorization(deviceCode, authorizeUrl) { const thisAuthorizationToken = currentAuthorizationToken; setTimeout(async function () { // Exit if the token has changed if (thisAuthorizationToken !== currentAuthorizationToken) { return; } try { const response = await fetch(authorizeUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({device_code: deviceCode}) }); const data = await response.json(); if (data.token) { const tokenInput = document.getElementById('provider_token'); tokenInput.value = data.token; oAuthBtn.disabled = false; tokenInput.disabled = false; setElementDisplay('oauth_section', 'none'); setElementDisplay('device_code_section', 'none'); } else { checkAuthorization(deviceCode, authorizeUrl); } } catch (error) { console.error('Error checking authorization:', error); oAuthBtn.disabled = false; } }, 5000); } // Function to update the file size output display function updateSizeOutput() { const slider = document.getElementById('max_size_slider'); const output = document.getElementById('max_size_output'); const value = slider.value; const maxSize = slider.max; output.textContent = value === maxSize ? 'Unlimited' : formatBytes(value); updateSliderTrack(slider); } // ---- Helper Functions ---- function showNotification(message, type = 'info') { toastr.options = { closeButton: true, newestOnTop: true, progressBar: true, positionClass: "toast-top-center", preventDuplicates: true, onclick: null, showDuration: "300", hideDuration: "1000", timeOut: "5000", extendedTimeOut: "1000", showEasing: "swing", hideEasing: "linear", showMethod: "fadeIn", hideMethod: "fadeOut", }; toastr[type](message); } function setElementDisplay(elementId, displayStatus) { document.getElementById(elementId).style.display = displayStatus; } function changeTooltipContent(elementId, text) { document.getElementById(elementId).dataset.bsOriginalTitle = text; } function validateUrl(url) { // This regex supports domain names, IPv4, and IPv6 addresses const urlPattern = /^(https?:\/\/)?(([a-z0-9]([a-z0-9-]*[a-z0-9])?\.)+[a-z]{2,}|localhost|\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}|\[(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}\])(:?\d+)?(\/[-a-z\d%_.~+]*)*(\?[;&a-z\d%_.~+=-]*)?(\#[-a-z\d_]*)?$/i; return urlPattern.test(url); } // Function to format bytes into a human-readable format function formatBytes(bytes, decimals = 2) { if (bytes === "0") return '0 Bytes'; const k = 1024; const dm = decimals < 0 ? 0 : decimals; const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]; } // Function to update the slider track based on the current value function updateSliderTrack(slider) { const percentage = (slider.value / slider.max) * 100; slider.style.background = `linear-gradient(to right, #4a47a3 ${percentage}%, #ddd ${percentage}%, #ddd 100%)`; } function setOAuthBtnTextContent(provider) { if (currentAuthorizationToken) { oAuthBtn.textContent = 'Authorizing...'; } else { oAuthBtn.textContent = 'Authorize with ' + provider.charAt(0).toUpperCase() + provider.slice(1); } } function adjustOAuthSectionDisplay() { const provider = document.getElementById('provider_service').value; const providerToken = document.getElementById('provider_token').value; setOAuthBtnTextContent(provider) if ((provider === 'seedr' || provider === 'realdebrid' || provider === 'debridlink' || provider === "premiumize") && !providerToken) { setElementDisplay('oauth_section', 'block'); } else { setElementDisplay('oauth_section', 'none'); } } function updateProviderFields(isChangeEvent = false) { const provider = document.getElementById('provider_service').value; const serviceUrlInput = document.getElementById('service_url'); const tokenInput = document.getElementById('provider_token'); const watchlistLabel = document.getElementById('watchlist_label'); if (servicesRequiringUrl.includes(provider)) { setElementDisplay('service_url_section', 'block'); } else { setElementDisplay('service_url_section', 'none'); } if (servicesNotNeedingDebridProxy.includes(provider)) { setElementDisplay('proxy_debrid_streams_section', 'none'); } else { setElementDisplay('proxy_debrid_streams_section', 'block'); } if (provider in providerSignupLinks) { // if providerSignupLinks[provider] is an array, randomly select one of the links if (Array.isArray(providerSignupLinks[provider])) { document.getElementById('signup_link').href = providerSignupLinks[provider][Math.floor(Math.random() * providerSignupLinks[provider].length)]; } else { document.getElementById('signup_link').href = providerSignupLinks[provider]; } setElementDisplay('signup_section', 'block'); } else { setElementDisplay('signup_section', 'none'); } // Toggle visibility of credentials and token input based on provider setElementDisplay('stremthru_config', provider === 'stremthru' ? 'block' : 'none'); if (provider) { if (servicesRequiringCredentials.includes(provider)) { setElementDisplay('credentials', 'block'); setElementDisplay('token_input', 'none'); setElementDisplay('qbittorrent_config', 'none'); } else if (provider === 'qbittorrent') { setElementDisplay('qbittorrent_config', 'block'); setElementDisplay('credentials', 'none'); setElementDisplay('token_input', 'none'); } else { setElementDisplay('credentials', 'none'); setElementDisplay('token_input', 'block'); changeTooltipContent('provider_token_tooltip', providerTokenTooltip[provider] || providerTokenTooltip['*']) setElementDisplay('qbittorrent_config', 'none'); } setElementDisplay('streaming_provider_options', 'block'); watchlistLabel.textContent = `Enable ${provider.charAt(0).toUpperCase() + provider.slice(1)} Watchlist`; } else { setElementDisplay('credentials', 'none'); setElementDisplay('token_input', 'none'); setElementDisplay('streaming_provider_options', 'none'); setElementDisplay('qbittorrent_config', 'none'); } // Reset the fields only if this is triggered by an onchange event if (isChangeEvent) { serviceUrlInput.value = ''; tokenInput.value = ''; tokenInput.disabled = false; currentAuthorizationToken = null; oAuthBtn.disabled = false; setElementDisplay('device_code_section', 'none'); } adjustOAuthSectionDisplay(); } // Function to show loading widget function showLoadingWidget(message = "Processing your configuration...") { const loadingWidget = document.getElementById('loadingWidget'); const loadingMessage = document.getElementById('loadingMessage'); if (loadingMessage) { loadingMessage.textContent = message; } if (loadingWidget) { loadingWidget.style.display = 'flex'; // Prevent background scrolling while loading document.body.style.overflow = 'hidden'; } } // Function to hide loading widget function hideLoadingWidget() { const loadingWidget = document.getElementById('loadingWidget'); if (loadingWidget) { loadingWidget.style.display = 'none'; // Restore background scrolling document.body.style.overflow = ''; } } // Function to get installation URL async function getInstallationUrl(isRedirect = false) { try { showLoadingWidget(); const userData = getUserData(); const existingConfig = document.getElementById('existing_config').value; let urlPrefix = window.location.protocol + "//"; if (isRedirect) { urlPrefix = "stremio://"; } if (!userData) { hideLoadingWidget(); showNotification('Validation failed. Please check your input.', 'error'); return null; } const encryptUrl = '/encrypt-user-data' + (existingConfig ? `/${existingConfig}` : ''); const response = await fetch(encryptUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(userData) }); const data = await response.json(); if (data.status === 'error') { hideLoadingWidget(); showNotification(data.message, 'error'); return null; } if (data.detail) { hideLoadingWidget(); showNotification(data.detail[0].msg, 'error'); return null; } if (!data.encrypted_str) { hideLoadingWidget(); showNotification('An error occurred while encrypting user data', 'error'); return null; } const installationUrl = urlPrefix + window.location.host + "/" + data.encrypted_str + "/manifest.json"; hideLoadingWidget(); return installationUrl; } catch (error) { hideLoadingWidget(); showNotification('An error occurred while encrypting user data', 'error'); console.error('Error encrypting user data:', error); return null; } } function getUserData() { let isValid = true; const provider = document.getElementById('provider_service').value; let streamingProviderData = {}; const validateInput = (elementId, condition) => { const element = document.getElementById(elementId); if (condition) { element.classList.remove('is-invalid'); } else { element.classList.add('is-invalid'); if (isValid) { element.focus(); // Set focus on the first invalid input } isValid = false; } }; // Validate and collect streaming provider data if (provider) { if (servicesRequiringUrl.includes(provider)) { const serviceUrl = document.getElementById('service_url').value.trim(); validateInput('service_url', validateUrl(serviceUrl)); streamingProviderData.url = serviceUrl; } if (provider === 'stremthru') { streamingProviderData.stremthru_store_name = document.getElementById('stremthru_store_name').value.trim() || null; } if (servicesRequiringCredentials.includes(provider)) { validateInput('email', document.getElementById('email').value); validateInput('password', document.getElementById('password').value); streamingProviderData.email = document.getElementById('email').value; streamingProviderData.password = document.getElementById('password').value; } else if (provider === 'qbittorrent') { streamingProviderData.qbittorrent_config = { qbittorrent_url: document.getElementById('qbittorrent_url').value, qbittorrent_username: document.getElementById('qbittorrent_username').value, qbittorrent_password: document.getElementById('qbittorrent_password').value, seeding_time_limit: parseInt(document.getElementById('seeding_time_limit').value, 10), seeding_ratio_limit: parseFloat(document.getElementById('seeding_ratio_limit').value), play_video_after: parseInt(document.getElementById('play_video_after_download').value, 10), category: document.getElementById('category').value, webdav_url: document.getElementById('webdav_url').value, webdav_username: document.getElementById('webdav_username').value, webdav_password: document.getElementById('webdav_password').value, webdav_downloads_path: document.getElementById('webdav_downloads_path').value, }; // Validate qBittorrent-specific inputs validateInput('qbittorrent_url', streamingProviderData.qbittorrent_config.qbittorrent_url); validateInput('seeding_time_limit', !isNaN(streamingProviderData.qbittorrent_config.seeding_time_limit)); validateInput('seeding_ratio_limit', !isNaN(streamingProviderData.qbittorrent_config.seeding_ratio_limit)); validateInput('play_video_after_download', !isNaN(streamingProviderData.qbittorrent_config.play_video_after)); validateInput('category', streamingProviderData.qbittorrent_config.category); } else { validateInput('provider_token', document.getElementById('provider_token').value); streamingProviderData.token = document.getElementById('provider_token').value; } streamingProviderData.service = provider; streamingProviderData.enable_watchlist_catalogs = document.getElementById('enable_watchlist').checked; if (document.getElementById('download_via_browser')) { streamingProviderData.download_via_browser = document.getElementById('download_via_browser').checked; } streamingProviderData.only_show_cached_streams = document.getElementById('only_show_cached_streams').checked; } else { streamingProviderData = null; } const mediaflowEnabled = document.getElementById('enable_mediaflow').checked let mediaflowConfig = null; if (mediaflowEnabled) { mediaflowConfig = { proxy_url: document.getElementById('mediaflow_proxy_url').value, api_password: document.getElementById('mediaflow_api_password').value, public_ip: document.getElementById('mediaflow_public_ip').value, proxy_live_streams: document.getElementById('proxy_live_streams').checked, proxy_debrid_streams: document.getElementById('proxy_debrid_streams').checked }; if (servicesNotNeedingDebridProxy.includes(provider)) { mediaflowConfig.proxy_debrid_streams = false; } validateInput('mediaflow_proxy_url', mediaflowConfig.proxy_url.trim() !== ''); validateInput('mediaflow_api_password', mediaflowConfig.api_password.trim() !== ''); } let rpdbConfig = null; if (document.getElementById('enable_rpdb').checked) { rpdbConfig = { api_key: document.getElementById('rpdb_api_key').value, }; validateInput('rpdb_api_key', rpdbConfig.api_key.trim() !== ''); } let mdblistConfig = null; if (document.getElementById('enable_mdblist').checked) { mdblistConfig = { api_key: document.getElementById('mdblist_api_key').value, lists: mdbListUI.getSelectedListsData() }; } // Collect and validate other user data const maxSizeSlider = document.getElementById('max_size_slider'); const maxSizeValue = maxSizeSlider.value; const maxSize = maxSizeSlider.max; const maxSizeBytes = maxSizeValue === maxSize ? 'inf' : maxSizeValue; const maxStreamsPerResolution = document.getElementById('maxStreamsPerResolution').value; validateInput('maxStreamsPerResolution', maxStreamsPerResolution && !isNaN(maxStreamsPerResolution) && maxStreamsPerResolution > 0); let apiPassword = null; // Check for API Password if authentication is required if (document.getElementById('api_password')) { validateInput('api_password', document.getElementById('api_password').value); apiPassword = document.getElementById('api_password').value; } if (!isValid) { return null; // Return null if validation fails } // Collect sorting options with their directions const selectedSortingOptions = Array.from( document.querySelectorAll('#streamSortOrder .form-check-input:checked') ).map(el => ({ key: el.value, direction: document.getElementById(`direction_${el.value}`).value })); // Collect and return the rest of the user data const torrentDisplayOption = document.querySelector('input[name="torrentDisplayOption"]:checked').value; const showTorrentLanguageFlag = document.getElementById('showTorrentLanguageFlag').checked; // Collect nudity filter data let selectedNudityFilters = Array.from(document.querySelectorAll('input[name="nudity_filter"]:checked')).map(el => el.value); if (selectedNudityFilters.length === 0) { selectedNudityFilters = ['Disable']; } // Collect certification filter data let selectedCertificationFilters = Array.from(document.querySelectorAll('input[name="certification_filter"]:checked')).map(el => el.value); if (selectedCertificationFilters.length === 0) { selectedCertificationFilters = ['Disable']; } // Collect language sorting order const languageSorting = Array.from(document.querySelectorAll('input[name="selected_languages"]:checked')).map(el => el.value || null); // Collect quality filter data const selectedQualityFilters = Array.from(document.querySelectorAll('input[name="quality_filter"]:checked')).map(el => el.value); return { streaming_provider: streamingProviderData, selected_catalogs: Array.from(document.querySelectorAll('input[name="selected_catalogs"]:checked')).map(el => el.value), selected_resolutions: Array.from(document.querySelectorAll('input[name="selected_resolutions"]:checked')).map(el => el.value || null), enable_catalogs: document.getElementById('enable_catalogs').checked, enable_imdb_metadata: document.getElementById('enable_imdb_metadata').checked, max_size: maxSizeBytes, max_streams_per_resolution: maxStreamsPerResolution, torrent_sorting_priority: selectedSortingOptions, show_full_torrent_name: torrentDisplayOption === 'fullName', show_language_country_flag: showTorrentLanguageFlag, nudity_filter: selectedNudityFilters, certification_filter: selectedCertificationFilters, language_sorting: languageSorting, quality_filter: selectedQualityFilters, api_password: apiPassword, mediaflow_config: mediaflowConfig, rpdb_config: rpdbConfig, live_search_streams: document.getElementById('liveSearchStreams').checked, contribution_streams: document.getElementById('contributionStreams').checked, mdblist_config: mdblistConfig, }; } // Function to display the installation URL in a textarea for manual copying function displayFallbackUrl(url) { const container = document.getElementById('fallbackUrlContainer'); const textarea = document.getElementById('fallbackUrl'); textarea.value = url; container.style.display = 'block'; // Make the container visible textarea.focus(); } // Configuration Mode Handling function setConfigMode(mode) { // Update button states document.querySelectorAll('.mode-btn').forEach(btn => { btn.classList.remove('active'); }); document.getElementById(mode + '_mode').classList.add('active'); // Update description const description = document.querySelector('.mode-description'); description.textContent = mode === 'pro' ? 'Pro Mode: Access to all advanced configuration options' : 'Newbie Mode: Quick setup with essential options for new users'; // Toggle visibility of pro sections const proSections = document.querySelectorAll('.pro-mode-section'); proSections.forEach(section => { section.style.display = mode === 'pro' ? 'block' : 'none'; }); // Handle specific settings visibility const streamingSection = document.querySelector('.streaming-preferences'); if (streamingSection) { const advancedOptions = streamingSection.querySelectorAll('.advanced-option'); advancedOptions.forEach(option => { option.style.display = mode === 'pro' ? 'block' : 'none'; }); } // Save preference with error handling try { localStorage.setItem('configMode', mode); } catch (e) { console.warn('Failed to save config mode preference:', e); } } function setupPasswordToggle(passwordInputId, toggleButtonId, toggleIconId) { document.getElementById(toggleButtonId).addEventListener('click', function (_) { const passwordInput = document.getElementById(passwordInputId); const passwordIcon = document.getElementById(toggleIconId); if (passwordInput.type === "password") { passwordInput.type = "text"; passwordIcon.className = "bi bi-eye-slash"; } else { passwordInput.type = "password"; passwordIcon.className = "bi bi-eye"; } }); } // Function to handle configured credential fields function handleConfiguredFields(field, isConfigured = false) { const inputField = document.getElementById(field); const resetBtn = document.createElement('button'); resetBtn.type = 'button'; resetBtn.className = 'btn btn-outline-secondary reset-config-btn'; resetBtn.innerHTML = ''; resetBtn.title = 'Reset Configuration'; if (isConfigured) { inputField.setAttribute('readonly', true); inputField.classList.add('configured-field'); // Add reset button next to the field if (!inputField.nextElementSibling?.classList.contains('reset-config-btn')) { inputField.parentElement.appendChild(resetBtn); } // Handle reset button click resetBtn.onclick = () => { inputField.value = ''; inputField.removeAttribute('readonly'); inputField.classList.remove('configured-field'); resetBtn.remove(); }; } } // Function to initialize configured fields function initConfiguredFields(configuredFields) { const sensitiveFields = [ 'provider_token', 'password', 'qbittorrent_password', 'webdav_password', 'mediaflow_api_password', 'rpdb_api_key' ]; sensitiveFields.forEach(field => { if (configuredFields.includes(field)) { handleConfiguredFields(field, true); } }); } async function initiateKodiSetup() { // Show modal to input Kodi code const kodiCodeModal = new bootstrap.Modal(document.getElementById('kodiCodeModal')); kodiCodeModal.show(); } async function submitKodiCodeAndSetup() { const kodiCode = document.getElementById('kodiCodeInput').value; if (kodiCode && kodiCode.length === 6) { const kodiCodeModal = bootstrap.Modal.getInstance(document.getElementById('kodiCodeModal')); kodiCodeModal.hide(); await setupKodiAddon(kodiCode); } else { showNotification('Please enter a valid 6-digit code.', 'error'); } } async function setupKodiAddon(kodiCode) { showLoadingWidget('Preparing Kodi setup...') const installationUrl = await getInstallationUrl(); if (installationUrl) { showLoadingWidget('Setting up Kodi addon...') try { const response = await fetch('/kodi/associate_manifest', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ code: kodiCode, manifest_url: installationUrl }), }); const data = await response.json(); if (response.ok) { showNotification('Kodi addon setup successful!', 'success'); } else { showNotification(`An error occurred while setting up the Kodi addon. ${data.detail}`, 'error'); } } catch (error) { console.error('Error setting up Kodi addon:', error); showNotification('An error occurred while setting up the Kodi addon.', 'error'); } finally { hideLoadingWidget(); } } } /** * Add select/deselect buttons and search input for a container * * @param {string} containerId - ID of the container element * @param {string} sectionName - Name of the section for labels and placeholders * @param {string} checkboxSelector - CSS selector for checkboxes in the container */ function addSelectDeselectSearch(containerId, sectionName, checkboxSelector) { const container = document.getElementById(containerId); if (!container) return; // Create a controls container const controlsContainer = document.createElement('div'); controlsContainer.className = 'mb-3'; // Create search input const searchGroup = document.createElement('div'); searchGroup.className = 'input-group mb-2'; const searchInput = document.createElement('input'); searchInput.type = 'text'; searchInput.className = 'form-control form-control-sm'; searchInput.placeholder = `Search ${sectionName}...`; searchInput.id = `${containerId}-search`; searchInput.setAttribute('autocomplete', 'off'); const searchClearBtn = document.createElement('button'); searchClearBtn.className = 'btn btn-outline-secondary btn-sm'; searchClearBtn.type = 'button'; searchClearBtn.innerHTML = ''; searchClearBtn.setAttribute('data-bs-toggle', 'tooltip'); searchClearBtn.setAttribute('data-bs-placement', 'top'); searchClearBtn.setAttribute('title', 'Clear Search'); searchGroup.appendChild(searchInput); searchGroup.appendChild(searchClearBtn); // Create button group const buttonGroup = document.createElement('div'); buttonGroup.className = 'btn-group btn-group-sm w-100'; const selectAllBtn = document.createElement('button'); selectAllBtn.type = 'button'; selectAllBtn.className = 'btn btn-outline-primary'; selectAllBtn.innerHTML = ' Select All'; const deselectAllBtn = document.createElement('button'); deselectAllBtn.type = 'button'; deselectAllBtn.className = 'btn btn-outline-secondary'; deselectAllBtn.innerHTML = ' Deselect All'; buttonGroup.appendChild(selectAllBtn); buttonGroup.appendChild(deselectAllBtn); // Add elements to controls container controlsContainer.appendChild(searchGroup); controlsContainer.appendChild(buttonGroup); // Insert controls at the beginning of the container container.parentNode.insertBefore(controlsContainer, container); // Add event listeners selectAllBtn.addEventListener('click', function() { const visibleCheckboxes = getVisibleCheckboxes(container, checkboxSelector); visibleCheckboxes.forEach(checkbox => { checkbox.checked = true; }); }); deselectAllBtn.addEventListener('click', function() { const visibleCheckboxes = getVisibleCheckboxes(container, checkboxSelector); visibleCheckboxes.forEach(checkbox => { checkbox.checked = false; }); }); searchInput.addEventListener('input', function() { filterItems(container, this.value.toLowerCase()); }); searchClearBtn.addEventListener('click', function() { searchInput.value = ''; filterItems(container, ''); }); } /** * Get all visible checkboxes in a container * * @param {HTMLElement} container - Container element * @param {string} checkboxSelector - CSS selector for checkboxes * @returns {HTMLElement[]} Array of visible checkbox elements */ function getVisibleCheckboxes(container, checkboxSelector) { const allCheckboxes = container.querySelectorAll(checkboxSelector); return Array.from(allCheckboxes).filter(checkbox => { const item = getItemContainer(checkbox); return item && !item.classList.contains('d-none'); }); } /** * Get the container element of a checkbox * * @param {HTMLElement} checkbox - Checkbox element * @returns {HTMLElement|null} Container element or null */ function getItemContainer(checkbox) { // Navigate up to find the draggable container or column let parent = checkbox.parentElement; while (parent && !parent.classList.contains('draggable-catalog') && !parent.classList.contains('draggable-language') && !parent.classList.contains('col-12') && !parent.classList.contains('col-md-6') && !parent.classList.contains('col-lg-4')) { parent = parent.parentElement; } return parent; } /** * Filter items in a container based on search text * * @param {HTMLElement} container - Container element * @param {string} searchText - Text to search for */ function filterItems(container, searchText) { // Find all items (columns or containers) const items = container.querySelectorAll('.draggable-catalog, .draggable-language, .col-12.col-md-6.col-lg-4'); if (searchText === '') { // Show all items if search is empty items.forEach(item => { item.classList.remove('d-none'); }); } else { // Show/hide items based on label text items.forEach(item => { const label = item.querySelector('label'); if (label && label.textContent.toLowerCase().includes(searchText)) { item.classList.remove('d-none'); } else { item.classList.add('d-none'); } }); } } // ---- Event Listeners ---- document.getElementById('provider_token').addEventListener('input', function () { adjustOAuthSectionDisplay(); }); document.getElementById('enable_mediaflow').addEventListener('change', function () { setElementDisplay('mediaflow_config', this.checked ? 'block' : 'none'); }); document.getElementById('enable_rpdb').addEventListener('change', function () { setElementDisplay('rpdb_config', this.checked ? 'block' : 'none'); }); document.getElementById('enable_mdblist').addEventListener('change', function() { setElementDisplay('mdblist_config', this.checked ? 'block' : 'none'); }); // Event listener for the slider document.getElementById('max_size_slider').addEventListener('input', updateSizeOutput); oAuthBtn.addEventListener('click', async function () { const provider = document.getElementById('provider_service').value; oAuthBtn.disabled = true; document.getElementById('provider_token').disabled = true; if (provider === 'seedr') { await initiateOAuthFlow('/streaming_provider/seedr/get-device-code', '/streaming_provider/seedr/authorize'); } else if (provider === 'realdebrid') { await initiateOAuthFlow('/streaming_provider/realdebrid/get-device-code', '/streaming_provider/realdebrid/authorize'); } else if (provider === 'debridlink') { await initiateOAuthFlow('/streaming_provider/debridlink/get-device-code', '/streaming_provider/debridlink/authorize') } else if (provider === 'premiumize') { return window.location.href = "/streaming_provider/premiumize/authorize"; } }); document.getElementById('configForm').addEventListener('submit', async function (event) { event.preventDefault(); showLoadingWidget('Preparing Stremio installation...'); const installationUrl = await getInstallationUrl(true); if (installationUrl) { window.location.href = installationUrl; } }); document.getElementById('shareBtn').addEventListener('click', async function (event) { event.preventDefault(); showLoadingWidget('Preparing share URL...'); const manifestUrl = await getInstallationUrl(); if (manifestUrl) { try { await navigator.share({ title: 'MediaFusion Addon Manifest', url: manifestUrl, }); showNotification('Manifest URL shared successfully. Do not share this URL with unknown persons.', 'success'); } catch (error) { displayFallbackUrl(manifestUrl); showNotification('Unable to use Share API. URL is ready to be copied manually.', 'info'); } } }); document.getElementById('copyBtn').addEventListener('click', async function (event) { event.preventDefault(); showLoadingWidget('Generating installation URL...'); const manifestUrl = await getInstallationUrl(); if (manifestUrl) { try { await navigator.clipboard.writeText(manifestUrl); showNotification('Manifest URL copied to clipboard. Do not share this URL with unknown persons.', 'success'); } catch (error) { displayFallbackUrl(manifestUrl); showNotification('Unable to access clipboard. URL is ready to be copied manually.', 'info'); } } }); // ---- Initial Setup ---- document.addEventListener('DOMContentLoaded', function () { mdbListUI = new MDBListUI(); updateProviderFields(); updateSizeOutput(); }); document.addEventListener('DOMContentLoaded', function () { let tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]')) let tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) { return new bootstrap.Tooltip(tooltipTriggerEl) }); if (navigator.share) { document.getElementById('shareBtn').style.display = 'block'; } else { document.getElementById('copyBtn').style.display = 'block'; } setupPasswordToggle('password', 'togglePassword', 'togglePasswordIcon'); // Check for API Password if authentication is required if (document.getElementById('api_password')) { setupPasswordToggle('api_password', 'toggleApiPassword', 'toggleApiPasswordIcon'); } setupPasswordToggle('qbittorrent_password', 'toggleQbittorrentPassword', 'toggleQbittorrentPasswordIcon'); setupPasswordToggle('webdav_password', 'toggleWebdavPassword', 'toggleWebdavPasswordIcon'); setupPasswordToggle('mediaflow_api_password', 'toggleMediaFlowPassword', 'toggleMediaFlowPasswordIcon'); setupPasswordToggle('rpdb_api_key', 'toggleRPDBApiKey', 'toggleRPDBApiKeyIcon'); setupPasswordToggle('mdblist_api_key', 'toggleMDBListApiKey', 'toggleMDBListApiKeyIcon'); }); document.addEventListener('DOMContentLoaded', function () { // Initialize sort direction toggles const toggleButtons = document.querySelectorAll('.sort-direction-toggle'); toggleButtons.forEach(button => { button.addEventListener('click', function () { const sortId = this.dataset.sortId; const directionInput = document.getElementById(`direction_${sortId}`); const currentDirection = directionInput.value; const newDirection = currentDirection === 'desc' ? 'asc' : 'desc'; // Update direction directionInput.value = newDirection; // Update button text and icon const iconContainer = this.querySelector('.sort-text'); const tooltipTexts = this.attributes['sorting_info'].value.split('|'); const newTooltipText = tooltipTexts[newDirection === 'asc' ? 1 : 0]; this.title = newTooltipText; this.dataset.bsOriginalTitle = newTooltipText; iconContainer.innerHTML = ` ${newTooltipText} `; }); }); // Enable/disable sort direction buttons based on checkbox state const sortCheckboxes = document.querySelectorAll('[name="selected_sorting_options"]'); sortCheckboxes.forEach(checkbox => { checkbox.addEventListener('change', function () { const sortId = this.value; const toggleButton = document.querySelector(`[data-sort-id="${sortId}"]`); const sortItem = this.closest('.sort-item'); toggleButton.disabled = !this.checked; if (this.checked) { sortItem.classList.add('active'); } else { sortItem.classList.remove('active'); } }); }); }); document.addEventListener('DOMContentLoaded', function () { // Initialize Sortable on the catalog container new Sortable(document.getElementById('catalogs'), { handle: '.draggable-catalog', animation: 150, ghostClass: 'sortable-ghost', dragClass: 'sortable-drag', delay: 200, delayOnTouchOnly: true, filter: '.form-check-input', preventOnFilter: false, }); new Sortable(document.getElementById('streamSortOrder'), { handle: '.sortable-list', animation: 150, ghostClass: 'sortable-ghost', dragClass: 'sortable-drag', delay: 200, delayOnTouchOnly: true, filter: '.form-check-input', preventOnFilter: false, }); new Sortable(document.getElementById('languageSortOrder'), { handle: '.draggable-language', animation: 150, ghostClass: 'sortable-ghost', dragClass: 'sortable-drag', delay: 200, delayOnTouchOnly: true, filter: '.form-check-input', preventOnFilter: false, }); }); // Add event listeners for mode switching document.addEventListener('DOMContentLoaded', function () { let storedMode = 'newbie'; try { const savedMode = localStorage.getItem('configMode'); if (savedMode) { storedMode = savedMode; } } catch (e) { console.warn('Failed to read config mode preference:', e); } setConfigMode(storedMode); }); document.addEventListener('DOMContentLoaded', function () { const kodiSetupBtn = document.getElementById('kodiSetupBtn'); if (kodiSetupBtn) { kodiSetupBtn.addEventListener('click', initiateKodiSetup); } const submitKodiCode = document.getElementById('submitKodiCode'); if (submitKodiCode) { submitKodiCode.addEventListener('click', submitKodiCodeAndSetup); } }); document.addEventListener('DOMContentLoaded', function () { // Initialize configured fields if they exist const configuredFields = JSON.parse(document.getElementById('configured_fields')?.value || '[]'); initConfiguredFields(configuredFields); // Show or hide the language sort section based on the sorting options document.querySelectorAll('input[name="selected_sorting_options"]').forEach(checkbox => { checkbox.addEventListener('change', function () { const languageSortSection = document.getElementById('languageSortSection'); if (document.querySelector('input[name="selected_sorting_options"][value="language"]').checked) { languageSortSection.style.display = 'block'; } else { languageSortSection.style.display = 'none'; } }); }); // Initial check to show/hide the language sort section if (document.querySelector('input[name="selected_sorting_options"][value="language"]').checked) { document.getElementById('languageSortSection').style.display = 'block'; } // Add UI elements for catalogs addSelectDeselectSearch('catalogs', 'Catalogs', 'input[name="selected_catalogs"]'); // Add UI elements for languages addSelectDeselectSearch('languageSortOrder', 'Languages', 'input[name="selected_languages"]'); // Initialize the parental guide checkboxes const parentalGuideCheckboxes = document.querySelectorAll('.parental-guide-checkbox'); parentalGuideCheckboxes.forEach(checkbox => { checkbox.addEventListener('change', function () { const category = this.name; if (this.value === 'Disable' && this.checked) { parentalGuideCheckboxes.forEach(cb => { if (cb !== this && cb.name === category) { cb.checked = false; cb.parentNode.classList.add('disabled-checkbox'); } }); } else if (this.value === 'Disable' && !this.checked) { parentalGuideCheckboxes.forEach(cb => { if (cb.name === category) { cb.parentNode.classList.remove('disabled-checkbox'); } }); } else if (this.checked) { const disableCheckbox = document.querySelector(`input[name="${category}"][value="Disable"]`); if (disableCheckbox) { disableCheckbox.checked = false; disableCheckbox.parentNode.classList.add('disabled-checkbox'); } } else { const anyChecked = Array.from(parentalGuideCheckboxes).some(cb => cb.checked && cb.name === category); if (!anyChecked) { const disableCheckbox = document.querySelector(`input[name="${category}"][value="Disable"]`); if (disableCheckbox) { disableCheckbox.parentNode.classList.remove('disabled-checkbox'); } } } }); }); });