Design retag layout
This commit is contained in:
parent
3dad2eae38
commit
c279f2e4fa
6 changed files with 589 additions and 98 deletions
|
|
@ -5311,6 +5311,21 @@ class MusicDatabase:
|
|||
logger.error(f"Error deleting retag group: {e}")
|
||||
return False
|
||||
|
||||
def delete_all_retag_groups(self) -> int:
|
||||
"""Delete all retag groups and tracks. Returns count deleted."""
|
||||
try:
|
||||
conn = self._get_connection()
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("SELECT COUNT(*) FROM retag_groups")
|
||||
count = cursor.fetchone()[0]
|
||||
cursor.execute("DELETE FROM retag_tracks")
|
||||
cursor.execute("DELETE FROM retag_groups")
|
||||
conn.commit()
|
||||
return count
|
||||
except Exception as e:
|
||||
logger.error(f"Error clearing all retag groups: {e}")
|
||||
return 0
|
||||
|
||||
# Thread-safe singleton pattern for database access
|
||||
_database_instances: Dict[int, MusicDatabase] = {} # Thread ID -> Database instance
|
||||
_database_lock = threading.Lock()
|
||||
|
|
|
|||
|
|
@ -14211,6 +14211,29 @@ def delete_retag_group(group_id):
|
|||
else:
|
||||
return jsonify({"success": False, "error": "Group not found"}), 404
|
||||
|
||||
@app.route('/api/retag/groups/delete-batch', methods=['POST'])
|
||||
def delete_retag_groups_batch():
|
||||
"""Delete multiple retag groups at once."""
|
||||
from database.music_database import get_database
|
||||
data = request.get_json() or {}
|
||||
group_ids = data.get('group_ids', [])
|
||||
if not group_ids:
|
||||
return jsonify({"success": False, "error": "No group IDs provided"}), 400
|
||||
db = get_database()
|
||||
removed = 0
|
||||
for gid in group_ids:
|
||||
if db.delete_retag_group(int(gid)):
|
||||
removed += 1
|
||||
return jsonify({"success": True, "removed": removed})
|
||||
|
||||
@app.route('/api/retag/groups/clear-all', methods=['POST'])
|
||||
def clear_all_retag_groups():
|
||||
"""Delete all retag groups."""
|
||||
from database.music_database import get_database
|
||||
db = get_database()
|
||||
count = db.delete_all_retag_groups()
|
||||
return jsonify({"success": True, "removed": count})
|
||||
|
||||
# ===============================
|
||||
# == DOWNLOAD MISSING TRACKS ==
|
||||
# ===============================
|
||||
|
|
|
|||
|
|
@ -4250,8 +4250,17 @@
|
|||
<div class="retag-modal-overlay" id="retag-modal">
|
||||
<div class="retag-modal-container">
|
||||
<div class="retag-modal-header">
|
||||
<h2 class="retag-modal-title">Retag Tool</h2>
|
||||
<button class="retag-modal-close" onclick="closeRetagModal()">×</button>
|
||||
<div class="retag-modal-header-left">
|
||||
<h2 class="retag-modal-title">Retag Tool</h2>
|
||||
</div>
|
||||
<div class="retag-header-actions">
|
||||
<button class="retag-clear-all-btn" id="retag-clear-all-btn" onclick="clearAllRetagGroups(this)">Clear All</button>
|
||||
<button class="retag-modal-close" onclick="closeRetagModal()">×</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="retag-batch-bar" id="retag-batch-bar" style="display: none;">
|
||||
<span class="retag-batch-count" id="retag-batch-count">0 selected</span>
|
||||
<button class="retag-batch-remove-btn" onclick="batchRemoveRetagGroups()">Remove Selected</button>
|
||||
</div>
|
||||
<div class="retag-modal-body" id="retag-modal-body">
|
||||
<div class="retag-loading">Loading downloads...</div>
|
||||
|
|
|
|||
|
|
@ -1442,6 +1442,50 @@
|
|||
padding: 0 16px;
|
||||
}
|
||||
|
||||
/* Retag modal on mobile — fullscreen */
|
||||
.retag-modal-container {
|
||||
max-width: 100vw;
|
||||
width: 100vw;
|
||||
max-height: 100vh;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.retag-modal-header {
|
||||
padding: 16px;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.retag-modal-body {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.retag-batch-bar {
|
||||
margin: 0 16px 12px;
|
||||
}
|
||||
|
||||
.retag-group-header {
|
||||
padding: 12px;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.retag-group-image,
|
||||
.retag-group-image-placeholder {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
}
|
||||
|
||||
.retag-group-album {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.retag-search-container {
|
||||
max-width: 100vw;
|
||||
width: 100vw;
|
||||
max-height: 100vh;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
/* Watchlist live activity - stack on mobile */
|
||||
#watchlist-live-activity {
|
||||
flex-direction: column;
|
||||
|
|
|
|||
|
|
@ -14503,6 +14503,12 @@ async function openRetagModal() {
|
|||
modal.style.display = 'flex';
|
||||
document.body.style.overflow = 'hidden';
|
||||
|
||||
// Reset batch bar and clear-all button
|
||||
const batchBar = document.getElementById('retag-batch-bar');
|
||||
if (batchBar) batchBar.style.display = 'none';
|
||||
const clearBtn = document.getElementById('retag-clear-all-btn');
|
||||
if (clearBtn) { clearBtn.textContent = 'Clear All'; clearBtn.dataset.confirming = ''; clearBtn.style.background = ''; }
|
||||
|
||||
const body = document.getElementById('retag-modal-body');
|
||||
body.innerHTML = '<div class="retag-loading">Loading downloads...</div>';
|
||||
|
||||
|
|
@ -14511,8 +14517,10 @@ async function openRetagModal() {
|
|||
const data = await response.json();
|
||||
if (!data.success || !data.groups || data.groups.length === 0) {
|
||||
body.innerHTML = '<p class="retag-empty">No downloads recorded yet. Downloads will appear here after completing album or single downloads.</p>';
|
||||
if (clearBtn) clearBtn.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
if (clearBtn) clearBtn.style.display = '';
|
||||
renderRetagGroups(data.groups, body);
|
||||
} catch (e) {
|
||||
body.innerHTML = '<p class="retag-error">Failed to load downloads.</p>';
|
||||
|
|
@ -14551,13 +14559,19 @@ function renderRetagGroups(groups, container) {
|
|||
|
||||
html += `<div class="retag-group-card" data-group-id="${group.id}">
|
||||
<div class="retag-group-header" onclick="toggleRetagGroup(${group.id})">
|
||||
<label class="retag-group-checkbox" onclick="event.stopPropagation();">
|
||||
<input type="checkbox" class="retag-select-cb" data-group-id="${group.id}" onchange="updateRetagBatchBar()">
|
||||
<span class="retag-checkbox-custom"></span>
|
||||
</label>
|
||||
${imgHtml}
|
||||
<div class="retag-group-info">
|
||||
<span class="retag-group-album">${escapeHtml(group.album_name || 'Unknown')}</span>
|
||||
<span class="retag-group-meta">${typeLabel}${releaseDate ? ' \u00b7 ' + releaseDate : ''} \u00b7 ${trackCount} track${trackCount !== 1 ? 's' : ''}</span>
|
||||
</div>
|
||||
<button class="retag-group-btn" onclick="event.stopPropagation(); openRetagSearch(${group.id}, '${escapeHtml(defaultQuery).replace(/'/g, "\\'")}')" title="Re-tag with different album">Retag</button>
|
||||
<button class="retag-group-delete-btn" onclick="event.stopPropagation(); deleteRetagGroup(${group.id})" title="Remove from list">×</button>
|
||||
<div class="retag-group-delete-area" id="retag-delete-area-${group.id}">
|
||||
<button class="retag-group-delete-btn" onclick="event.stopPropagation(); showRetagDeleteConfirm(${group.id})" title="Remove from list">×</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="retag-group-tracks" id="retag-tracks-${group.id}" style="display:none;">
|
||||
<div class="retag-tracks-loading">Loading tracks...</div>
|
||||
|
|
@ -14687,7 +14701,7 @@ async function searchRetagAlbums(query) {
|
|||
: '<div class="retag-result-image-placeholder"></div>';
|
||||
const typeLabel = (a.album_type || 'album').charAt(0).toUpperCase() + (a.album_type || 'album').slice(1);
|
||||
const releaseYear = a.release_date ? a.release_date.substring(0, 4) : '';
|
||||
return `<div class="retag-search-result" onclick="confirmRetag(${retagCurrentGroupId}, '${a.id}', '${escapeHtml(a.name).replace(/'/g, "\\'")}')">
|
||||
return `<div class="retag-search-result" id="retag-result-${a.id}" onclick="showRetagConfirm(this, ${retagCurrentGroupId}, '${a.id}', '${escapeHtml(a.name).replace(/'/g, "\\'")}')">
|
||||
${imgHtml}
|
||||
<div class="retag-result-info">
|
||||
<span class="retag-result-name">${escapeHtml(a.name || 'Unknown')}</span>
|
||||
|
|
@ -14704,8 +14718,46 @@ async function searchRetagAlbums(query) {
|
|||
}
|
||||
}
|
||||
|
||||
async function confirmRetag(groupId, albumId, albumName) {
|
||||
if (!confirm(`Re-tag this group with "${albumName}"?\n\nThis will overwrite existing file tags and may move/rename files.`)) return;
|
||||
/**
|
||||
* Show inline confirmation on a search result before retagging
|
||||
*/
|
||||
function showRetagConfirm(el, groupId, albumId, albumName) {
|
||||
// Clear any other confirming states
|
||||
document.querySelectorAll('.retag-search-result.retag-confirming').forEach(r => {
|
||||
r.classList.remove('retag-confirming');
|
||||
const bar = r.querySelector('.retag-result-confirm-bar');
|
||||
if (bar) bar.remove();
|
||||
r.onclick = r._originalOnclick || null;
|
||||
});
|
||||
|
||||
el.classList.add('retag-confirming');
|
||||
el._originalOnclick = el.onclick;
|
||||
el.onclick = null; // Disable clicking the row again
|
||||
|
||||
const confirmBar = document.createElement('div');
|
||||
confirmBar.className = 'retag-result-confirm-bar';
|
||||
confirmBar.innerHTML = `
|
||||
<span>Re-tag with "${escapeHtml(albumName)}"?</span>
|
||||
<div class="retag-result-confirm-actions">
|
||||
<button class="retag-result-confirm-yes" onclick="event.stopPropagation(); executeRetag(${groupId}, '${albumId}', '${albumName.replace(/'/g, "\\'")}')">Confirm</button>
|
||||
<button class="retag-result-confirm-cancel" onclick="event.stopPropagation(); cancelRetagConfirm(this)">Cancel</button>
|
||||
</div>
|
||||
`;
|
||||
el.appendChild(confirmBar);
|
||||
}
|
||||
|
||||
function cancelRetagConfirm(cancelBtn) {
|
||||
const result = cancelBtn.closest('.retag-search-result');
|
||||
if (!result) return;
|
||||
result.classList.remove('retag-confirming');
|
||||
const bar = result.querySelector('.retag-result-confirm-bar');
|
||||
if (bar) bar.remove();
|
||||
if (result._originalOnclick) {
|
||||
result.onclick = result._originalOnclick;
|
||||
}
|
||||
}
|
||||
|
||||
async function executeRetag(groupId, albumId, albumName) {
|
||||
|
||||
closeRetagSearch();
|
||||
closeRetagModal();
|
||||
|
|
@ -14780,25 +14832,41 @@ function updateRetagProgressUI(state) {
|
|||
}
|
||||
}
|
||||
|
||||
async function deleteRetagGroup(groupId) {
|
||||
if (!confirm('Remove this group from the retag list?\n\nYour files will not be deleted.')) return;
|
||||
/**
|
||||
* Show inline delete confirmation for a retag group
|
||||
*/
|
||||
function showRetagDeleteConfirm(groupId) {
|
||||
const area = document.getElementById(`retag-delete-area-${groupId}`);
|
||||
if (!area) return;
|
||||
area.innerHTML = `<div class="retag-confirm-inline">
|
||||
<span>Remove?</span>
|
||||
<button class="retag-confirm-yes" onclick="event.stopPropagation(); executeRetagGroupDelete(${groupId})">Yes</button>
|
||||
<button class="retag-confirm-no" onclick="event.stopPropagation(); cancelRetagDeleteConfirm(${groupId})">No</button>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function cancelRetagDeleteConfirm(groupId) {
|
||||
const area = document.getElementById(`retag-delete-area-${groupId}`);
|
||||
if (!area) return;
|
||||
area.innerHTML = `<button class="retag-group-delete-btn" onclick="event.stopPropagation(); showRetagDeleteConfirm(${groupId})" title="Remove from list">×</button>`;
|
||||
}
|
||||
|
||||
async function executeRetagGroupDelete(groupId) {
|
||||
try {
|
||||
const response = await fetch(`/api/retag/groups/${groupId}`, { method: 'DELETE' });
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
// Remove the card from DOM
|
||||
const card = document.querySelector(`.retag-group-card[data-group-id="${groupId}"]`);
|
||||
if (card) {
|
||||
const section = card.closest('.retag-artist-section');
|
||||
card.remove();
|
||||
// If no more groups for this artist, remove the artist section
|
||||
if (section && section.querySelectorAll('.retag-group-card').length === 0) {
|
||||
section.remove();
|
||||
}
|
||||
}
|
||||
loadRetagStats();
|
||||
showToast('Group removed from retag list', 'success');
|
||||
updateRetagBatchBar();
|
||||
showToast('Group removed', 'success');
|
||||
} else {
|
||||
showToast('Failed to remove group', 'error');
|
||||
}
|
||||
|
|
@ -14807,6 +14875,94 @@ async function deleteRetagGroup(groupId) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the retag batch action bar based on checkbox selection
|
||||
*/
|
||||
function updateRetagBatchBar() {
|
||||
const checked = document.querySelectorAll('.retag-select-cb:checked');
|
||||
const bar = document.getElementById('retag-batch-bar');
|
||||
const countEl = document.getElementById('retag-batch-count');
|
||||
if (!bar) return;
|
||||
|
||||
if (checked.length > 0) {
|
||||
bar.style.display = 'flex';
|
||||
countEl.textContent = `${checked.length} selected`;
|
||||
} else {
|
||||
bar.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Batch remove selected retag groups
|
||||
*/
|
||||
async function batchRemoveRetagGroups() {
|
||||
const checked = document.querySelectorAll('.retag-select-cb:checked');
|
||||
if (checked.length === 0) return;
|
||||
|
||||
const groupIds = Array.from(checked).map(cb => parseInt(cb.getAttribute('data-group-id')));
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/retag/groups/delete-batch', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ group_ids: groupIds })
|
||||
});
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
showToast(`Removed ${data.removed} group${data.removed !== 1 ? 's' : ''}`, 'success');
|
||||
openRetagModal(); // Refresh
|
||||
} else {
|
||||
showToast('Failed to remove groups', 'error');
|
||||
}
|
||||
} catch (e) {
|
||||
showToast('Failed to remove groups', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all retag groups — inline confirm on the button itself
|
||||
*/
|
||||
function clearAllRetagGroups(btn) {
|
||||
if (!btn) return;
|
||||
if (btn.dataset.confirming === 'true') {
|
||||
// Already confirming — execute
|
||||
btn.dataset.confirming = '';
|
||||
btn.textContent = 'Clear All';
|
||||
executeClearAllRetag();
|
||||
return;
|
||||
}
|
||||
// First click — show confirm state
|
||||
btn.dataset.confirming = 'true';
|
||||
btn.textContent = 'Confirm Clear?';
|
||||
btn.style.background = 'rgba(255, 59, 48, 0.15)';
|
||||
// Auto-reset after 3 seconds if not clicked again
|
||||
setTimeout(() => {
|
||||
if (btn.dataset.confirming === 'true') {
|
||||
btn.dataset.confirming = '';
|
||||
btn.textContent = 'Clear All';
|
||||
btn.style.background = '';
|
||||
}
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
async function executeClearAllRetag() {
|
||||
try {
|
||||
const response = await fetch('/api/retag/groups/clear-all', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
showToast(`Cleared ${data.removed} group${data.removed !== 1 ? 's' : ''}`, 'success');
|
||||
openRetagModal(); // Refresh
|
||||
} else {
|
||||
showToast('Failed to clear groups', 'error');
|
||||
}
|
||||
} catch (e) {
|
||||
showToast('Failed to clear groups', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function stopWishlistCountPolling() {
|
||||
if (wishlistCountInterval) {
|
||||
clearInterval(wishlistCountInterval);
|
||||
|
|
|
|||
|
|
@ -19172,51 +19172,89 @@ body {
|
|||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.6);
|
||||
backdrop-filter: blur(4px);
|
||||
background: rgba(18, 18, 18, 0.85);
|
||||
backdrop-filter: blur(12px);
|
||||
z-index: 10001;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
animation: fadeIn 0.2s ease;
|
||||
}
|
||||
|
||||
.retag-search-overlay {
|
||||
z-index: 10002;
|
||||
}
|
||||
|
||||
/* Main modal — fullscreen glassmorphic */
|
||||
.retag-modal-container {
|
||||
background-color: #1a1a1a;
|
||||
border-radius: 12px;
|
||||
max-width: 700px;
|
||||
width: 90%;
|
||||
max-height: 80vh;
|
||||
background: linear-gradient(135deg,
|
||||
rgba(20, 20, 20, 0.95) 0%,
|
||||
rgba(12, 12, 12, 0.98) 100%);
|
||||
backdrop-filter: blur(20px) saturate(1.2);
|
||||
border-radius: 20px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.18);
|
||||
max-width: 95vw;
|
||||
width: 95vw;
|
||||
max-height: 95vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5);
|
||||
animation: slideUp 0.3s ease;
|
||||
box-shadow:
|
||||
0 20px 60px rgba(0, 0, 0, 0.6),
|
||||
0 8px 32px rgba(0, 0, 0, 0.4),
|
||||
0 0 40px rgba(var(--accent-rgb), 0.1),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.15);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.retag-modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20px 24px;
|
||||
border-bottom: 1px solid #333;
|
||||
padding: 24px 32px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.retag-modal-header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.retag-modal-title {
|
||||
font-size: 20px;
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.retag-header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.retag-clear-all-btn {
|
||||
background: none;
|
||||
border: 1px solid rgba(255, 59, 48, 0.3);
|
||||
color: #ff6b6b;
|
||||
padding: 6px 14px;
|
||||
border-radius: 8px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s ease, border-color 0.15s ease;
|
||||
}
|
||||
|
||||
.retag-clear-all-btn:hover {
|
||||
background: rgba(255, 59, 48, 0.15);
|
||||
border-color: rgba(255, 59, 48, 0.5);
|
||||
}
|
||||
|
||||
.retag-modal-close {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #999;
|
||||
font-size: 24px;
|
||||
color: #b3b3b3;
|
||||
font-size: 28px;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
line-height: 1;
|
||||
|
|
@ -19228,7 +19266,7 @@ body {
|
|||
}
|
||||
|
||||
.retag-modal-body {
|
||||
padding: 20px 24px;
|
||||
padding: 24px 32px;
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
}
|
||||
|
|
@ -19238,60 +19276,144 @@ body {
|
|||
.retag-error {
|
||||
text-align: center;
|
||||
color: #b3b3b3;
|
||||
padding: 40px 0;
|
||||
font-size: 14px;
|
||||
padding: 60px 0;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
/* Batch action bar */
|
||||
.retag-batch-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 10px 20px;
|
||||
margin: 0 32px 16px;
|
||||
background: rgba(255, 59, 48, 0.1);
|
||||
border: 1px solid rgba(255, 59, 48, 0.25);
|
||||
border-radius: 10px;
|
||||
animation: batchBarSlideIn 0.2s ease-out;
|
||||
}
|
||||
|
||||
.retag-batch-count {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.retag-batch-remove-btn {
|
||||
background: none;
|
||||
border: 1px solid rgba(255, 59, 48, 0.4);
|
||||
color: #ff6b6b;
|
||||
padding: 6px 14px;
|
||||
border-radius: 8px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s ease;
|
||||
}
|
||||
|
||||
.retag-batch-remove-btn:hover {
|
||||
background: rgba(255, 59, 48, 0.2);
|
||||
border-color: rgba(255, 59, 48, 0.6);
|
||||
}
|
||||
|
||||
/* Artist sections */
|
||||
.retag-artist-section {
|
||||
margin-bottom: 20px;
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
|
||||
.retag-artist-name {
|
||||
font-size: 14px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #b3b3b3;
|
||||
color: rgb(var(--accent-light-rgb));
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
margin: 0 0 10px 0;
|
||||
padding-bottom: 6px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
|
||||
letter-spacing: 0.8px;
|
||||
margin: 0 0 12px 0;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid rgba(var(--accent-rgb), 0.15);
|
||||
}
|
||||
|
||||
/* Group cards */
|
||||
.retag-group-card {
|
||||
background: rgba(30, 30, 30, 0.8);
|
||||
border-radius: 10px;
|
||||
margin-bottom: 8px;
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
border-radius: 12px;
|
||||
margin-bottom: 10px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||
transition: border-color 0.2s;
|
||||
transition: border-color 0.2s ease, background 0.2s ease;
|
||||
}
|
||||
|
||||
.retag-group-card:hover {
|
||||
border-color: rgba(255, 255, 255, 0.12);
|
||||
border-color: rgba(var(--accent-rgb), 0.2);
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.retag-group-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
gap: 14px;
|
||||
padding: 14px 16px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Group checkbox */
|
||||
.retag-group-checkbox {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
cursor: pointer;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.retag-group-checkbox input[type="checkbox"] {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.retag-group-checkbox .retag-checkbox-custom {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 2px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 4px;
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
transition: all 0.15s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.retag-group-checkbox:hover .retag-checkbox-custom {
|
||||
border-color: rgba(255, 255, 255, 0.4);
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
|
||||
.retag-group-checkbox input:checked+.retag-checkbox-custom {
|
||||
background: rgba(var(--accent-rgb), 0.3);
|
||||
border-color: rgb(var(--accent-rgb));
|
||||
}
|
||||
|
||||
.retag-group-checkbox input:checked+.retag-checkbox-custom::after {
|
||||
content: '\2713';
|
||||
color: rgb(var(--accent-rgb));
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.retag-group-image {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 6px;
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: 8px;
|
||||
object-fit: cover;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.retag-group-image-placeholder {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 6px;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: 8px;
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
|
|
@ -19299,13 +19421,13 @@ body {
|
|||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
gap: 3px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.retag-group-album {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
font-size: 15px;
|
||||
color: #fff;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
|
|
@ -19314,46 +19436,97 @@ body {
|
|||
|
||||
.retag-group-meta {
|
||||
font-size: 12px;
|
||||
color: #b3b3b3;
|
||||
color: rgba(255, 255, 255, 0.45);
|
||||
}
|
||||
|
||||
.retag-group-btn {
|
||||
background: rgb(var(--accent-rgb));
|
||||
color: #fff;
|
||||
border: none;
|
||||
padding: 6px 16px;
|
||||
padding: 7px 18px;
|
||||
border-radius: 20px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
transition: background 0.2s, transform 0.1s;
|
||||
transition: background 0.15s ease;
|
||||
}
|
||||
|
||||
.retag-group-btn:hover {
|
||||
background: rgb(var(--accent-light-rgb));
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
/* Delete button — shows × or inline confirm */
|
||||
.retag-group-delete-area {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.retag-group-delete-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #666;
|
||||
font-size: 18px;
|
||||
color: rgba(255, 255, 255, 0.3);
|
||||
font-size: 20px;
|
||||
cursor: pointer;
|
||||
padding: 4px 8px;
|
||||
line-height: 1;
|
||||
flex-shrink: 0;
|
||||
transition: color 0.2s;
|
||||
transition: color 0.15s ease;
|
||||
}
|
||||
|
||||
.retag-group-delete-btn:hover {
|
||||
color: #ff4444;
|
||||
}
|
||||
|
||||
/* Inline confirmation */
|
||||
.retag-confirm-inline {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 12px;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
animation: batchBarSlideIn 0.15s ease-out;
|
||||
}
|
||||
|
||||
.retag-confirm-inline span {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.retag-confirm-yes {
|
||||
background: rgba(255, 59, 48, 0.2);
|
||||
border: 1px solid rgba(255, 59, 48, 0.4);
|
||||
color: #ff6b6b;
|
||||
padding: 3px 10px;
|
||||
border-radius: 6px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s ease;
|
||||
}
|
||||
|
||||
.retag-confirm-yes:hover {
|
||||
background: rgba(255, 59, 48, 0.4);
|
||||
}
|
||||
|
||||
.retag-confirm-no {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
padding: 3px 10px;
|
||||
border-radius: 6px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s ease;
|
||||
}
|
||||
|
||||
.retag-confirm-no:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
/* Track list */
|
||||
.retag-group-tracks {
|
||||
padding: 0 12px 12px;
|
||||
padding: 4px 16px 14px;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.04);
|
||||
}
|
||||
|
||||
|
|
@ -19361,7 +19534,7 @@ body {
|
|||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 6px 0;
|
||||
padding: 7px 0;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.03);
|
||||
}
|
||||
|
||||
|
|
@ -19370,7 +19543,7 @@ body {
|
|||
}
|
||||
|
||||
.retag-track-number {
|
||||
color: #666;
|
||||
color: rgba(255, 255, 255, 0.3);
|
||||
font-size: 12px;
|
||||
width: 32px;
|
||||
text-align: right;
|
||||
|
|
@ -19396,37 +19569,44 @@ body {
|
|||
|
||||
.retag-tracks-loading,
|
||||
.retag-tracks-empty {
|
||||
color: #666;
|
||||
color: rgba(255, 255, 255, 0.35);
|
||||
font-size: 12px;
|
||||
padding: 8px 0;
|
||||
padding: 10px 0;
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Search sub-modal */
|
||||
/* Search sub-modal — glassmorphic */
|
||||
.retag-search-container {
|
||||
background-color: #1a1a1a;
|
||||
border-radius: 12px;
|
||||
max-width: 600px;
|
||||
background: linear-gradient(135deg,
|
||||
rgba(20, 20, 20, 0.95) 0%,
|
||||
rgba(12, 12, 12, 0.98) 100%);
|
||||
backdrop-filter: blur(20px) saturate(1.2);
|
||||
border-radius: 16px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
max-width: 650px;
|
||||
width: 90%;
|
||||
max-height: 75vh;
|
||||
max-height: 80vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5);
|
||||
animation: slideUp 0.3s ease;
|
||||
box-shadow:
|
||||
0 20px 60px rgba(0, 0, 0, 0.6),
|
||||
0 8px 32px rgba(0, 0, 0, 0.4),
|
||||
0 0 30px rgba(var(--accent-rgb), 0.08);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.retag-search-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid #333;
|
||||
padding: 20px 24px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.retag-search-title {
|
||||
font-size: 16px;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
margin: 0;
|
||||
|
|
@ -19435,8 +19615,8 @@ body {
|
|||
.retag-search-close {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #999;
|
||||
font-size: 22px;
|
||||
color: #b3b3b3;
|
||||
font-size: 24px;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
line-height: 1;
|
||||
|
|
@ -19448,29 +19628,31 @@ body {
|
|||
}
|
||||
|
||||
.retag-search-input-section {
|
||||
padding: 16px 20px 8px;
|
||||
padding: 20px 24px 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.retag-search-input-section input {
|
||||
width: 100%;
|
||||
padding: 10px 14px;
|
||||
background: #2a2a2a;
|
||||
border: 1px solid #404040;
|
||||
border-radius: 8px;
|
||||
padding: 12px 16px;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 10px;
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
font-family: inherit;
|
||||
outline: none;
|
||||
transition: border-color 0.2s;
|
||||
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.retag-search-input-section input:focus {
|
||||
border-color: rgb(var(--accent-rgb));
|
||||
border-color: rgba(var(--accent-rgb), 0.4);
|
||||
box-shadow: 0 0 0 3px rgba(var(--accent-rgb), 0.1);
|
||||
}
|
||||
|
||||
.retag-search-results {
|
||||
padding: 8px 20px 20px;
|
||||
padding: 8px 24px 24px;
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
}
|
||||
|
|
@ -19480,28 +19662,90 @@ body {
|
|||
.retag-search-error {
|
||||
text-align: center;
|
||||
color: #b3b3b3;
|
||||
padding: 20px 0;
|
||||
font-size: 13px;
|
||||
padding: 24px 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.retag-search-result {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 10px;
|
||||
border-radius: 8px;
|
||||
gap: 14px;
|
||||
padding: 12px;
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s;
|
||||
transition: background 0.15s ease;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.retag-search-result:hover {
|
||||
background: rgba(var(--accent-rgb), 0.1);
|
||||
background: rgba(var(--accent-rgb), 0.08);
|
||||
border-color: rgba(var(--accent-rgb), 0.15);
|
||||
}
|
||||
|
||||
/* Retag search result confirm state */
|
||||
.retag-search-result.retag-confirming {
|
||||
background: rgba(var(--accent-rgb), 0.08);
|
||||
border-color: rgba(var(--accent-rgb), 0.25);
|
||||
cursor: default;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.retag-result-confirm-bar {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-top: 10px;
|
||||
padding-top: 10px;
|
||||
border-top: 1px solid rgba(var(--accent-rgb), 0.15);
|
||||
}
|
||||
|
||||
.retag-result-confirm-bar span {
|
||||
font-size: 13px;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
.retag-result-confirm-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.retag-result-confirm-yes {
|
||||
background: rgb(var(--accent-rgb));
|
||||
border: none;
|
||||
color: #fff;
|
||||
padding: 6px 16px;
|
||||
border-radius: 8px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s ease;
|
||||
}
|
||||
|
||||
.retag-result-confirm-yes:hover {
|
||||
background: rgb(var(--accent-light-rgb));
|
||||
}
|
||||
|
||||
.retag-result-confirm-cancel {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
padding: 6px 16px;
|
||||
border-radius: 8px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s ease;
|
||||
}
|
||||
|
||||
.retag-result-confirm-cancel:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.retag-result-image {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: 6px;
|
||||
border-radius: 8px;
|
||||
object-fit: cover;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
|
@ -19509,8 +19753,8 @@ body {
|
|||
.retag-result-image-placeholder {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: 6px;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
border-radius: 8px;
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
|
|
@ -19518,13 +19762,13 @@ body {
|
|||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
gap: 3px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.retag-result-name {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
font-size: 15px;
|
||||
color: #fff;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
|
|
@ -19538,7 +19782,7 @@ body {
|
|||
|
||||
.retag-result-meta {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
|
||||
/* ====================================
|
||||
|
|
|
|||
Loading…
Reference in a new issue