style: auto-fix linting issues
This commit is contained in:
parent
5eba6aa224
commit
48937ed573
5 changed files with 485 additions and 219 deletions
371
index.html
371
index.html
|
|
@ -738,95 +738,272 @@
|
|||
<div class="modal-overlay"></div>
|
||||
<div class="modal-content">
|
||||
<h3>Edit Profile</h3>
|
||||
<div style="margin-bottom: 1rem;">
|
||||
<label style="display: block; margin-bottom: 0.5rem; font-size: 0.9rem;">Username</label>
|
||||
<div style="margin-bottom: 1rem">
|
||||
<label style="display: block; margin-bottom: 0.5rem; font-size: 0.9rem">Username</label>
|
||||
<input type="text" id="edit-profile-username" class="template-input" placeholder="username" />
|
||||
<p id="username-error" style="color: #ef4444; font-size: 0.8rem; display: none; margin-top: 0.25rem;"></p>
|
||||
<p
|
||||
id="username-error"
|
||||
style="color: #ef4444; font-size: 0.8rem; display: none; margin-top: 0.25rem"
|
||||
></p>
|
||||
</div>
|
||||
<div style="margin-bottom: 1rem;">
|
||||
<label style="display: block; margin-bottom: 0.5rem; font-size: 0.9rem;">Display Name</label>
|
||||
<input type="text" id="edit-profile-display-name" class="template-input" placeholder="Display Name" />
|
||||
<div style="margin-bottom: 1rem">
|
||||
<label style="display: block; margin-bottom: 0.5rem; font-size: 0.9rem">Display Name</label>
|
||||
<input
|
||||
type="text"
|
||||
id="edit-profile-display-name"
|
||||
class="template-input"
|
||||
placeholder="Display Name"
|
||||
/>
|
||||
</div>
|
||||
<div style="margin-bottom: 1rem;">
|
||||
<label style="display: block; margin-bottom: 0.5rem; font-size: 0.9rem;">Avatar URL</label>
|
||||
<div style="margin-bottom: 1rem">
|
||||
<label style="display: block; margin-bottom: 0.5rem; font-size: 0.9rem">Avatar URL</label>
|
||||
<div style="display: flex; gap: 0.5rem; align-items: stretch">
|
||||
<input type="url" id="edit-profile-avatar" class="template-input" placeholder="Avatar URL" style="flex: 1; margin: 0; display: none" />
|
||||
<input
|
||||
type="url"
|
||||
id="edit-profile-avatar"
|
||||
class="template-input"
|
||||
placeholder="Avatar URL"
|
||||
style="flex: 1; margin: 0; display: none"
|
||||
/>
|
||||
<input type="file" id="edit-profile-avatar-file" accept="image/*" style="display: none" />
|
||||
<button type="button" id="edit-profile-avatar-upload-btn" class="template-btn" style="flex: 1; padding: 0.5rem; font-size: 0.85rem; display: flex; align-items: center; justify-content: center; gap: 0.5rem; background: var(--input); border: 1px solid var(--border); color: var(--foreground); border-radius: var(--radius); cursor: pointer;">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<button
|
||||
type="button"
|
||||
id="edit-profile-avatar-upload-btn"
|
||||
class="template-btn"
|
||||
style="
|
||||
flex: 1;
|
||||
padding: 0.5rem;
|
||||
font-size: 0.85rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
background: var(--input);
|
||||
border: 1px solid var(--border);
|
||||
color: var(--foreground);
|
||||
border-radius: var(--radius);
|
||||
cursor: pointer;
|
||||
"
|
||||
>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
|
||||
<polyline points="17 8 12 3 7 8" />
|
||||
<line x1="12" y1="3" x2="12" y2="15" />
|
||||
</svg>
|
||||
<span>Upload</span>
|
||||
</button>
|
||||
<button type="button" id="edit-profile-avatar-toggle-btn" class="template-btn" style="padding: 0.5rem; font-size: 0.85rem; white-space: nowrap; background: var(--input); border: 1px solid var(--border); color: var(--foreground); border-radius: var(--radius); cursor: pointer;" title="Switch to URL input">or URL</button>
|
||||
<button
|
||||
type="button"
|
||||
id="edit-profile-avatar-toggle-btn"
|
||||
class="template-btn"
|
||||
style="
|
||||
padding: 0.5rem;
|
||||
font-size: 0.85rem;
|
||||
white-space: nowrap;
|
||||
background: var(--input);
|
||||
border: 1px solid var(--border);
|
||||
color: var(--foreground);
|
||||
border-radius: var(--radius);
|
||||
cursor: pointer;
|
||||
"
|
||||
title="Switch to URL input"
|
||||
>
|
||||
or URL
|
||||
</button>
|
||||
</div>
|
||||
<div id="edit-profile-avatar-upload-status" style="display: none; margin-top: 0.25rem; font-size: 0.75rem; opacity: 0.8"></div>
|
||||
<div
|
||||
id="edit-profile-avatar-upload-status"
|
||||
style="display: none; margin-top: 0.25rem; font-size: 0.75rem; opacity: 0.8"
|
||||
></div>
|
||||
</div>
|
||||
<div style="margin-bottom: 1rem;">
|
||||
<label style="display: block; margin-bottom: 0.5rem; font-size: 0.9rem;">Banner URL</label>
|
||||
<div style="margin-bottom: 1rem">
|
||||
<label style="display: block; margin-bottom: 0.5rem; font-size: 0.9rem">Banner URL</label>
|
||||
<div style="display: flex; gap: 0.5rem; align-items: stretch">
|
||||
<input type="url" id="edit-profile-banner" class="template-input" placeholder="Banner URL" style="flex: 1; margin: 0; display: none" />
|
||||
<input
|
||||
type="url"
|
||||
id="edit-profile-banner"
|
||||
class="template-input"
|
||||
placeholder="Banner URL"
|
||||
style="flex: 1; margin: 0; display: none"
|
||||
/>
|
||||
<input type="file" id="edit-profile-banner-file" accept="image/*" style="display: none" />
|
||||
<button type="button" id="edit-profile-banner-upload-btn" class="template-btn" style="flex: 1; padding: 0.5rem; font-size: 0.85rem; display: flex; align-items: center; justify-content: center; gap: 0.5rem; background: var(--input); border: 1px solid var(--border); color: var(--foreground); border-radius: var(--radius); cursor: pointer;">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<button
|
||||
type="button"
|
||||
id="edit-profile-banner-upload-btn"
|
||||
class="template-btn"
|
||||
style="
|
||||
flex: 1;
|
||||
padding: 0.5rem;
|
||||
font-size: 0.85rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
background: var(--input);
|
||||
border: 1px solid var(--border);
|
||||
color: var(--foreground);
|
||||
border-radius: var(--radius);
|
||||
cursor: pointer;
|
||||
"
|
||||
>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
|
||||
<polyline points="17 8 12 3 7 8" />
|
||||
<line x1="12" y1="3" x2="12" y2="15" />
|
||||
</svg>
|
||||
<span>Upload</span>
|
||||
</button>
|
||||
<button type="button" id="edit-profile-banner-toggle-btn" class="template-btn" style="padding: 0.5rem; font-size: 0.85rem; white-space: nowrap; background: var(--input); border: 1px solid var(--border); color: var(--foreground); border-radius: var(--radius); cursor: pointer;" title="Switch to URL input">or URL</button>
|
||||
<button
|
||||
type="button"
|
||||
id="edit-profile-banner-toggle-btn"
|
||||
class="template-btn"
|
||||
style="
|
||||
padding: 0.5rem;
|
||||
font-size: 0.85rem;
|
||||
white-space: nowrap;
|
||||
background: var(--input);
|
||||
border: 1px solid var(--border);
|
||||
color: var(--foreground);
|
||||
border-radius: var(--radius);
|
||||
cursor: pointer;
|
||||
"
|
||||
title="Switch to URL input"
|
||||
>
|
||||
or URL
|
||||
</button>
|
||||
</div>
|
||||
<div id="edit-profile-banner-upload-status" style="display: none; margin-top: 0.25rem; font-size: 0.75rem; opacity: 0.8"></div>
|
||||
<div
|
||||
id="edit-profile-banner-upload-status"
|
||||
style="display: none; margin-top: 0.25rem; font-size: 0.75rem; opacity: 0.8"
|
||||
></div>
|
||||
</div>
|
||||
<div style="margin-bottom: 1rem;">
|
||||
<label style="display: block; margin-bottom: 0.5rem; font-size: 0.9rem;">Status (Listening to...)</label>
|
||||
<div style="margin-bottom: 1rem">
|
||||
<label style="display: block; margin-bottom: 0.5rem; font-size: 0.9rem"
|
||||
>Status (Listening to...)</label
|
||||
>
|
||||
<div class="status-picker-container">
|
||||
<input type="text" id="edit-profile-status-search" class="template-input" placeholder="Search for a song or album..." autocomplete="off" />
|
||||
<input
|
||||
type="text"
|
||||
id="edit-profile-status-search"
|
||||
class="template-input"
|
||||
placeholder="Search for a song or album..."
|
||||
autocomplete="off"
|
||||
/>
|
||||
<div id="status-search-results" class="search-results-dropdown"></div>
|
||||
<input type="hidden" id="edit-profile-status-json">
|
||||
<div id="status-preview" style="display: none; margin-top: 0.5rem; padding: 0.5rem; background: var(--secondary); border-radius: var(--radius); align-items: center; gap: 0.5rem;">
|
||||
<img id="status-preview-img" src="" style="width: 32px; height: 32px; border-radius: 4px; object-fit: cover;">
|
||||
<div style="flex: 1; min-width: 0;">
|
||||
<div id="status-preview-title" style="font-weight: 500; font-size: 0.9rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;"></div>
|
||||
<div id="status-preview-subtitle" style="font-size: 0.8rem; color: var(--muted-foreground); white-space: nowrap; overflow: hidden; text-overflow: ellipsis;"></div>
|
||||
<input type="hidden" id="edit-profile-status-json" />
|
||||
<div
|
||||
id="status-preview"
|
||||
style="
|
||||
display: none;
|
||||
margin-top: 0.5rem;
|
||||
padding: 0.5rem;
|
||||
background: var(--secondary);
|
||||
border-radius: var(--radius);
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
"
|
||||
>
|
||||
<img
|
||||
id="status-preview-img"
|
||||
src=""
|
||||
style="width: 32px; height: 32px; border-radius: 4px; object-fit: cover"
|
||||
/>
|
||||
<div style="flex: 1; min-width: 0">
|
||||
<div
|
||||
id="status-preview-title"
|
||||
style="
|
||||
font-weight: 500;
|
||||
font-size: 0.9rem;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
"
|
||||
></div>
|
||||
<div
|
||||
id="status-preview-subtitle"
|
||||
style="
|
||||
font-size: 0.8rem;
|
||||
color: var(--muted-foreground);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
"
|
||||
></div>
|
||||
</div>
|
||||
<button id="clear-status-btn" class="btn-icon" style="width: 24px; height: 24px;">×</button>
|
||||
<button id="clear-status-btn" class="btn-icon" style="width: 24px; height: 24px">
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-bottom: 1rem;">
|
||||
<label style="display: block; margin-bottom: 0.5rem; font-size: 0.9rem;">Favorite Albums (Max 5)</label>
|
||||
<div id="edit-favorite-albums-list" style="display: flex; flex-direction: column; gap: 0.5rem; margin-bottom: 0.5rem;"></div>
|
||||
<div style="margin-bottom: 1rem">
|
||||
<label style="display: block; margin-bottom: 0.5rem; font-size: 0.9rem"
|
||||
>Favorite Albums (Max 5)</label
|
||||
>
|
||||
<div
|
||||
id="edit-favorite-albums-list"
|
||||
style="display: flex; flex-direction: column; gap: 0.5rem; margin-bottom: 0.5rem"
|
||||
></div>
|
||||
<div class="status-picker-container">
|
||||
<input type="text" id="edit-favorite-albums-search" class="template-input" placeholder="Search for an album..." autocomplete="off" />
|
||||
<input
|
||||
type="text"
|
||||
id="edit-favorite-albums-search"
|
||||
class="template-input"
|
||||
placeholder="Search for an album..."
|
||||
autocomplete="off"
|
||||
/>
|
||||
<div id="edit-favorite-albums-results" class="search-results-dropdown"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-bottom: 1rem;">
|
||||
<label style="display: block; margin-bottom: 0.5rem; font-size: 0.9rem;">About Me</label>
|
||||
<textarea id="edit-profile-about" class="template-input" style="resize: vertical; min-height: 80px;" placeholder="Tell us about yourself"></textarea>
|
||||
<div style="margin-bottom: 1rem">
|
||||
<label style="display: block; margin-bottom: 0.5rem; font-size: 0.9rem">About Me</label>
|
||||
<textarea
|
||||
id="edit-profile-about"
|
||||
class="template-input"
|
||||
style="resize: vertical; min-height: 80px"
|
||||
placeholder="Tell us about yourself"
|
||||
></textarea>
|
||||
</div>
|
||||
<div style="margin-bottom: 1rem;">
|
||||
<label style="display: block; margin-bottom: 0.5rem; font-size: 0.9rem;">Website</label>
|
||||
<div style="margin-bottom: 1rem">
|
||||
<label style="display: block; margin-bottom: 0.5rem; font-size: 0.9rem">Website</label>
|
||||
<input type="url" id="edit-profile-website" class="template-input" placeholder="https://..." />
|
||||
</div>
|
||||
<div style="margin-bottom: 1rem;">
|
||||
<label style="display: block; margin-bottom: 0.5rem; font-size: 0.9rem;">Last.fm Username</label>
|
||||
<div style="margin-bottom: 1rem">
|
||||
<label style="display: block; margin-bottom: 0.5rem; font-size: 0.9rem">Last.fm Username</label>
|
||||
<input type="text" id="edit-profile-lastfm" class="template-input" placeholder="Last.fm Username" />
|
||||
<p style="font-size: 0.8rem; color: var(--muted-foreground); margin-top: 0.5rem; line-height: 1.5;">Integrating Last.fm enables recent activity and top stats on your profile. Authorize it in <strong>Settings > Scrobbling</strong>. Note: Last.fm authorization is stored locally and must be repeated on each device.</p>
|
||||
<p style="font-size: 0.8rem; color: var(--muted-foreground); margin-top: 0.5rem; line-height: 1.5">
|
||||
Integrating Last.fm enables recent activity and top stats on your profile. Authorize it in
|
||||
<strong>Settings > Scrobbling</strong>. Note: Last.fm authorization is stored locally and
|
||||
must be repeated on each device.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<h4 style="margin-top: 1.5rem; margin-bottom: 1rem; font-size: 1rem;">Privacy</h4>
|
||||
<div class="setting-item" style="padding: 0.5rem 0; border: none;">
|
||||
|
||||
<h4 style="margin-top: 1.5rem; margin-bottom: 1rem; font-size: 1rem">Privacy</h4>
|
||||
<div class="setting-item" style="padding: 0.5rem 0; border: none">
|
||||
<div class="info"><span class="label">Public Playlists</span></div>
|
||||
<label class="toggle-switch"><input type="checkbox" id="privacy-playlists-toggle" checked><span class="slider"></span></label>
|
||||
<label class="toggle-switch"
|
||||
><input type="checkbox" id="privacy-playlists-toggle" checked /><span class="slider"></span
|
||||
></label>
|
||||
</div>
|
||||
<div class="setting-item" style="padding: 0.5rem 0; border: none;">
|
||||
<div class="setting-item" style="padding: 0.5rem 0; border: none">
|
||||
<div class="info"><span class="label">Show Last.fm Link & Stats</span></div>
|
||||
<label class="toggle-switch"><input type="checkbox" id="privacy-lastfm-toggle" checked><span class="slider"></span></label>
|
||||
<label class="toggle-switch"
|
||||
><input type="checkbox" id="privacy-lastfm-toggle" checked /><span class="slider"></span
|
||||
></label>
|
||||
</div>
|
||||
|
||||
<div class="modal-actions">
|
||||
|
|
@ -865,8 +1042,20 @@
|
|||
<div class="modal-overlay"></div>
|
||||
<div class="modal-content">
|
||||
<h3 style="text-align: center; margin-bottom: 10px">Email Authentication</h3>
|
||||
<input type="email" id="auth-email" class="template-input" placeholder="Email Address" style="margin-bottom: 0.5rem;" />
|
||||
<input type="password" id="auth-password" class="template-input" placeholder="Password" style="margin-bottom: 1rem;" />
|
||||
<input
|
||||
type="email"
|
||||
id="auth-email"
|
||||
class="template-input"
|
||||
placeholder="Email Address"
|
||||
style="margin-bottom: 0.5rem"
|
||||
/>
|
||||
<input
|
||||
type="password"
|
||||
id="auth-password"
|
||||
class="template-input"
|
||||
placeholder="Password"
|
||||
style="margin-bottom: 1rem"
|
||||
/>
|
||||
<div style="display: flex; gap: 10px; justify-content: center; margin-top: 10px">
|
||||
<button id="email-signin-btn" class="btn-primary" style="flex: 1">Sign In</button>
|
||||
<button id="email-signup-btn" class="btn-secondary" style="flex: 1">Sign Up</button>
|
||||
|
|
@ -878,7 +1067,9 @@
|
|||
>
|
||||
Forgot Password?
|
||||
</button>
|
||||
<button id="cancel-email-auth-btn" class="btn-secondary" style="width: 100%; margin-top: 10px">Cancel</button>
|
||||
<button id="cancel-email-auth-btn" class="btn-secondary" style="width: 100%; margin-top: 10px">
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -1475,8 +1666,20 @@
|
|||
<line x1="3" y1="18" x2="21" y2="18"></line>
|
||||
</svg>
|
||||
</button>
|
||||
<div class="header-account-control" style="position: relative; margin-right: 1rem;">
|
||||
<button id="header-account-btn" class="btn-icon" title="Account" style="width: 36px; height: 36px; border-radius: 50%; overflow: hidden; padding: 0; border: 1px solid var(--border);">
|
||||
<div class="header-account-control" style="position: relative; margin-right: 1rem">
|
||||
<button
|
||||
id="header-account-btn"
|
||||
class="btn-icon"
|
||||
title="Account"
|
||||
style="
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
border: 1px solid var(--border);
|
||||
"
|
||||
>
|
||||
<svg
|
||||
id="header-account-icon"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
|
|
@ -1492,7 +1695,11 @@
|
|||
<path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"></path>
|
||||
<circle cx="12" cy="7" r="4"></circle>
|
||||
</svg>
|
||||
<img id="header-account-img" src="" style="width: 100%; height: 100%; object-fit: cover; display: none;">
|
||||
<img
|
||||
id="header-account-img"
|
||||
src=""
|
||||
style="width: 100%; height: 100%; object-fit: cover; display: none"
|
||||
/>
|
||||
</button>
|
||||
<div id="header-account-dropdown" class="dropdown-menu"></div>
|
||||
</div>
|
||||
|
|
@ -2647,45 +2854,63 @@
|
|||
<div class="profile-header-container">
|
||||
<div class="profile-banner" id="profile-banner"></div>
|
||||
<div class="profile-info-section">
|
||||
<img id="profile-avatar" src="/assets/appicon.png" class="profile-avatar" alt="Avatar">
|
||||
<img id="profile-avatar" src="/assets/appicon.png" class="profile-avatar" alt="Avatar" />
|
||||
<div class="profile-details">
|
||||
<h1 id="profile-display-name">User</h1>
|
||||
<div id="profile-username" class="profile-username">@username</div>
|
||||
<div id="profile-status" class="profile-status" style="display: none;"></div>
|
||||
<div id="profile-status" class="profile-status" style="display: none"></div>
|
||||
<div id="profile-about" class="profile-about"></div>
|
||||
<div class="profile-links">
|
||||
<a id="profile-website" href="#" target="_blank" class="profile-link" style="display: none;">Website</a>
|
||||
<a id="profile-lastfm" href="#" target="_blank" class="profile-link" style="display: none;">Last.fm</a>
|
||||
<a
|
||||
id="profile-website"
|
||||
href="#"
|
||||
target="_blank"
|
||||
class="profile-link"
|
||||
style="display: none"
|
||||
>Website</a
|
||||
>
|
||||
<a
|
||||
id="profile-lastfm"
|
||||
href="#"
|
||||
target="_blank"
|
||||
class="profile-link"
|
||||
style="display: none"
|
||||
>Last.fm</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="profile-actions">
|
||||
<button id="profile-edit-btn" class="btn-secondary" style="display: none;">Edit Profile</button>
|
||||
<button id="profile-edit-btn" class="btn-secondary" style="display: none">
|
||||
Edit Profile
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="profile-content">
|
||||
<h2 class="section-title">Public Playlists</h2>
|
||||
<div class="card-grid" id="profile-playlists-container"></div>
|
||||
<div id="profile-favorite-albums-section" style="display: none; margin-top: 3rem;">
|
||||
<div id="profile-favorite-albums-section" style="display: none; margin-top: 3rem">
|
||||
<h2 class="section-title">Favorite Albums of All Time</h2>
|
||||
<div id="profile-favorite-albums-container"></div>
|
||||
</div>
|
||||
<div id="profile-recent-scrobbles-section" style="display: none; margin-top: 3rem;">
|
||||
<div style="display: flex; align-items: baseline; gap: 1rem; margin-bottom: 1rem;">
|
||||
<h2 class="section-title" style="margin-bottom: 0;">Recent Scrobbling</h2>
|
||||
<span style="font-size: 0.8rem; color: var(--muted-foreground);">Powered by Last.fm</span>
|
||||
<div id="profile-recent-scrobbles-section" style="display: none; margin-top: 3rem">
|
||||
<div style="display: flex; align-items: baseline; gap: 1rem; margin-bottom: 1rem">
|
||||
<h2 class="section-title" style="margin-bottom: 0">Recent Scrobbling</h2>
|
||||
<span style="font-size: 0.8rem; color: var(--muted-foreground)"
|
||||
>Powered by Last.fm</span
|
||||
>
|
||||
</div>
|
||||
<div class="track-list" id="profile-recent-scrobbles-container"></div>
|
||||
</div>
|
||||
<div id="profile-top-artists-section" style="display: none; margin-top: 3rem;">
|
||||
<div id="profile-top-artists-section" style="display: none; margin-top: 3rem">
|
||||
<h2 class="section-title">Top Artists</h2>
|
||||
<div class="card-grid" id="profile-top-artists-container"></div>
|
||||
</div>
|
||||
<div id="profile-top-albums-section" style="display: none; margin-top: 3rem;">
|
||||
<div id="profile-top-albums-section" style="display: none; margin-top: 3rem">
|
||||
<h2 class="section-title">Top Albums</h2>
|
||||
<div class="card-grid" id="profile-top-albums-container"></div>
|
||||
</div>
|
||||
<div id="profile-top-tracks-section" style="display: none; margin-top: 3rem;">
|
||||
<div id="profile-top-tracks-section" style="display: none; margin-top: 3rem">
|
||||
<h2 class="section-title">Top Tracks</h2>
|
||||
<div class="track-list" id="profile-top-tracks-container"></div>
|
||||
</div>
|
||||
|
|
@ -4350,9 +4575,13 @@
|
|||
<div class="setting-item">
|
||||
<div class="info">
|
||||
<span class="label">Clear Cloud Data</span>
|
||||
<span class="description">Delete all your data from the cloud (cannot be undone)</span>
|
||||
<span class="description"
|
||||
>Delete all your data from the cloud (cannot be undone)</span
|
||||
>
|
||||
</div>
|
||||
<button id="firebase-clear-cloud-btn" class="btn-secondary danger">Clear Cloud Data</button>
|
||||
<button id="firebase-clear-cloud-btn" class="btn-secondary danger">
|
||||
Clear Cloud Data
|
||||
</button>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div class="info">
|
||||
|
|
@ -4620,7 +4849,9 @@
|
|||
>
|
||||
<button id="firebase-connect-btn" class="btn-secondary">Connect with Google</button>
|
||||
<button id="toggle-email-auth-btn" class="btn-secondary">Connect with Email</button>
|
||||
<button id="view-my-profile-btn" class="btn-secondary" style="display: none;">View My Profile</button>
|
||||
<button id="view-my-profile-btn" class="btn-secondary" style="display: none">
|
||||
View My Profile
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p id="firebase-status" style="text-align: center; padding-top: 15px; color: #8b8b93">
|
||||
|
|
|
|||
283
js/profile.js
283
js/profile.js
|
|
@ -5,7 +5,6 @@ import { MusicAPI } from './music-api.js';
|
|||
import { apiSettings } from './storage.js';
|
||||
import { debounce, escapeHtml } from './utils.js';
|
||||
|
||||
|
||||
// objects execution february 29th 2027
|
||||
|
||||
const profilePage = document.getElementById('page-profile');
|
||||
|
|
@ -101,7 +100,9 @@ function setupImageUploadControl(idPrefix) {
|
|||
urlInput.value = url;
|
||||
statusEl.textContent = 'Done!';
|
||||
statusEl.style.color = '#10b981';
|
||||
setTimeout(() => { statusEl.style.display = 'none'; }, 2000);
|
||||
setTimeout(() => {
|
||||
statusEl.style.display = 'none';
|
||||
}, 2000);
|
||||
} catch (error) {
|
||||
statusEl.textContent = 'Failed - try URL';
|
||||
statusEl.style.color = '#ef4444';
|
||||
|
|
@ -123,9 +124,9 @@ const resetAvatarControl = setupImageUploadControl('edit-profile-avatar');
|
|||
const resetBannerControl = setupImageUploadControl('edit-profile-banner');
|
||||
|
||||
export async function loadProfile(username) {
|
||||
document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
|
||||
document.querySelectorAll('.page').forEach((p) => p.classList.remove('active'));
|
||||
profilePage.classList.add('active');
|
||||
|
||||
|
||||
document.getElementById('profile-banner').style.backgroundImage = '';
|
||||
document.getElementById('profile-avatar').src = '/assets/appicon.png';
|
||||
document.getElementById('profile-display-name').textContent = 'Loading...';
|
||||
|
|
@ -135,7 +136,7 @@ export async function loadProfile(username) {
|
|||
document.getElementById('profile-website').style.display = 'none';
|
||||
document.getElementById('profile-lastfm').style.display = 'none';
|
||||
document.getElementById('profile-playlists-container').innerHTML = '';
|
||||
|
||||
|
||||
const favAlbumsSection = document.getElementById('profile-favorite-albums-section');
|
||||
const favAlbumsContainer = document.getElementById('profile-favorite-albums-container');
|
||||
if (favAlbumsSection) favAlbumsSection.style.display = 'none';
|
||||
|
|
@ -145,7 +146,7 @@ export async function loadProfile(username) {
|
|||
const recentContainer = document.getElementById('profile-recent-scrobbles-container');
|
||||
if (recentSection) recentSection.style.display = 'none';
|
||||
if (recentContainer) recentContainer.innerHTML = '';
|
||||
|
||||
|
||||
const topArtistsSection = document.getElementById('profile-top-artists-section');
|
||||
const topArtistsContainer = document.getElementById('profile-top-artists-container');
|
||||
const topAlbumsSection = document.getElementById('profile-top-albums-section');
|
||||
|
|
@ -174,7 +175,7 @@ export async function loadProfile(username) {
|
|||
document.getElementById('profile-display-name').textContent = profile.display_name || username;
|
||||
if (profile.banner) document.getElementById('profile-banner').style.backgroundImage = `url('${profile.banner}')`;
|
||||
if (profile.avatar_url) document.getElementById('profile-avatar').src = profile.avatar_url;
|
||||
|
||||
|
||||
if (profile.status) {
|
||||
const statusEl = document.getElementById('profile-status');
|
||||
try {
|
||||
|
|
@ -184,7 +185,10 @@ export async function loadProfile(username) {
|
|||
<img src="${statusObj.image}" style="width: 20px; height: 20px; border-radius: 2px; vertical-align: middle; margin-right: 0.5rem;">
|
||||
<a href="${statusObj.link}" class="status-link" style="color: inherit; text-decoration: none; font-weight: 500;">${statusObj.text}</a>
|
||||
`;
|
||||
statusEl.querySelector('.status-link').onclick = (e) => { e.preventDefault(); navigate(statusObj.link); };
|
||||
statusEl.querySelector('.status-link').onclick = (e) => {
|
||||
e.preventDefault();
|
||||
navigate(statusObj.link);
|
||||
};
|
||||
} catch {
|
||||
statusEl.textContent = `Listening to: ${profile.status}`;
|
||||
}
|
||||
|
|
@ -204,9 +208,10 @@ export async function loadProfile(username) {
|
|||
if (profile.favorite_albums && profile.favorite_albums.length > 0) {
|
||||
if (favAlbumsSection && favAlbumsContainer) {
|
||||
favAlbumsSection.style.display = 'block';
|
||||
favAlbumsContainer.innerHTML = profile.favorite_albums.map(album => {
|
||||
const image = api.getCoverUrl(album.cover);
|
||||
return `
|
||||
favAlbumsContainer.innerHTML = profile.favorite_albums
|
||||
.map((album) => {
|
||||
const image = api.getCoverUrl(album.cover);
|
||||
return `
|
||||
<div class="favorite-album-item" style="display: flex; gap: 1rem; margin-bottom: 1rem; background: var(--card); padding: 1rem; border-radius: var(--radius); border: 1px solid var(--border);">
|
||||
<div class="card" style="width: 120px; flex-shrink: 0; padding: 0; border: none; background: transparent; cursor: pointer;" onclick="window.location.hash='/album/${album.id}'">
|
||||
<div class="card-image-wrapper" style="margin-bottom: 0.5rem;">
|
||||
|
|
@ -223,13 +228,13 @@ export async function loadProfile(username) {
|
|||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
})
|
||||
.join('');
|
||||
}
|
||||
}
|
||||
|
||||
const dataSource = profile.profile_data_source || (profile.lastfm_username ? 'lastfm' : null);
|
||||
|
||||
|
||||
if (profile.lastfm_username && profile.privacy?.lastfm !== 'private') {
|
||||
const lfmEl = document.getElementById('profile-lastfm');
|
||||
lfmEl.href = `https://last.fm/user/${profile.lastfm_username}`;
|
||||
|
|
@ -237,26 +242,30 @@ export async function loadProfile(username) {
|
|||
}
|
||||
|
||||
if (profile.lastfm_username && profile.privacy?.lastfm !== 'private') {
|
||||
fetchLastFmRecentTracks(profile.lastfm_username).then(async tracks => {
|
||||
fetchLastFmRecentTracks(profile.lastfm_username).then(async (tracks) => {
|
||||
if (tracks.length > 0) {
|
||||
recentSection.style.display = 'block';
|
||||
recentContainer.innerHTML = tracks.map((track, index) => {
|
||||
const isNowPlaying = track['@attr']?.nowplaying === 'true';
|
||||
let image = getLastFmImage(track.image);
|
||||
const hasImage = !!image;
|
||||
if (!image) image = '/assets/appicon.png';
|
||||
|
||||
track._imgId = `scrobble-img-${index}`;
|
||||
track._needsCover = !hasImage;
|
||||
|
||||
let dateDisplay = '';
|
||||
if (isNowPlaying) dateDisplay = 'Scrobbling now';
|
||||
else if (track.date) {
|
||||
const date = new Date(track.date.uts * 1000);
|
||||
dateDisplay = date.toLocaleDateString() + ' ' + date.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'});
|
||||
}
|
||||
recentContainer.innerHTML = tracks
|
||||
.map((track, index) => {
|
||||
const isNowPlaying = track['@attr']?.nowplaying === 'true';
|
||||
let image = getLastFmImage(track.image);
|
||||
const hasImage = !!image;
|
||||
if (!image) image = '/assets/appicon.png';
|
||||
|
||||
return `
|
||||
track._imgId = `scrobble-img-${index}`;
|
||||
track._needsCover = !hasImage;
|
||||
|
||||
let dateDisplay = '';
|
||||
if (isNowPlaying) dateDisplay = 'Scrobbling now';
|
||||
else if (track.date) {
|
||||
const date = new Date(track.date.uts * 1000);
|
||||
dateDisplay =
|
||||
date.toLocaleDateString() +
|
||||
' ' +
|
||||
date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
||||
}
|
||||
|
||||
return `
|
||||
<div class="track-item lastfm-track" data-title="${escapeHtml(track.name)}" data-artist="${escapeHtml(track.artist?.['#text'] || track.artist?.name || '')}" style="grid-template-columns: 40px 1fr auto; cursor: pointer;">
|
||||
<img id="${track._imgId}" src="${image}" class="track-item-cover" style="width: 40px; height: 40px; border-radius: 4px;" loading="lazy" onerror="this.src='/assets/appicon.png'">
|
||||
<div class="track-item-info">
|
||||
|
|
@ -268,11 +277,15 @@ export async function loadProfile(username) {
|
|||
<div class="track-item-duration" style="font-size: 0.8rem; min-width: auto;">${dateDisplay}</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
})
|
||||
.join('');
|
||||
|
||||
recentContainer.querySelectorAll('.track-item').forEach(item => {
|
||||
recentContainer.querySelectorAll('.track-item').forEach((item) => {
|
||||
item.addEventListener('click', () => handleTrackClick(item.dataset.title, item.dataset.artist));
|
||||
item.addEventListener('contextmenu', (e) => { e.preventDefault(); return false; });
|
||||
item.addEventListener('contextmenu', (e) => {
|
||||
e.preventDefault();
|
||||
return false;
|
||||
});
|
||||
});
|
||||
|
||||
for (const track of tracks) {
|
||||
|
|
@ -283,19 +296,20 @@ export async function loadProfile(username) {
|
|||
}
|
||||
});
|
||||
|
||||
fetchLastFmTopArtists(profile.lastfm_username).then(async artists => {
|
||||
fetchLastFmTopArtists(profile.lastfm_username).then(async (artists) => {
|
||||
if (artists.length > 0 && topArtistsSection && topArtistsContainer) {
|
||||
topArtistsSection.style.display = 'block';
|
||||
topArtistsContainer.innerHTML = artists.map((artist, index) => {
|
||||
let image = getLastFmImage(artist.image);
|
||||
const hasImage = !!image;
|
||||
if (!image) image = '/assets/appicon.png';
|
||||
|
||||
const imgId = `top-artist-img-${index}`;
|
||||
artist._imgId = imgId;
|
||||
artist._needsCover = !hasImage;
|
||||
topArtistsContainer.innerHTML = artists
|
||||
.map((artist, index) => {
|
||||
let image = getLastFmImage(artist.image);
|
||||
const hasImage = !!image;
|
||||
if (!image) image = '/assets/appicon.png';
|
||||
|
||||
return `
|
||||
const imgId = `top-artist-img-${index}`;
|
||||
artist._imgId = imgId;
|
||||
artist._needsCover = !hasImage;
|
||||
|
||||
return `
|
||||
<div class="card artist lastfm-card" data-name="${escapeHtml(artist.name)}" style="cursor: pointer;">
|
||||
<div class="card-image-wrapper">
|
||||
<img id="${imgId}" src="${image}" class="card-image" loading="lazy" onerror="this.src='/assets/appicon.png'">
|
||||
|
|
@ -306,11 +320,15 @@ export async function loadProfile(username) {
|
|||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
})
|
||||
.join('');
|
||||
|
||||
topArtistsContainer.querySelectorAll('.card').forEach(card => {
|
||||
topArtistsContainer.querySelectorAll('.card').forEach((card) => {
|
||||
card.addEventListener('click', () => handleArtistClick(card.dataset.name));
|
||||
card.addEventListener('contextmenu', (e) => { e.preventDefault(); return false; });
|
||||
card.addEventListener('contextmenu', (e) => {
|
||||
e.preventDefault();
|
||||
return false;
|
||||
});
|
||||
});
|
||||
|
||||
for (const artist of artists) {
|
||||
|
|
@ -321,22 +339,26 @@ export async function loadProfile(username) {
|
|||
}
|
||||
});
|
||||
|
||||
fetchLastFmTopAlbums(profile.lastfm_username).then(async albums => {
|
||||
fetchLastFmTopAlbums(profile.lastfm_username).then(async (albums) => {
|
||||
if (albums.length > 0 && topAlbumsSection && topAlbumsContainer) {
|
||||
topAlbumsSection.style.display = 'block';
|
||||
topAlbumsContainer.innerHTML = albums.map((album, index) => {
|
||||
let image = getLastFmImage(album.image);
|
||||
const hasImage = !!image;
|
||||
if (!image) image = '/assets/appicon.png';
|
||||
topAlbumsContainer.innerHTML = albums
|
||||
.map((album, index) => {
|
||||
let image = getLastFmImage(album.image);
|
||||
const hasImage = !!image;
|
||||
if (!image) image = '/assets/appicon.png';
|
||||
|
||||
const imgId = `top-album-img-${index}`;
|
||||
album._imgId = imgId;
|
||||
album._needsCover = !hasImage;
|
||||
|
||||
const artistName = album.artist?.name || album.artist?.['#text'] || (typeof album.artist === 'string' ? album.artist : 'Unknown Artist');
|
||||
album._artistName = artistName;
|
||||
const imgId = `top-album-img-${index}`;
|
||||
album._imgId = imgId;
|
||||
album._needsCover = !hasImage;
|
||||
|
||||
return `
|
||||
const artistName =
|
||||
album.artist?.name ||
|
||||
album.artist?.['#text'] ||
|
||||
(typeof album.artist === 'string' ? album.artist : 'Unknown Artist');
|
||||
album._artistName = artistName;
|
||||
|
||||
return `
|
||||
<div class="card lastfm-card" data-name="${escapeHtml(album.name)}" data-artist="${escapeHtml(artistName)}" style="cursor: pointer;">
|
||||
<div class="card-image-wrapper">
|
||||
<img id="${imgId}" src="${image}" class="card-image" loading="lazy" onerror="this.src='/assets/appicon.png'">
|
||||
|
|
@ -347,11 +369,15 @@ export async function loadProfile(username) {
|
|||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
})
|
||||
.join('');
|
||||
|
||||
topAlbumsContainer.querySelectorAll('.card').forEach(card => {
|
||||
topAlbumsContainer.querySelectorAll('.card').forEach((card) => {
|
||||
card.addEventListener('click', () => handleAlbumClick(card.dataset.name, card.dataset.artist));
|
||||
card.addEventListener('contextmenu', (e) => { e.preventDefault(); return false; });
|
||||
card.addEventListener('contextmenu', (e) => {
|
||||
e.preventDefault();
|
||||
return false;
|
||||
});
|
||||
});
|
||||
|
||||
for (const album of albums) {
|
||||
|
|
@ -362,22 +388,26 @@ export async function loadProfile(username) {
|
|||
}
|
||||
});
|
||||
|
||||
fetchLastFmTopTracks(profile.lastfm_username).then(async tracks => {
|
||||
fetchLastFmTopTracks(profile.lastfm_username).then(async (tracks) => {
|
||||
if (tracks.length > 0 && topTracksSection && topTracksContainer) {
|
||||
topTracksSection.style.display = 'block';
|
||||
topTracksContainer.innerHTML = tracks.map((track, index) => {
|
||||
let image = getLastFmImage(track.image);
|
||||
const hasImage = !!image;
|
||||
if (!image) image = '/assets/appicon.png';
|
||||
topTracksContainer.innerHTML = tracks
|
||||
.map((track, index) => {
|
||||
let image = getLastFmImage(track.image);
|
||||
const hasImage = !!image;
|
||||
if (!image) image = '/assets/appicon.png';
|
||||
|
||||
const imgId = `top-track-img-${index}`;
|
||||
track._imgId = imgId;
|
||||
track._needsCover = !hasImage;
|
||||
|
||||
const artistName = track.artist?.name || track.artist?.['#text'] || (typeof track.artist === 'string' ? track.artist : 'Unknown Artist');
|
||||
track._artistName = artistName;
|
||||
const imgId = `top-track-img-${index}`;
|
||||
track._imgId = imgId;
|
||||
track._needsCover = !hasImage;
|
||||
|
||||
return `
|
||||
const artistName =
|
||||
track.artist?.name ||
|
||||
track.artist?.['#text'] ||
|
||||
(typeof track.artist === 'string' ? track.artist : 'Unknown Artist');
|
||||
track._artistName = artistName;
|
||||
|
||||
return `
|
||||
<div class="track-item lastfm-track" data-title="${escapeHtml(track.name)}" data-artist="${escapeHtml(artistName)}" style="grid-template-columns: 40px 1fr auto; cursor: pointer;">
|
||||
<img id="${imgId}" src="${image}" class="track-item-cover" style="width: 40px; height: 40px; border-radius: 4px;" loading="lazy" onerror="this.src='/assets/appicon.png'">
|
||||
<div class="track-item-info">
|
||||
|
|
@ -389,11 +419,15 @@ export async function loadProfile(username) {
|
|||
<div class="track-item-duration" style="font-size: 0.8rem; min-width: auto;">${parseInt(track.playcount).toLocaleString()} plays</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
})
|
||||
.join('');
|
||||
|
||||
topTracksContainer.querySelectorAll('.track-item').forEach(item => {
|
||||
topTracksContainer.querySelectorAll('.track-item').forEach((item) => {
|
||||
item.addEventListener('click', () => handleTrackClick(item.dataset.title, item.dataset.artist));
|
||||
item.addEventListener('contextmenu', (e) => { e.preventDefault(); return false; });
|
||||
item.addEventListener('contextmenu', (e) => {
|
||||
e.preventDefault();
|
||||
return false;
|
||||
});
|
||||
});
|
||||
|
||||
for (const track of tracks) {
|
||||
|
|
@ -415,8 +449,8 @@ export async function loadProfile(username) {
|
|||
if (profile.privacy?.playlists !== 'private' || isOwner) {
|
||||
const container = document.getElementById('profile-playlists-container');
|
||||
const playlists = profile.user_playlists || {};
|
||||
|
||||
Object.values(playlists).forEach(playlist => {
|
||||
|
||||
Object.values(playlists).forEach((playlist) => {
|
||||
if (!playlist.isPublic && !isOwner) return;
|
||||
|
||||
const card = document.createElement('div');
|
||||
|
|
@ -437,21 +471,22 @@ export async function loadProfile(username) {
|
|||
});
|
||||
|
||||
if (container.children.length === 0) {
|
||||
container.innerHTML = '<p style="color: var(--muted-foreground); grid-column: 1/-1; text-align: center;">No public playlists.</p>';
|
||||
container.innerHTML =
|
||||
'<p style="color: var(--muted-foreground); grid-column: 1/-1; text-align: center;">No public playlists.</p>';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function openEditProfile() {
|
||||
syncManager.getUserData().then(data => {
|
||||
syncManager.getUserData().then((data) => {
|
||||
if (!data || !data.profile) return;
|
||||
const p = data.profile;
|
||||
|
||||
|
||||
editUsername.value = p.username || '';
|
||||
editDisplayName.value = p.display_name || '';
|
||||
resetAvatarControl(p.avatar_url);
|
||||
resetBannerControl(p.banner);
|
||||
|
||||
|
||||
editStatusJson.value = p.status || '';
|
||||
editStatusSearch.value = '';
|
||||
if (p.status) {
|
||||
|
|
@ -476,7 +511,7 @@ export function openEditProfile() {
|
|||
editAbout.value = p.about || '';
|
||||
editWebsite.value = p.website || '';
|
||||
editLastfm.value = p.lastfm_username || '';
|
||||
|
||||
|
||||
privacyPlaylists.checked = p.privacy?.playlists !== 'private';
|
||||
privacyLastfm.checked = p.privacy?.lastfm !== 'private';
|
||||
|
||||
|
|
@ -487,7 +522,7 @@ export function openEditProfile() {
|
|||
async function saveProfile() {
|
||||
const newUsername = editUsername.value.trim();
|
||||
if (!newUsername) {
|
||||
usernameError.textContent = "Username cannot be empty";
|
||||
usernameError.textContent = 'Username cannot be empty';
|
||||
usernameError.style.display = 'block';
|
||||
return;
|
||||
}
|
||||
|
|
@ -496,7 +531,7 @@ async function saveProfile() {
|
|||
if (currentUser.profile.username !== newUsername) {
|
||||
const taken = await syncManager.isUsernameTaken(newUsername);
|
||||
if (taken) {
|
||||
usernameError.textContent = "Username is already taken";
|
||||
usernameError.textContent = 'Username is already taken';
|
||||
usernameError.style.display = 'block';
|
||||
return;
|
||||
}
|
||||
|
|
@ -518,8 +553,8 @@ async function saveProfile() {
|
|||
lastfm_username: editLastfm.value.trim(),
|
||||
privacy: {
|
||||
playlists: privacyPlaylists.checked ? 'public' : 'private',
|
||||
lastfm: privacyLastfm.checked ? 'public' : 'private'
|
||||
}
|
||||
lastfm: privacyLastfm.checked ? 'public' : 'private',
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
|
|
@ -552,7 +587,7 @@ viewMyProfileBtn.addEventListener('click', async () => {
|
|||
}
|
||||
});
|
||||
|
||||
authManager.onAuthStateChanged(user => {
|
||||
authManager.onAuthStateChanged((user) => {
|
||||
viewMyProfileBtn.style.display = user ? 'inline-block' : 'none';
|
||||
});
|
||||
|
||||
|
|
@ -585,18 +620,19 @@ const performStatusSearch = debounce(async (query) => {
|
|||
try {
|
||||
const [tracks, albums] = await Promise.all([
|
||||
api.searchTracks(query, { limit: 3 }),
|
||||
api.searchAlbums(query, { limit: 3 })
|
||||
api.searchAlbums(query, { limit: 3 }),
|
||||
]);
|
||||
|
||||
statusSearchResults.innerHTML = '';
|
||||
|
||||
|
||||
const createItem = (item, type) => {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'search-result-item';
|
||||
const title = item.title;
|
||||
const subtitle = type === 'track' ? (item.artist?.name || 'Unknown Artist') : (item.artist?.name || 'Unknown Artist');
|
||||
const subtitle =
|
||||
type === 'track' ? item.artist?.name || 'Unknown Artist' : item.artist?.name || 'Unknown Artist';
|
||||
const image = api.getCoverUrl(item.album?.cover || item.cover);
|
||||
|
||||
|
||||
div.innerHTML = `
|
||||
<img src="${image}">
|
||||
<div class="search-result-info">
|
||||
|
|
@ -604,7 +640,7 @@ const performStatusSearch = debounce(async (query) => {
|
|||
<div class="search-result-subtitle">${type === 'track' ? 'Song' : 'Album'} • ${subtitle}</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
|
||||
div.onclick = () => {
|
||||
const data = {
|
||||
type: type,
|
||||
|
|
@ -613,7 +649,7 @@ const performStatusSearch = debounce(async (query) => {
|
|||
title: title,
|
||||
subtitle: subtitle,
|
||||
image: image,
|
||||
link: `/${type}/${item.id}`
|
||||
link: `/${type}/${item.id}`,
|
||||
};
|
||||
editStatusJson.value = JSON.stringify(data);
|
||||
showStatusPreview(data);
|
||||
|
|
@ -622,10 +658,10 @@ const performStatusSearch = debounce(async (query) => {
|
|||
return div;
|
||||
};
|
||||
|
||||
tracks.items.forEach(t => statusSearchResults.appendChild(createItem(t, 'track')));
|
||||
albums.items.forEach(a => statusSearchResults.appendChild(createItem(a, 'album')));
|
||||
tracks.items.forEach((t) => statusSearchResults.appendChild(createItem(t, 'track')));
|
||||
albums.items.forEach((a) => statusSearchResults.appendChild(createItem(a, 'album')));
|
||||
|
||||
statusSearchResults.style.display = (tracks.items.length || albums.items.length) ? 'block' : 'none';
|
||||
statusSearchResults.style.display = tracks.items.length || albums.items.length ? 'block' : 'none';
|
||||
} catch (e) {
|
||||
console.error('Status search failed', e);
|
||||
}
|
||||
|
|
@ -639,7 +675,9 @@ document.addEventListener('click', (e) => {
|
|||
});
|
||||
|
||||
function renderEditFavoriteAlbums() {
|
||||
editFavoriteAlbumsList.innerHTML = currentFavoriteAlbums.map((album, index) => `
|
||||
editFavoriteAlbumsList.innerHTML = currentFavoriteAlbums
|
||||
.map(
|
||||
(album, index) => `
|
||||
<div class="edit-favorite-album-item" style="background: var(--secondary); padding: 0.5rem; border-radius: var(--radius); border: 1px solid var(--border);">
|
||||
<div style="display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.5rem;">
|
||||
<img src="${api.getCoverUrl(album.cover)}" style="width: 40px; height: 40px; border-radius: 4px; object-fit: cover;">
|
||||
|
|
@ -651,9 +689,11 @@ function renderEditFavoriteAlbums() {
|
|||
</div>
|
||||
<textarea class="template-input album-description-input" data-index="${index}" placeholder="Why is this a favorite?" style="min-height: 60px; font-size: 0.85rem; resize: vertical;">${escapeHtml(album.description || '')}</textarea>
|
||||
</div>
|
||||
`).join('');
|
||||
`
|
||||
)
|
||||
.join('');
|
||||
|
||||
editFavoriteAlbumsList.querySelectorAll('.remove-album-btn').forEach(btn => {
|
||||
editFavoriteAlbumsList.querySelectorAll('.remove-album-btn').forEach((btn) => {
|
||||
btn.onclick = () => {
|
||||
const idx = parseInt(btn.dataset.index);
|
||||
currentFavoriteAlbums.splice(idx, 1);
|
||||
|
|
@ -661,7 +701,7 @@ function renderEditFavoriteAlbums() {
|
|||
};
|
||||
});
|
||||
|
||||
editFavoriteAlbumsList.querySelectorAll('.album-description-input').forEach(input => {
|
||||
editFavoriteAlbumsList.querySelectorAll('.album-description-input').forEach((input) => {
|
||||
input.oninput = () => {
|
||||
const idx = parseInt(input.dataset.index);
|
||||
currentFavoriteAlbums[idx].description = input.value;
|
||||
|
|
@ -670,10 +710,10 @@ function renderEditFavoriteAlbums() {
|
|||
|
||||
if (currentFavoriteAlbums.length >= 5) {
|
||||
editFavoriteAlbumsSearch.disabled = true;
|
||||
editFavoriteAlbumsSearch.placeholder = "Max 5 albums reached";
|
||||
editFavoriteAlbumsSearch.placeholder = 'Max 5 albums reached';
|
||||
} else {
|
||||
editFavoriteAlbumsSearch.disabled = false;
|
||||
editFavoriteAlbumsSearch.placeholder = "Search for an album...";
|
||||
editFavoriteAlbumsSearch.placeholder = 'Search for an album...';
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -692,11 +732,11 @@ const performFavoriteAlbumSearch = debounce(async (query) => {
|
|||
return;
|
||||
}
|
||||
|
||||
results.items.forEach(album => {
|
||||
results.items.forEach((album) => {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'search-result-item';
|
||||
const image = api.getCoverUrl(album.cover);
|
||||
|
||||
|
||||
div.innerHTML = `
|
||||
<img src="${image}">
|
||||
<div class="search-result-info">
|
||||
|
|
@ -704,14 +744,14 @@ const performFavoriteAlbumSearch = debounce(async (query) => {
|
|||
<div class="search-result-subtitle">${album.artist?.name || 'Unknown Artist'}</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
|
||||
div.onclick = () => {
|
||||
currentFavoriteAlbums.push({
|
||||
id: album.id,
|
||||
title: album.title,
|
||||
artist: album.artist?.name || 'Unknown Artist',
|
||||
cover: album.cover,
|
||||
description: ''
|
||||
description: '',
|
||||
});
|
||||
renderEditFavoriteAlbums();
|
||||
editFavoriteAlbumsSearch.value = '';
|
||||
|
|
@ -728,27 +768,23 @@ const performFavoriteAlbumSearch = debounce(async (query) => {
|
|||
|
||||
editFavoriteAlbumsSearch.addEventListener('input', (e) => performFavoriteAlbumSearch(e.target.value.trim()));
|
||||
|
||||
|
||||
function getLastFmImage(images) {
|
||||
if (!images) return null;
|
||||
const imgArray = Array.isArray(images) ? images : [images];
|
||||
const sizes = ['extralarge', 'large', 'medium', 'small'];
|
||||
|
||||
const placeholders = [
|
||||
'2a96cbd8b46e442fc41c2b86b821562f',
|
||||
'c6f59c1e5e7240a4c0d427abd71f3dbb'
|
||||
];
|
||||
const placeholders = ['2a96cbd8b46e442fc41c2b86b821562f', 'c6f59c1e5e7240a4c0d427abd71f3dbb'];
|
||||
|
||||
const isValidUrl = (url) => {
|
||||
if (!url) return false;
|
||||
return !placeholders.some(ph => url.includes(ph));
|
||||
return !placeholders.some((ph) => url.includes(ph));
|
||||
};
|
||||
|
||||
for (const size of sizes) {
|
||||
const img = imgArray.find(i => i.size === size);
|
||||
const img = imgArray.find((i) => i.size === size);
|
||||
if (img && img['#text'] && isValidUrl(img['#text'])) return img['#text'];
|
||||
}
|
||||
const anyImg = imgArray.find(i => i['#text'] && isValidUrl(i['#text']));
|
||||
const anyImg = imgArray.find((i) => i['#text'] && isValidUrl(i['#text']));
|
||||
if (anyImg) return anyImg['#text'];
|
||||
return null;
|
||||
}
|
||||
|
|
@ -801,12 +837,12 @@ async function handleTrackClick(title, artist) {
|
|||
async function fetchFallbackCover(title, artist, imgId) {
|
||||
try {
|
||||
const query = `${title} ${artist}`;
|
||||
await new Promise(r => setTimeout(r, 100));
|
||||
await new Promise((r) => setTimeout(r, 100));
|
||||
const results = await api.searchTracks(query, { limit: 5 });
|
||||
let foundCover = false;
|
||||
|
||||
if (results.items && results.items.length > 0) {
|
||||
const found = results.items.find(item => item.album?.cover);
|
||||
const found = results.items.find((item) => item.album?.cover);
|
||||
if (found) {
|
||||
const newUrl = api.getCoverUrl(found.album.cover);
|
||||
const imgEl = document.getElementById(imgId);
|
||||
|
|
@ -828,12 +864,12 @@ async function fetchFallbackCover(title, artist, imgId) {
|
|||
async function fetchFallbackAlbumCover(title, artist, imgId) {
|
||||
try {
|
||||
const query = `${title} ${artist}`;
|
||||
await new Promise(r => setTimeout(r, 100));
|
||||
await new Promise((r) => setTimeout(r, 100));
|
||||
const results = await api.searchAlbums(query, { limit: 5 });
|
||||
let foundCover = false;
|
||||
|
||||
if (results.items && results.items.length > 0) {
|
||||
const found = results.items.find(item => item.cover);
|
||||
const found = results.items.find((item) => item.cover);
|
||||
if (found) {
|
||||
const newUrl = api.getCoverUrl(found.cover);
|
||||
const imgEl = document.getElementById(imgId);
|
||||
|
|
@ -854,18 +890,17 @@ async function fetchFallbackAlbumCover(title, artist, imgId) {
|
|||
|
||||
async function fetchFallbackArtistImage(artistName, imgId) {
|
||||
try {
|
||||
await new Promise(r => setTimeout(r, 100));
|
||||
await new Promise((r) => setTimeout(r, 100));
|
||||
const results = await api.searchArtists(artistName, { limit: 3 });
|
||||
if (results.items && results.items.length > 0) {
|
||||
const found = results.items.find(item => item.picture);
|
||||
const found = results.items.find((item) => item.picture);
|
||||
if (found) {
|
||||
const newUrl = api.getArtistPictureUrl(found.picture);
|
||||
const imgEl = document.getElementById(imgId);
|
||||
if (imgEl) imgEl.src = newUrl;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
async function fetchLastFmRecentTracks(username) {
|
||||
|
|
@ -920,4 +955,4 @@ async function fetchLastFmTopTracks(username) {
|
|||
console.error('Failed to fetch Last.fm top tracks', e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ export function initializeSettings(scrobbler, player, api, ui) {
|
|||
cancelEmailBtn.addEventListener('click', () => {
|
||||
authModal.classList.remove('active');
|
||||
});
|
||||
|
||||
|
||||
authModal.querySelector('.modal-overlay').addEventListener('click', () => {
|
||||
authModal.classList.remove('active');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -74,26 +74,26 @@ Create two collections: `DB_users` and `public_playlists` (do NOT use the defaul
|
|||
|
||||
#### DB_users Fields
|
||||
|
||||
| Field Name | Type | Description |
|
||||
| ------------------- | ---------- | ------------------------- |
|
||||
| `firebase_id` | Plain Text | Links to Firebase user ID |
|
||||
| `lastUpdated` | Number | Timestamp of last update |
|
||||
| `history` | JSON | User listening history |
|
||||
| `library` | JSON | User's saved library |
|
||||
| `user_playlists` | JSON | User's custom playlists |
|
||||
| `user_folders` | JSON | User's playlist folders |
|
||||
| `deleted_playlists` | JSON | Soft-deleted playlists |
|
||||
| `username` | Plain Text | Unique username |
|
||||
| `display_name` | Plain Text | Profile display name |
|
||||
| `avatar_url` | URL | Profile avatar URL |
|
||||
| `banner` | URL | Profile banner URL |
|
||||
| `status` | Plain Text | User status |
|
||||
| `about` | Plain Text | About me bio |
|
||||
| `website` | URL | Personal website URL |
|
||||
| `lastfm_username` | Plain Text | Last.fm username |
|
||||
| `privacy` | JSON | Privacy settings |
|
||||
| Field Name | Type | Description |
|
||||
| --------------------- | --------------- | --------------------------------- |
|
||||
| `firebase_id` | Plain Text | Links to Firebase user ID |
|
||||
| `lastUpdated` | Number | Timestamp of last update |
|
||||
| `history` | JSON | User listening history |
|
||||
| `library` | JSON | User's saved library |
|
||||
| `user_playlists` | JSON | User's custom playlists |
|
||||
| `user_folders` | JSON | User's playlist folders |
|
||||
| `deleted_playlists` | JSON | Soft-deleted playlists |
|
||||
| `username` | Plain Text | Unique username |
|
||||
| `display_name` | Plain Text | Profile display name |
|
||||
| `avatar_url` | URL | Profile avatar URL |
|
||||
| `banner` | URL | Profile banner URL |
|
||||
| `status` | Plain Text | User status |
|
||||
| `about` | Plain Text | About me bio |
|
||||
| `website` | URL | Personal website URL |
|
||||
| `lastfm_username` | Plain Text | Last.fm username |
|
||||
| `privacy` | JSON | Privacy settings |
|
||||
| `profile_data_source` | Select (lastfm) | Preferred data source for profile |
|
||||
| `favorite_albums` | JSON | User's favorite albums |
|
||||
| `favorite_albums` | JSON | User's favorite albums |
|
||||
|
||||
#### public_playlists Fields
|
||||
|
||||
|
|
|
|||
10
styles.css
10
styles.css
|
|
@ -7233,8 +7233,8 @@ textarea:focus {
|
|||
top: 0;
|
||||
left: 0;
|
||||
z-index: 0;
|
||||
mask-image: linear-gradient(to bottom, rgba(0,0,0,1) 50%, rgba(0,0,0,0));
|
||||
-webkit-mask-image: linear-gradient(to bottom, rgba(0,0,0,1) 50%, rgba(0,0,0,0));
|
||||
mask-image: linear-gradient(to bottom, rgb(0, 0, 0, 1) 50%, rgb(0, 0, 0, 0));
|
||||
mask-image: linear-gradient(to bottom, rgb(0, 0, 0, 1) 50%, rgb(0, 0, 0, 0));
|
||||
}
|
||||
|
||||
.profile-info-section {
|
||||
|
|
@ -7272,16 +7272,16 @@ textarea:focus {
|
|||
#profile-display-name {
|
||||
font-size: 3rem;
|
||||
font-weight: 800;
|
||||
margin: 0 0 0.5rem 0;
|
||||
margin: 0 0 0.5rem;
|
||||
line-height: 1.1;
|
||||
text-shadow: 0 4px 12px rgba(0,0,0,0.5);
|
||||
text-shadow: 0 4px 12px rgb(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.profile-username {
|
||||
color: var(--muted-foreground);
|
||||
font-size: 1.2rem;
|
||||
margin-bottom: 1rem;
|
||||
background: rgba(0,0,0,0.3);
|
||||
background: rgb(0, 0, 0, 0.3);
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: var(--radius-full);
|
||||
backdrop-filter: blur(4px);
|
||||
|
|
|
|||
Loading…
Reference in a new issue