Some checks failed
Docker Build & Push / build (push) Has been cancelled
- Added WebLLM service for client-side AI summarization and translation - Improved summary quality (5 sentences, 600 char limit) - Added Vietnamese character detection for proper language labels - Added Copy button for summary content - Key Points now extract conceptual ideas, not transcript excerpts - Removed mini player (scroll-to-minimize) feature - Fixed main.js null container error - Silent WebLLM loading (no overlay/toasts) - Added transcript service with yt-dlp
355 lines
No EOL
11 KiB
HTML
Executable file
355 lines
No EOL
11 KiB
HTML
Executable file
{% extends "layout.html" %}
|
|
|
|
{% block content %}
|
|
<div class="yt-settings-container">
|
|
<h2 class="yt-settings-title">Settings</h2>
|
|
|
|
<!-- Appearance & Playback in one card -->
|
|
<div class="yt-settings-card compact">
|
|
<div class="yt-setting-row">
|
|
<span class="yt-setting-label">Theme</span>
|
|
<div class="yt-toggle-group">
|
|
<button type="button" class="yt-toggle-btn" id="themeBtnLight"
|
|
onclick="setTheme('light')">Light</button>
|
|
<button type="button" class="yt-toggle-btn" id="themeBtnDark" onclick="setTheme('dark')">Dark</button>
|
|
</div>
|
|
</div>
|
|
<div class="yt-setting-row">
|
|
<span class="yt-setting-label">Player</span>
|
|
<div class="yt-toggle-group">
|
|
<button type="button" class="yt-toggle-btn" id="playerBtnArt"
|
|
onclick="setPlayerPref('artplayer')">Artplayer</button>
|
|
<button type="button" class="yt-toggle-btn" id="playerBtnNative"
|
|
onclick="setPlayerPref('native')">Native</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- System Updates -->
|
|
<div class="yt-settings-card">
|
|
<h3>System Updates</h3>
|
|
|
|
<!-- yt-dlp Stable -->
|
|
<div class="yt-update-row">
|
|
<div class="yt-update-info">
|
|
<strong>yt-dlp</strong>
|
|
<span class="yt-update-version" id="ytdlpVersion">Stable</span>
|
|
</div>
|
|
<button id="updateYtdlpStable" onclick="updatePackage('ytdlp', 'stable')" class="yt-update-btn small">
|
|
<i class="fas fa-sync-alt"></i> Update
|
|
</button>
|
|
</div>
|
|
|
|
<!-- yt-dlp Nightly -->
|
|
<div class="yt-update-row">
|
|
<div class="yt-update-info">
|
|
<strong>yt-dlp Nightly</strong>
|
|
<span class="yt-update-version">Experimental</span>
|
|
</div>
|
|
<button id="updateYtdlpNightly" onclick="updatePackage('ytdlp', 'nightly')"
|
|
class="yt-update-btn small nightly">
|
|
<i class="fas fa-flask"></i> Install
|
|
</button>
|
|
</div>
|
|
|
|
<!-- ytfetcher -->
|
|
<div class="yt-update-row">
|
|
<div class="yt-update-info">
|
|
<strong>ytfetcher</strong>
|
|
<span class="yt-update-version" id="ytfetcherVersion">CC & Transcripts</span>
|
|
</div>
|
|
<button id="updateYtfetcher" onclick="updatePackage('ytfetcher', 'latest')" class="yt-update-btn small">
|
|
<i class="fas fa-sync-alt"></i> Update
|
|
</button>
|
|
</div>
|
|
|
|
<div id="updateStatus" class="yt-update-status"></div>
|
|
</div>
|
|
|
|
{% if session.get('user_id') %}
|
|
<div class="yt-settings-card compact">
|
|
<div class="yt-setting-row">
|
|
<span class="yt-setting-label">Display Name</span>
|
|
<form id="profileForm" onsubmit="updateProfile(event)"
|
|
style="display: flex; gap: 8px; flex: 1; max-width: 300px;">
|
|
<input type="text" class="yt-form-input" id="displayName" value="{{ session.username }}" required
|
|
style="flex: 1;">
|
|
<button type="submit" class="yt-update-btn small">Save</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<div class="yt-settings-card compact">
|
|
<div class="yt-setting-row" style="justify-content: center;">
|
|
<span class="yt-about-text">KV-Tube v1.0 • YouTube-like streaming</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<style>
|
|
.yt-settings-container {
|
|
max-width: 500px;
|
|
margin: 0 auto;
|
|
padding: 16px;
|
|
}
|
|
|
|
.yt-settings-title {
|
|
font-size: 20px;
|
|
font-weight: 500;
|
|
margin-bottom: 16px;
|
|
text-align: center;
|
|
}
|
|
|
|
.yt-settings-card {
|
|
background: var(--yt-bg-secondary);
|
|
border-radius: 12px;
|
|
padding: 16px;
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
.yt-settings-card.compact {
|
|
padding: 12px 16px;
|
|
}
|
|
|
|
.yt-settings-card h3 {
|
|
font-size: 14px;
|
|
margin-bottom: 12px;
|
|
color: var(--yt-text-secondary);
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
}
|
|
|
|
.yt-setting-row {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: 8px 0;
|
|
}
|
|
|
|
.yt-setting-row:not(:last-child) {
|
|
border-bottom: 1px solid var(--yt-bg-hover);
|
|
}
|
|
|
|
.yt-setting-label {
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.yt-toggle-group {
|
|
display: flex;
|
|
background: var(--yt-bg-elevated);
|
|
padding: 3px;
|
|
border-radius: 20px;
|
|
gap: 2px;
|
|
}
|
|
|
|
.yt-toggle-btn {
|
|
padding: 6px 14px;
|
|
border-radius: 16px;
|
|
font-size: 13px;
|
|
font-weight: 500;
|
|
color: var(--yt-text-secondary);
|
|
background: transparent;
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
.yt-toggle-btn:hover {
|
|
color: var(--yt-text-primary);
|
|
}
|
|
|
|
.yt-toggle-btn.active {
|
|
background: var(--yt-accent-red);
|
|
color: white;
|
|
}
|
|
|
|
.yt-update-row {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: 10px 0;
|
|
border-bottom: 1px solid var(--yt-bg-hover);
|
|
}
|
|
|
|
.yt-update-row:last-of-type {
|
|
border-bottom: none;
|
|
}
|
|
|
|
.yt-update-info {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 2px;
|
|
}
|
|
|
|
.yt-update-info strong {
|
|
font-size: 14px;
|
|
}
|
|
|
|
.yt-update-version {
|
|
font-size: 11px;
|
|
color: var(--yt-text-secondary);
|
|
}
|
|
|
|
.yt-update-btn {
|
|
background: var(--yt-accent-red);
|
|
color: white;
|
|
padding: 8px 16px;
|
|
border-radius: 20px;
|
|
font-size: 12px;
|
|
font-weight: 500;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
.yt-update-btn.small {
|
|
padding: 6px 12px;
|
|
font-size: 12px;
|
|
}
|
|
|
|
.yt-update-btn.nightly {
|
|
background: #9c27b0;
|
|
}
|
|
|
|
.yt-update-btn:hover {
|
|
opacity: 0.9;
|
|
transform: scale(1.02);
|
|
}
|
|
|
|
.yt-update-btn:disabled {
|
|
background: var(--yt-bg-hover) !important;
|
|
cursor: not-allowed;
|
|
transform: none;
|
|
}
|
|
|
|
.yt-update-status {
|
|
margin-top: 10px;
|
|
font-size: 12px;
|
|
text-align: center;
|
|
min-height: 20px;
|
|
}
|
|
|
|
.yt-form-input {
|
|
background: var(--yt-bg-elevated);
|
|
border: 1px solid var(--yt-bg-hover);
|
|
border-radius: 8px;
|
|
padding: 8px 12px;
|
|
color: var(--yt-text-primary);
|
|
font-size: 13px;
|
|
}
|
|
|
|
.yt-about-text {
|
|
font-size: 12px;
|
|
color: var(--yt-text-secondary);
|
|
}
|
|
</style>
|
|
|
|
<script>
|
|
async function fetchVersions() {
|
|
const pkgs = ['ytdlp', 'ytfetcher'];
|
|
for (const pkg of pkgs) {
|
|
try {
|
|
const res = await fetch(`/api/package/version?package=${pkg}`);
|
|
const data = await res.json();
|
|
if (data.success) {
|
|
const el = document.getElementById(pkg === 'ytdlp' ? 'ytdlpVersion' : 'ytfetcherVersion');
|
|
if (el) {
|
|
el.innerText = `Installed: ${data.version}`;
|
|
// Highlight if nightly
|
|
if (pkg === 'ytdlp' && (data.version.includes('2026') || data.version.includes('.dev'))) {
|
|
el.style.color = '#9c27b0';
|
|
el.innerText += ' (Nightly)';
|
|
}
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.error(e);
|
|
}
|
|
}
|
|
}
|
|
|
|
async function updatePackage(pkg, version) {
|
|
const btnId = pkg === 'ytdlp' ?
|
|
(version === 'nightly' ? 'updateYtdlpNightly' : 'updateYtdlpStable') :
|
|
'updateYtfetcher';
|
|
const btn = document.getElementById(btnId);
|
|
const status = document.getElementById('updateStatus');
|
|
const originalHTML = btn.innerHTML;
|
|
|
|
btn.disabled = true;
|
|
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Updating...';
|
|
status.style.color = 'var(--yt-text-secondary)';
|
|
status.innerText = `Updating ${pkg}${version === 'nightly' ? ' (nightly)' : ''}...`;
|
|
|
|
try {
|
|
const response = await fetch('/api/update_package', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ package: pkg, version: version })
|
|
});
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
status.style.color = '#4caf50';
|
|
status.innerText = '✓ ' + data.message;
|
|
btn.innerHTML = '<i class="fas fa-check"></i> Updated';
|
|
// Refresh versions
|
|
setTimeout(fetchVersions, 1000);
|
|
setTimeout(() => {
|
|
btn.innerHTML = originalHTML;
|
|
btn.disabled = false;
|
|
}, 3000);
|
|
} else {
|
|
status.style.color = '#f44336';
|
|
status.innerText = '✗ ' + data.message;
|
|
btn.innerHTML = originalHTML;
|
|
btn.disabled = false;
|
|
}
|
|
} catch (e) {
|
|
status.style.color = '#f44336';
|
|
status.innerText = '✗ Error: ' + e.message;
|
|
btn.innerHTML = originalHTML;
|
|
btn.disabled = false;
|
|
}
|
|
}
|
|
|
|
// --- Player Preference ---
|
|
window.setPlayerPref = function (type) {
|
|
localStorage.setItem('kv_player_pref', type);
|
|
updatePlayerButtons(type);
|
|
}
|
|
|
|
window.updatePlayerButtons = function (type) {
|
|
const artBtn = document.getElementById('playerBtnArt');
|
|
const natBtn = document.getElementById('playerBtnNative');
|
|
if (artBtn) artBtn.classList.remove('active');
|
|
if (natBtn) natBtn.classList.remove('active');
|
|
if (type === 'native') {
|
|
if (natBtn) natBtn.classList.add('active');
|
|
} else {
|
|
if (artBtn) artBtn.classList.add('active');
|
|
}
|
|
}
|
|
|
|
// Initialize Settings
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
// Theme init
|
|
const currentTheme = localStorage.getItem('theme') || 'dark';
|
|
const lightBtn = document.getElementById('themeBtnLight');
|
|
const darkBtn = document.getElementById('themeBtnDark');
|
|
if (currentTheme === 'light') {
|
|
if (lightBtn) lightBtn.classList.add('active');
|
|
} else {
|
|
if (darkBtn) darkBtn.classList.add('active');
|
|
}
|
|
|
|
// Player init - default to artplayer
|
|
const playerPref = localStorage.getItem('kv_player_pref') || 'artplayer';
|
|
updatePlayerButtons(playerPref);
|
|
|
|
// Fetch versions
|
|
fetchVersions();
|
|
});
|
|
</script>
|
|
{% endblock %} |