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
|
||||
|
||||
|
||||
@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'])
|
||||
def write_track_tags(track_id):
|
||||
"""Write DB metadata into the audio file tags for a single track."""
|
||||
|
|
|
|||
|
|
@ -2598,6 +2598,34 @@
|
|||
</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 -->
|
||||
<div class="page" id="discover-page">
|
||||
<div class="discover-container">
|
||||
|
|
|
|||
|
|
@ -35130,7 +35130,7 @@ function sortEnhancedTracks(album, field, ascending) {
|
|||
|
||||
async function deleteLibraryTrack(trackId, albumId) {
|
||||
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 {
|
||||
const response = await fetch(`/api/library/track/${trackId}`, { method: 'DELETE' });
|
||||
const result = await response.json();
|
||||
|
|
@ -35151,7 +35151,7 @@ async function deleteLibraryTrack(trackId, 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 {
|
||||
const response = await fetch(`/api/library/album/${albumId}`, { method: 'DELETE' });
|
||||
const result = await response.json();
|
||||
|
|
@ -36060,26 +36060,179 @@ async function writeAlbumTags(albumId) {
|
|||
return;
|
||||
}
|
||||
|
||||
const serverType = artistDetailPageState.serverType;
|
||||
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);
|
||||
await showBatchTagPreview(tracks.map(t => t.id), album.title);
|
||||
}
|
||||
|
||||
async function batchWriteTagsSelected() {
|
||||
const trackIds = Array.from(artistDetailPageState.selectedTracks);
|
||||
if (trackIds.length === 0) return;
|
||||
|
||||
const serverType = artistDetailPageState.serverType;
|
||||
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)?`;
|
||||
if (canSync) msg += `\n\nThis will also sync changes to ${serverLabel}.`;
|
||||
if (!confirm(msg)) return;
|
||||
await _startBatchWriteTags(trackIds, true, canSync);
|
||||
await showBatchTagPreview(trackIds, null);
|
||||
}
|
||||
|
||||
async function showBatchTagPreview(trackIds, albumTitle) {
|
||||
const overlay = document.getElementById('batch-tag-preview-overlay');
|
||||
const body = document.getElementById('batch-tag-preview-body');
|
||||
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) {
|
||||
|
|
|
|||
|
|
@ -32729,7 +32729,6 @@ textarea.enhanced-meta-field-input {
|
|||
width: 500px;
|
||||
max-width: 90vw;
|
||||
box-shadow: 0 25px 80px rgba(0,0,0,0.7);
|
||||
overflow: hidden;
|
||||
}
|
||||
.enhanced-bulk-modal-header {
|
||||
padding: 20px 24px;
|
||||
|
|
@ -34106,6 +34105,201 @@ tr.tag-diff-same {
|
|||
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 == */
|
||||
/* ======================================================= */
|
||||
|
|
|
|||
Loading…
Reference in a new issue