Files
KiraTV-APP-NodeCast-Modific…/public/js/pages/Settings.js
T

486 lines
20 KiB
JavaScript

/**
* Settings Page Controller
*/
class SettingsPage {
constructor(app) {
this.app = app;
this.tabs = document.querySelectorAll('.tabs .tab');
this.tabContents = document.querySelectorAll('.tab-content');
this.init();
}
init() {
// Tab switching
this.tabs.forEach(tab => {
tab.addEventListener('click', () => this.switchTab(tab.dataset.tab));
});
// Player settings
this.initPlayerSettings();
// Transcoding settings
this.initTranscodingSettings();
// User management (admin only)
this.initUserManagement();
}
initPlayerSettings() {
const arrowKeysToggle = document.getElementById('setting-arrow-keys');
const overlayDurationInput = document.getElementById('setting-overlay-duration');
const defaultVolumeSlider = document.getElementById('setting-default-volume');
const volumeValueDisplay = document.getElementById('volume-value');
const rememberVolumeToggle = document.getElementById('setting-remember-volume');
const autoPlayNextToggle = document.getElementById('setting-autoplay-next');
// Load current settings
if (this.app.player?.settings) {
arrowKeysToggle.checked = this.app.player.settings.arrowKeysChangeChannel;
overlayDurationInput.value = this.app.player.settings.overlayDuration;
defaultVolumeSlider.value = this.app.player.settings.defaultVolume;
volumeValueDisplay.textContent = this.app.player.settings.defaultVolume + '%';
rememberVolumeToggle.checked = this.app.player.settings.rememberVolume;
autoPlayNextToggle.checked = this.app.player.settings.autoPlayNextEpisode;
}
// Arrow keys toggle
arrowKeysToggle.addEventListener('change', () => {
this.app.player.settings.arrowKeysChangeChannel = arrowKeysToggle.checked;
this.app.player.saveSettings();
});
// Overlay duration
overlayDurationInput.addEventListener('change', () => {
this.app.player.settings.overlayDuration = parseInt(overlayDurationInput.value) || 5;
this.app.player.saveSettings();
});
// Default volume slider
defaultVolumeSlider.addEventListener('input', () => {
const value = defaultVolumeSlider.value;
volumeValueDisplay.textContent = value + '%';
this.app.player.settings.defaultVolume = parseInt(value);
this.app.player.saveSettings();
});
// Remember volume toggle
rememberVolumeToggle.addEventListener('change', () => {
this.app.player.settings.rememberVolume = rememberVolumeToggle.checked;
this.app.player.saveSettings();
});
// Auto-play next episode toggle
autoPlayNextToggle.addEventListener('change', () => {
this.app.player.settings.autoPlayNextEpisode = autoPlayNextToggle.checked;
this.app.player.saveSettings();
});
// EPG refresh interval
const epgRefreshSelect = document.getElementById('epg-refresh-interval');
if (epgRefreshSelect && this.app.player?.settings) {
// Load saved value from player settings
epgRefreshSelect.value = this.app.player.settings.epgRefreshInterval || '24';
// Save on change - server will restart its sync timer via PUT /api/settings
epgRefreshSelect.addEventListener('change', () => {
this.app.player.settings.epgRefreshInterval = epgRefreshSelect.value;
this.app.player.saveSettings();
});
}
// Update last refreshed display
this.updateEpgLastRefreshed();
}
initTranscodingSettings() {
// Encoder settings
const hwEncoderSelect = document.getElementById('setting-hw-encoder');
const maxResolutionSelect = document.getElementById('setting-max-resolution');
const qualitySelect = document.getElementById('setting-quality');
// Stream processing (use -tc suffix IDs from Transcoding tab)
const forceProxyToggle = document.getElementById('setting-force-proxy-tc');
const autoTranscodeToggle = document.getElementById('setting-auto-transcode-tc');
const forceTranscodeToggle = document.getElementById('setting-force-transcode-tc');
const forceVideoTranscodeToggle = document.getElementById('setting-force-video-transcode-tc');
const forceRemuxToggle = document.getElementById('setting-force-remux-tc');
const streamFormatSelect = document.getElementById('setting-stream-format-tc');
// User-Agent (Transcoding tab versions)
const userAgentSelect = document.getElementById('setting-user-agent-tc');
const userAgentCustomInput = document.getElementById('setting-user-agent-custom-tc');
const customUaContainer = document.getElementById('custom-user-agent-container-tc');
// Load current settings
if (this.app.player?.settings) {
const s = this.app.player.settings;
if (hwEncoderSelect) hwEncoderSelect.value = s.hwEncoder || 'auto';
if (maxResolutionSelect) maxResolutionSelect.value = s.maxResolution || '1080p';
if (qualitySelect) qualitySelect.value = s.quality || 'medium';
if (forceProxyToggle) forceProxyToggle.checked = s.forceProxy === true;
if (autoTranscodeToggle) autoTranscodeToggle.checked = s.autoTranscode !== false;
if (forceTranscodeToggle) forceTranscodeToggle.checked = s.forceTranscode === true;
if (forceVideoTranscodeToggle) forceVideoTranscodeToggle.checked = s.forceVideoTranscode === true;
if (forceRemuxToggle) forceRemuxToggle.checked = s.forceRemux || false;
if (streamFormatSelect) streamFormatSelect.value = s.streamFormat || 'm3u8';
if (userAgentSelect) userAgentSelect.value = s.userAgentPreset || 'chrome';
if (userAgentCustomInput) userAgentCustomInput.value = s.userAgentCustom || '';
if (customUaContainer) {
customUaContainer.style.display = userAgentSelect?.value === 'custom' ? 'flex' : 'none';
}
}
// Event listeners for encoder settings
hwEncoderSelect?.addEventListener('change', () => {
this.app.player.settings.hwEncoder = hwEncoderSelect.value;
this.app.player.saveSettings();
});
maxResolutionSelect?.addEventListener('change', () => {
this.app.player.settings.maxResolution = maxResolutionSelect.value;
this.app.player.saveSettings();
});
qualitySelect?.addEventListener('change', () => {
this.app.player.settings.quality = qualitySelect.value;
this.app.player.saveSettings();
});
// Stream processing toggles
forceProxyToggle?.addEventListener('change', () => {
this.app.player.settings.forceProxy = forceProxyToggle.checked;
this.app.player.saveSettings();
});
autoTranscodeToggle?.addEventListener('change', () => {
this.app.player.settings.autoTranscode = autoTranscodeToggle.checked;
this.app.player.saveSettings();
});
forceTranscodeToggle?.addEventListener('change', () => {
this.app.player.settings.forceTranscode = forceTranscodeToggle.checked;
this.app.player.saveSettings();
});
forceVideoTranscodeToggle?.addEventListener('change', () => {
this.app.player.settings.forceVideoTranscode = forceVideoTranscodeToggle.checked;
this.app.player.saveSettings();
});
forceRemuxToggle?.addEventListener('change', () => {
this.app.player.settings.forceRemux = forceRemuxToggle.checked;
this.app.player.saveSettings();
});
streamFormatSelect?.addEventListener('change', () => {
this.app.player.settings.streamFormat = streamFormatSelect.value;
this.app.player.saveSettings();
});
// User-Agent handlers
const toggleCustomInput = () => {
if (customUaContainer) {
customUaContainer.style.display = userAgentSelect?.value === 'custom' ? 'flex' : 'none';
}
};
userAgentSelect?.addEventListener('change', () => {
this.app.player.settings.userAgentPreset = userAgentSelect.value;
this.app.player.saveSettings();
toggleCustomInput();
});
userAgentCustomInput?.addEventListener('change', () => {
this.app.player.settings.userAgentCustom = userAgentCustomInput.value;
this.app.player.saveSettings();
});
}
/**
* Load and display hardware info in Transcoding tab
*/
async loadHardwareInfo() {
const container = document.getElementById('hw-info-container');
if (!container) return;
try {
const response = await fetch('/api/settings/hw-info');
if (!response.ok) throw new Error('Failed to fetch hardware info');
const hwInfo = await response.json();
const detected = [];
// Only show detected hardware
if (hwInfo.nvidia?.available) {
detected.push(`<div class="hw-info-item hw-available">
<span class="hw-badge">✓ NVIDIA</span>
<span class="hw-name">${hwInfo.nvidia.name}</span>
</div>`);
}
if (hwInfo.amf?.available) {
detected.push(`<div class="hw-info-item hw-available">
<span class="hw-badge">✓ AMD</span>
<span class="hw-name">${hwInfo.amf.name || 'Available'}</span>
</div>`);
}
if (hwInfo.qsv?.available) {
detected.push(`<div class="hw-info-item hw-available">
<span class="hw-badge">✓ Intel QSV</span>
<span class="hw-name">Available</span>
</div>`);
}
if (hwInfo.vaapi?.available) {
detected.push(`<div class="hw-info-item hw-available">
<span class="hw-badge">✓ VAAPI</span>
<span class="hw-name">${hwInfo.vaapi.device || 'Available'}</span>
</div>`);
}
let html;
if (detected.length > 0) {
html = `<div class="hw-info-grid">${detected.join('')}</div>`;
html += `<p class="hint" style="margin-top: var(--space-sm);">Recommended encoder: <strong>${hwInfo.recommended}</strong></p>`;
} else {
html = `<p class="hint">No GPU acceleration detected. Using software encoding.</p>`;
}
container.innerHTML = html;
} catch (err) {
console.error('Error loading hardware info:', err);
container.innerHTML = '<p class="hint error">Failed to load hardware info</p>';
}
}
initUserManagement() {
// User tab visibility is handled in show() method
// when currentUser is available
// Handle add user form
const addUserForm = document.getElementById('add-user-form');
if (addUserForm) {
addUserForm.addEventListener('submit', async (e) => {
e.preventDefault();
const username = document.getElementById('new-username').value;
const password = document.getElementById('new-password').value;
const role = document.getElementById('new-role').value;
try {
await API.users.create({ username, password, role });
alert('User created successfully!');
addUserForm.reset();
this.loadUsers();
} catch (err) {
alert('Error creating user: ' + err.message);
}
});
}
}
async loadUsers() {
const userList = document.getElementById('user-list');
if (!userList) return;
try {
const users = await API.users.getAll();
if (users.length === 0) {
userList.innerHTML = '<tr><td colspan="4" class="hint">No users found</td></tr>';
return;
}
userList.innerHTML = users.map(user => `
<tr>
<td>${user.username}</td>
<td><span class="badge badge-${user.role === 'admin' ? 'primary' : 'secondary'}">${user.role}</span></td>
<td>${user.createdAt ? new Date(user.createdAt).toLocaleDateString() : 'N/A'}</td>
<td>
<button class="btn btn-sm btn-secondary" onclick="window.app.pages.settings.editUser(${user.id})">Edit</button>
<button class="btn btn-sm btn-error" onclick="window.app.pages.settings.deleteUser(${user.id}, '${user.username}')">Delete</button>
</td>
</tr>
`).join('');
} catch (err) {
console.error('Error loading users:', err);
userList.innerHTML = '<tr><td colspan="4" class="hint">Error loading users</td></tr>';
}
}
async editUser(userId) {
const username = prompt('Enter new username (leave blank to keep current):');
const password = prompt('Enter new password (leave blank to keep current):');
const role = prompt('Enter role (admin or viewer, leave blank to keep current):');
const updates = {};
if (username) updates.username = username;
if (password) updates.password = password;
if (role) updates.role = role;
if (Object.keys(updates).length === 0) {
alert('No changes made');
return;
}
try {
await API.users.update(userId, updates);
alert('User updated successfully!');
this.loadUsers();
} catch (err) {
alert('Error updating user: ' + err.message);
}
}
async deleteUser(userId, username) {
if (!confirm(`Are you sure you want to delete user "${username}"?`)) {
return;
}
try {
await API.users.delete(userId);
alert('User deleted successfully!');
this.loadUsers();
} catch (err) {
alert('Error deleting user: ' + err.message);
}
}
switchTab(tabName) {
this.tabs.forEach(t => t.classList.toggle('active', t.dataset.tab === tabName));
this.tabContents.forEach(c => c.classList.toggle('active', c.id === `tab-${tabName}`));
// Load content browser when switching to that tab
if (tabName === 'content') {
this.app.sourceManager.loadContentSources();
}
// Load users when switching to users tab
if (tabName === 'users') {
this.loadUsers();
}
// Load hardware info when switching to transcode tab
if (tabName === 'transcode') {
this.loadHardwareInfo();
}
}
async show() {
// Show users tab for admin
if (this.app.currentUser && this.app.currentUser.role === 'admin') {
const usersTab = document.getElementById('users-tab');
if (usersTab) {
usersTab.style.display = 'block';
}
}
// Load sources when page is shown
await this.app.sourceManager.loadSources();
// Refresh ALL player settings from server
if (this.app.player?.settings) {
const s = this.app.player.settings;
// Player settings
const arrowKeysToggle = document.getElementById('setting-arrow-keys');
const overlayDurationInput = document.getElementById('setting-overlay-duration');
const defaultVolumeSlider = document.getElementById('setting-default-volume');
const volumeValueDisplay = document.getElementById('volume-value');
const rememberVolumeToggle = document.getElementById('setting-remember-volume');
const autoPlayNextToggle = document.getElementById('setting-autoplay-next');
const forceProxyToggle = document.getElementById('setting-force-proxy');
const forceTranscodeToggle = document.getElementById('setting-force-transcode');
const forceRemuxToggle = document.getElementById('setting-force-remux');
const autoTranscodeToggle = document.getElementById('setting-auto-transcode');
const epgRefreshSelect = document.getElementById('epg-refresh-interval');
const streamFormatSelect = document.getElementById('setting-stream-format');
if (arrowKeysToggle) arrowKeysToggle.checked = s.arrowKeysChangeChannel;
if (overlayDurationInput) overlayDurationInput.value = s.overlayDuration;
if (defaultVolumeSlider) defaultVolumeSlider.value = s.defaultVolume;
if (volumeValueDisplay) volumeValueDisplay.textContent = s.defaultVolume + '%';
if (rememberVolumeToggle) rememberVolumeToggle.checked = s.rememberVolume;
if (autoPlayNextToggle) autoPlayNextToggle.checked = s.autoPlayNextEpisode;
if (forceProxyToggle) forceProxyToggle.checked = s.forceProxy || false;
if (forceTranscodeToggle) forceTranscodeToggle.checked = s.forceTranscode || false;
if (forceRemuxToggle) forceRemuxToggle.checked = s.forceRemux || false;
if (autoTranscodeToggle) autoTranscodeToggle.checked = s.autoTranscode || false;
if (epgRefreshSelect) epgRefreshSelect.value = s.epgRefreshInterval || '24';
if (streamFormatSelect) streamFormatSelect.value = s.streamFormat || 'm3u8';
// User-Agent settings
const userAgentSelect = document.getElementById('setting-user-agent');
const userAgentCustomInput = document.getElementById('setting-user-agent-custom');
const customUaContainer = document.getElementById('custom-user-agent-container');
if (userAgentSelect) {
userAgentSelect.value = s.userAgentPreset || 'chrome';
if (customUaContainer) {
customUaContainer.style.display = userAgentSelect.value === 'custom' ? 'flex' : 'none';
}
}
if (userAgentCustomInput) userAgentCustomInput.value = s.userAgentCustom || '';
}
// Update EPG last refreshed display
this.updateEpgLastRefreshed();
}
/**
* Update the EPG last refreshed display
*/
async updateEpgLastRefreshed() {
const display = document.getElementById('epg-last-refreshed');
if (!display) return;
try {
// Fetch last sync time from server
const response = await fetch('/api/settings/sync-status');
if (!response.ok) throw new Error('Failed to fetch sync status');
const data = await response.json();
if (data.lastSyncTime) {
const lastRefreshTime = new Date(data.lastSyncTime);
// Format as relative time or absolute
const now = new Date();
const diffMs = now - lastRefreshTime;
const diffMins = Math.floor(diffMs / 60000);
const diffHours = Math.floor(diffMins / 60);
let text;
if (diffMins < 1) {
text = 'Just now';
} else if (diffMins < 60) {
text = `${diffMins} minute${diffMins === 1 ? '' : 's'} ago`;
} else if (diffHours < 24) {
text = `${diffHours} hour${diffHours === 1 ? '' : 's'} ago`;
} else {
// Use absolute time for older refreshes
text = lastRefreshTime.toLocaleString();
}
display.textContent = text;
display.title = lastRefreshTime.toLocaleString(); // Full timestamp on hover
} else {
display.textContent = 'Never';
display.title = 'Sync has not run yet since server started';
}
} catch (err) {
console.error('Error fetching sync status:', err);
display.textContent = 'Unknown';
display.title = 'Could not fetch sync status';
}
}
hide() {
// Page is hidden
}
}
window.SettingsPage = SettingsPage;