enhanced library write all modal and confirmation dialog
This commit is contained in:
parent
6c4de45b32
commit
e8df863205
4 changed files with 497 additions and 17 deletions
105
web_server.py
105
web_server.py
|
|
@ -9329,6 +9329,111 @@ def get_track_tag_preview(track_id):
|
||||||
return jsonify({"success": False, "error": str(e)}), 500
|
return jsonify({"success": False, "error": str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/library/tracks/tag-preview-batch', methods=['POST'])
|
||||||
|
def get_batch_tag_preview():
|
||||||
|
"""Read current file tags and compare against DB metadata for multiple tracks."""
|
||||||
|
try:
|
||||||
|
from core.tag_writer import read_file_tags, build_tag_diff
|
||||||
|
data = request.get_json()
|
||||||
|
track_ids = data.get('track_ids', [])
|
||||||
|
if not track_ids:
|
||||||
|
return jsonify({"success": False, "error": "No track IDs provided"}), 400
|
||||||
|
|
||||||
|
database = get_database()
|
||||||
|
conn = database._get_connection()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
placeholders = ','.join('?' for _ in track_ids)
|
||||||
|
cursor.execute(f"""
|
||||||
|
SELECT t.*, a.name as artist_name, al.title as album_title,
|
||||||
|
al.year, al.genres as album_genres, al.track_count,
|
||||||
|
al.thumb_url as album_thumb_url, a.thumb_url as artist_thumb_url
|
||||||
|
FROM tracks t
|
||||||
|
JOIN artists a ON t.artist_id = a.id
|
||||||
|
JOIN albums al ON t.album_id = al.id
|
||||||
|
WHERE t.id IN ({placeholders})
|
||||||
|
""", [str(tid) for tid in track_ids])
|
||||||
|
rows = [dict(r) for r in cursor.fetchall()]
|
||||||
|
|
||||||
|
results = []
|
||||||
|
for track_data in rows:
|
||||||
|
track_id = track_data['id']
|
||||||
|
file_path = track_data.get('file_path')
|
||||||
|
resolved_path = _resolve_library_file_path(file_path)
|
||||||
|
|
||||||
|
entry = {
|
||||||
|
'track_id': track_id,
|
||||||
|
'title': track_data.get('title', 'Unknown'),
|
||||||
|
'track_number': track_data.get('track_number'),
|
||||||
|
}
|
||||||
|
|
||||||
|
if not resolved_path:
|
||||||
|
entry['error'] = 'File not found'
|
||||||
|
results.append(entry)
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
file_tags = read_file_tags(resolved_path)
|
||||||
|
if file_tags.get('error'):
|
||||||
|
entry['error'] = file_tags['error']
|
||||||
|
results.append(entry)
|
||||||
|
continue
|
||||||
|
|
||||||
|
album_genres = []
|
||||||
|
if track_data.get('album_genres'):
|
||||||
|
try:
|
||||||
|
parsed = json.loads(track_data['album_genres'])
|
||||||
|
album_genres = parsed if isinstance(parsed, list) else [str(parsed)]
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
album_genres = [g.strip() for g in track_data['album_genres'].split(',') if g.strip()]
|
||||||
|
|
||||||
|
db_data = {
|
||||||
|
'title': track_data.get('title'),
|
||||||
|
'artist_name': track_data.get('artist_name'),
|
||||||
|
'album_title': track_data.get('album_title'),
|
||||||
|
'year': track_data.get('year'),
|
||||||
|
'genres': album_genres,
|
||||||
|
'track_number': track_data.get('track_number'),
|
||||||
|
'disc_number': track_data.get('disc_number'),
|
||||||
|
'bpm': track_data.get('bpm'),
|
||||||
|
'track_count': track_data.get('track_count'),
|
||||||
|
'thumb_url': track_data.get('album_thumb_url') or track_data.get('artist_thumb_url'),
|
||||||
|
}
|
||||||
|
|
||||||
|
diff = build_tag_diff(file_tags, db_data)
|
||||||
|
has_changes = any(d['changed'] for d in diff)
|
||||||
|
changed_fields = [d for d in diff if d['changed']]
|
||||||
|
|
||||||
|
entry['diff'] = diff
|
||||||
|
entry['has_changes'] = has_changes
|
||||||
|
entry['changed_count'] = len(changed_fields)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
entry['error'] = str(e)
|
||||||
|
|
||||||
|
results.append(entry)
|
||||||
|
|
||||||
|
# Server type info
|
||||||
|
active_server = config_manager.get_active_media_server()
|
||||||
|
server_connected = False
|
||||||
|
if active_server == 'plex':
|
||||||
|
server_connected = plex_client.is_connected()
|
||||||
|
elif active_server == 'jellyfin':
|
||||||
|
server_connected = jellyfin_client.is_connected()
|
||||||
|
elif active_server == 'navidrome':
|
||||||
|
server_connected = navidrome_client.is_connected()
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
"success": True,
|
||||||
|
"tracks": results,
|
||||||
|
"server_type": active_server if server_connected else None,
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Batch tag preview error: {e}")
|
||||||
|
return jsonify({"success": False, "error": str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
@app.route('/api/library/track/<track_id>/write-tags', methods=['POST'])
|
@app.route('/api/library/track/<track_id>/write-tags', methods=['POST'])
|
||||||
def write_track_tags(track_id):
|
def write_track_tags(track_id):
|
||||||
"""Write DB metadata into the audio file tags for a single track."""
|
"""Write DB metadata into the audio file tags for a single track."""
|
||||||
|
|
|
||||||
|
|
@ -2598,6 +2598,34 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Batch Tag Preview Modal -->
|
||||||
|
<div class="modal-overlay hidden" id="batch-tag-preview-overlay">
|
||||||
|
<div class="enhanced-bulk-modal batch-tag-preview-modal">
|
||||||
|
<div class="enhanced-bulk-modal-header">
|
||||||
|
<h3 id="batch-tag-preview-title">Write Tags</h3>
|
||||||
|
<button class="enhanced-bulk-modal-close" onclick="closeBatchTagPreviewModal()">×</button>
|
||||||
|
</div>
|
||||||
|
<div id="batch-tag-preview-summary"></div>
|
||||||
|
<div class="enhanced-bulk-modal-body batch-tag-preview-body" id="batch-tag-preview-body">
|
||||||
|
<!-- Populated dynamically -->
|
||||||
|
</div>
|
||||||
|
<div class="enhanced-bulk-modal-footer">
|
||||||
|
<label class="tag-preview-cover-label">
|
||||||
|
<input type="checkbox" id="batch-tag-preview-embed-cover" checked>
|
||||||
|
Embed cover art
|
||||||
|
</label>
|
||||||
|
<label class="tag-preview-cover-label hidden" id="batch-tag-preview-sync-label">
|
||||||
|
<input type="checkbox" id="batch-tag-preview-sync-server" checked>
|
||||||
|
<span id="batch-tag-preview-sync-text">Sync to server</span>
|
||||||
|
</label>
|
||||||
|
<div class="tag-preview-footer-actions">
|
||||||
|
<button class="enhanced-bulk-btn secondary" onclick="closeBatchTagPreviewModal()">Cancel</button>
|
||||||
|
<button class="enhanced-bulk-btn primary" id="batch-tag-preview-write-btn" onclick="executeBatchWriteTags()">Write Tags</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Discover Page -->
|
<!-- Discover Page -->
|
||||||
<div class="page" id="discover-page">
|
<div class="page" id="discover-page">
|
||||||
<div class="discover-container">
|
<div class="discover-container">
|
||||||
|
|
|
||||||
|
|
@ -35130,7 +35130,7 @@ function sortEnhancedTracks(album, field, ascending) {
|
||||||
|
|
||||||
async function deleteLibraryTrack(trackId, albumId) {
|
async function deleteLibraryTrack(trackId, albumId) {
|
||||||
cancelInlineEdit();
|
cancelInlineEdit();
|
||||||
if (!confirm('Delete this track from the library? (File on disk is not affected)')) return;
|
if (!await showConfirmDialog({ title: 'Delete Track', message: 'Delete this track from the library? (File on disk is not affected)', confirmText: 'Delete', destructive: true })) return;
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/library/track/${trackId}`, { method: 'DELETE' });
|
const response = await fetch(`/api/library/track/${trackId}`, { method: 'DELETE' });
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
|
|
@ -35151,7 +35151,7 @@ async function deleteLibraryTrack(trackId, albumId) {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteLibraryAlbum(albumId) {
|
async function deleteLibraryAlbum(albumId) {
|
||||||
if (!confirm('Delete this album and all its tracks from the library? (Files on disk are not affected)')) return;
|
if (!await showConfirmDialog({ title: 'Delete Album', message: 'Delete this album and all its tracks from the library? (Files on disk are not affected)', confirmText: 'Delete', destructive: true })) return;
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/library/album/${albumId}`, { method: 'DELETE' });
|
const response = await fetch(`/api/library/album/${albumId}`, { method: 'DELETE' });
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
|
|
@ -36060,26 +36060,179 @@ async function writeAlbumTags(albumId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const serverType = artistDetailPageState.serverType;
|
await showBatchTagPreview(tracks.map(t => t.id), album.title);
|
||||||
const canSync = serverType && serverType !== 'navidrome';
|
|
||||||
const serverLabel = serverType === 'plex' ? 'Plex' : serverType === 'jellyfin' ? 'Jellyfin' : '';
|
|
||||||
let msg = `Write DB metadata to file tags for all ${tracks.length} tracks in "${album.title}"?`;
|
|
||||||
if (canSync) msg += `\n\nThis will also sync changes to ${serverLabel}.`;
|
|
||||||
if (!confirm(msg)) return;
|
|
||||||
await _startBatchWriteTags(tracks.map(t => t.id), true, canSync);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function batchWriteTagsSelected() {
|
async function batchWriteTagsSelected() {
|
||||||
const trackIds = Array.from(artistDetailPageState.selectedTracks);
|
const trackIds = Array.from(artistDetailPageState.selectedTracks);
|
||||||
if (trackIds.length === 0) return;
|
if (trackIds.length === 0) return;
|
||||||
|
|
||||||
const serverType = artistDetailPageState.serverType;
|
await showBatchTagPreview(trackIds, null);
|
||||||
const canSync = serverType && serverType !== 'navidrome';
|
}
|
||||||
const serverLabel = serverType === 'plex' ? 'Plex' : serverType === 'jellyfin' ? 'Jellyfin' : '';
|
|
||||||
let msg = `Write DB metadata to file tags for ${trackIds.length} selected track(s)?`;
|
async function showBatchTagPreview(trackIds, albumTitle) {
|
||||||
if (canSync) msg += `\n\nThis will also sync changes to ${serverLabel}.`;
|
const overlay = document.getElementById('batch-tag-preview-overlay');
|
||||||
if (!confirm(msg)) return;
|
const body = document.getElementById('batch-tag-preview-body');
|
||||||
await _startBatchWriteTags(trackIds, true, canSync);
|
const titleEl = document.getElementById('batch-tag-preview-title');
|
||||||
|
const summary = document.getElementById('batch-tag-preview-summary');
|
||||||
|
const writeBtn = document.getElementById('batch-tag-preview-write-btn');
|
||||||
|
if (!overlay || !body) return;
|
||||||
|
|
||||||
|
titleEl.textContent = albumTitle ? `Write Tags — ${albumTitle}` : `Write Tags — ${trackIds.length} Tracks`;
|
||||||
|
body.innerHTML = '<div class="tag-preview-loading">Loading tag previews...</div>';
|
||||||
|
summary.innerHTML = '';
|
||||||
|
writeBtn.disabled = true;
|
||||||
|
overlay.classList.remove('hidden');
|
||||||
|
|
||||||
|
// Hide sync checkbox until we know server type
|
||||||
|
const syncLabel = document.getElementById('batch-tag-preview-sync-label');
|
||||||
|
if (syncLabel) syncLabel.classList.add('hidden');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/library/tracks/tag-preview-batch', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ track_ids: trackIds })
|
||||||
|
});
|
||||||
|
const result = await response.json();
|
||||||
|
if (!result.success) {
|
||||||
|
body.innerHTML = `<div class="tag-preview-error">${escapeHtml(result.error)}</div>`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tracks = result.tracks || [];
|
||||||
|
const serverType = result.server_type || null;
|
||||||
|
|
||||||
|
// Show sync checkbox if server connected
|
||||||
|
if (syncLabel && serverType && serverType !== 'navidrome') {
|
||||||
|
const syncText = document.getElementById('batch-tag-preview-sync-text');
|
||||||
|
if (syncText) syncText.textContent = `Sync to ${serverType === 'plex' ? 'Plex' : 'Jellyfin'}`;
|
||||||
|
syncLabel.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Categorize tracks
|
||||||
|
const withChanges = tracks.filter(t => t.has_changes);
|
||||||
|
const noChanges = tracks.filter(t => !t.error && !t.has_changes);
|
||||||
|
const errors = tracks.filter(t => t.error);
|
||||||
|
|
||||||
|
// Summary bar
|
||||||
|
let summaryHtml = '<div class="batch-tag-summary">';
|
||||||
|
if (withChanges.length > 0) summaryHtml += `<span class="batch-tag-stat changed">${withChanges.length} with changes</span>`;
|
||||||
|
if (noChanges.length > 0) summaryHtml += `<span class="batch-tag-stat unchanged">${noChanges.length} unchanged</span>`;
|
||||||
|
if (errors.length > 0) summaryHtml += `<span class="batch-tag-stat errored">${errors.length} unavailable</span>`;
|
||||||
|
summaryHtml += '</div>';
|
||||||
|
summary.innerHTML = summaryHtml;
|
||||||
|
|
||||||
|
// Build track accordion
|
||||||
|
let html = '';
|
||||||
|
|
||||||
|
// Tracks with changes (expanded by default)
|
||||||
|
withChanges.forEach(track => {
|
||||||
|
html += _renderBatchTrackDiff(track, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Errors
|
||||||
|
errors.forEach(track => {
|
||||||
|
html += `<div class="batch-tag-track error">`;
|
||||||
|
html += `<div class="batch-tag-track-header">`;
|
||||||
|
html += `<span class="batch-tag-track-number">${track.track_number || '—'}</span>`;
|
||||||
|
html += `<span class="batch-tag-track-title">${escapeHtml(track.title)}</span>`;
|
||||||
|
html += `<span class="batch-tag-track-status error">${escapeHtml(track.error)}</span>`;
|
||||||
|
html += `</div></div>`;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Unchanged tracks (collapsed)
|
||||||
|
if (noChanges.length > 0) {
|
||||||
|
html += `<div class="batch-tag-unchanged-group">`;
|
||||||
|
html += `<div class="batch-tag-unchanged-header" onclick="this.parentElement.classList.toggle('expanded')">`;
|
||||||
|
html += `<span>${noChanges.length} track${noChanges.length !== 1 ? 's' : ''} already up to date</span>`;
|
||||||
|
html += `<span class="batch-tag-chevron">▾</span>`;
|
||||||
|
html += `</div>`;
|
||||||
|
html += `<div class="batch-tag-unchanged-list">`;
|
||||||
|
noChanges.forEach(track => {
|
||||||
|
html += `<div class="batch-tag-track-row unchanged">`;
|
||||||
|
html += `<span class="batch-tag-track-number">${track.track_number || '—'}</span>`;
|
||||||
|
html += `<span class="batch-tag-track-title">${escapeHtml(track.title)}</span>`;
|
||||||
|
html += `<span class="batch-tag-track-status ok">✓ Tags match</span>`;
|
||||||
|
html += `</div>`;
|
||||||
|
});
|
||||||
|
html += `</div></div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (withChanges.length === 0 && errors.length === 0) {
|
||||||
|
html += '<div class="tag-preview-no-changes">All file tags already match DB metadata</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
body.innerHTML = html;
|
||||||
|
|
||||||
|
// Store state for write action
|
||||||
|
overlay._batchTrackIds = trackIds;
|
||||||
|
overlay._batchServerType = serverType;
|
||||||
|
writeBtn.disabled = withChanges.length === 0;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
body.innerHTML = `<div class="tag-preview-error">Failed to load previews: ${escapeHtml(error.message)}</div>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function _renderBatchTrackDiff(track, expanded) {
|
||||||
|
let html = `<div class="batch-tag-track${expanded ? ' expanded' : ''}">`;
|
||||||
|
html += `<div class="batch-tag-track-header" onclick="this.parentElement.classList.toggle('expanded')">`;
|
||||||
|
html += `<span class="batch-tag-track-number">${track.track_number || '—'}</span>`;
|
||||||
|
html += `<span class="batch-tag-track-title">${escapeHtml(track.title)}</span>`;
|
||||||
|
html += `<span class="batch-tag-track-status changed">${track.changed_count} field${track.changed_count !== 1 ? 's' : ''} changed</span>`;
|
||||||
|
html += `<span class="batch-tag-chevron">▾</span>`;
|
||||||
|
html += `</div>`;
|
||||||
|
html += `<div class="batch-tag-track-diff">`;
|
||||||
|
html += '<table class="tag-preview-table"><thead><tr>';
|
||||||
|
html += '<th>Field</th><th>Current File</th><th></th><th>New Value</th>';
|
||||||
|
html += '</tr></thead><tbody>';
|
||||||
|
|
||||||
|
(track.diff || []).forEach(d => {
|
||||||
|
if (!d.changed) return; // Only show changed fields in batch view
|
||||||
|
html += `<tr class="tag-diff-changed">`;
|
||||||
|
html += `<td class="tag-field-name">${d.field}</td>`;
|
||||||
|
html += `<td class="tag-file-value">${escapeHtml(d.file_value) || '<span class="tag-empty">empty</span>'}</td>`;
|
||||||
|
html += `<td class="tag-diff-indicator"><span class="tag-diff-arrow">→</span></td>`;
|
||||||
|
html += `<td class="tag-db-value">${escapeHtml(d.db_value) || '<span class="tag-empty">empty</span>'}</td>`;
|
||||||
|
html += '</tr>';
|
||||||
|
});
|
||||||
|
|
||||||
|
html += '</tbody></table></div></div>';
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeBatchTagPreviewModal() {
|
||||||
|
const overlay = document.getElementById('batch-tag-preview-overlay');
|
||||||
|
if (overlay) {
|
||||||
|
overlay.classList.add('hidden');
|
||||||
|
overlay._batchTrackIds = null;
|
||||||
|
overlay._batchServerType = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function executeBatchWriteTags() {
|
||||||
|
const overlay = document.getElementById('batch-tag-preview-overlay');
|
||||||
|
const trackIds = overlay?._batchTrackIds;
|
||||||
|
if (!trackIds || trackIds.length === 0) return;
|
||||||
|
|
||||||
|
const writeBtn = document.getElementById('batch-tag-preview-write-btn');
|
||||||
|
if (writeBtn) {
|
||||||
|
writeBtn.disabled = true;
|
||||||
|
writeBtn.textContent = 'Writing...';
|
||||||
|
}
|
||||||
|
|
||||||
|
const embedCover = document.getElementById('batch-tag-preview-embed-cover')?.checked ?? true;
|
||||||
|
const serverType = overlay._batchServerType;
|
||||||
|
const syncToServer = document.getElementById('batch-tag-preview-sync-server')?.checked && serverType && serverType !== 'navidrome';
|
||||||
|
|
||||||
|
closeBatchTagPreviewModal();
|
||||||
|
await _startBatchWriteTags(trackIds, embedCover, syncToServer);
|
||||||
|
|
||||||
|
if (writeBtn) {
|
||||||
|
writeBtn.disabled = false;
|
||||||
|
writeBtn.textContent = 'Write Tags';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function _startBatchWriteTags(trackIds, embedCover, syncToServer = false) {
|
async function _startBatchWriteTags(trackIds, embedCover, syncToServer = false) {
|
||||||
|
|
|
||||||
|
|
@ -32729,7 +32729,6 @@ textarea.enhanced-meta-field-input {
|
||||||
width: 500px;
|
width: 500px;
|
||||||
max-width: 90vw;
|
max-width: 90vw;
|
||||||
box-shadow: 0 25px 80px rgba(0,0,0,0.7);
|
box-shadow: 0 25px 80px rgba(0,0,0,0.7);
|
||||||
overflow: hidden;
|
|
||||||
}
|
}
|
||||||
.enhanced-bulk-modal-header {
|
.enhanced-bulk-modal-header {
|
||||||
padding: 20px 24px;
|
padding: 20px 24px;
|
||||||
|
|
@ -34106,6 +34105,201 @@ tr.tag-diff-same {
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Batch Tag Preview Modal */
|
||||||
|
.batch-tag-preview-modal {
|
||||||
|
width: 850px;
|
||||||
|
max-width: 92vw;
|
||||||
|
max-height: 85vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-tag-preview-modal .enhanced-bulk-modal-footer {
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-tag-preview-body {
|
||||||
|
overflow-y: auto;
|
||||||
|
max-height: 55vh;
|
||||||
|
padding: 8px 16px 16px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-tag-summary {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 10px 20px;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-tag-stat {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
padding: 3px 10px;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-tag-stat.changed {
|
||||||
|
background: rgba(var(--accent-rgb), 0.1);
|
||||||
|
color: rgb(var(--accent-rgb));
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-tag-stat.unchanged {
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
color: rgba(255, 255, 255, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-tag-stat.errored {
|
||||||
|
background: rgba(255, 80, 80, 0.1);
|
||||||
|
color: rgba(255, 80, 80, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-tag-track {
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||||
|
border-radius: 10px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-tag-track.error {
|
||||||
|
border-color: rgba(255, 80, 80, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-tag-track-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 10px 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-tag-track-header:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.03);
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-tag-track-number {
|
||||||
|
color: rgba(255, 255, 255, 0.3);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
min-width: 20px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-tag-track-title {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 13px;
|
||||||
|
color: rgba(255, 255, 255, 0.85);
|
||||||
|
font-weight: 500;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-tag-track-status {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 500;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-tag-track-status.changed {
|
||||||
|
background: rgba(var(--accent-rgb), 0.1);
|
||||||
|
color: rgb(var(--accent-rgb));
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-tag-track-status.error {
|
||||||
|
background: rgba(255, 80, 80, 0.1);
|
||||||
|
color: rgba(255, 80, 80, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-tag-track-status.ok {
|
||||||
|
color: rgba(255, 255, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-tag-chevron {
|
||||||
|
color: rgba(255, 255, 255, 0.2);
|
||||||
|
font-size: 11px;
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-tag-track.expanded .batch-tag-chevron {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-tag-track-diff {
|
||||||
|
display: none;
|
||||||
|
padding: 0 14px 12px;
|
||||||
|
border-top: 1px solid rgba(255, 255, 255, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-tag-track.expanded .batch-tag-track-diff {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-tag-track-diff .tag-preview-table {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-tag-track-diff .tag-preview-table th {
|
||||||
|
font-size: 10px;
|
||||||
|
padding: 6px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-tag-track-diff .tag-preview-table td {
|
||||||
|
padding: 5px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Unchanged tracks group */
|
||||||
|
.batch-tag-unchanged-group {
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.04);
|
||||||
|
border-radius: 10px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-tag-unchanged-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 10px 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(255, 255, 255, 0.35);
|
||||||
|
transition: background 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-tag-unchanged-header:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.02);
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-tag-unchanged-list {
|
||||||
|
display: none;
|
||||||
|
border-top: 1px solid rgba(255, 255, 255, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-tag-unchanged-group.expanded .batch-tag-unchanged-list {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-tag-unchanged-group.expanded .batch-tag-chevron {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-tag-track-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 7px 14px;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.02);
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-tag-track-row:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-tag-track-row.unchanged {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
/* ======================================================= */
|
/* ======================================================= */
|
||||||
/* == HELP & DOCS PAGE == */
|
/* == HELP & DOCS PAGE == */
|
||||||
/* ======================================================= */
|
/* ======================================================= */
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue