/**
* @copyright 2026 NHCarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
import { Component, OnInit, inject, signal, computed } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { MusicService } from '../../services/music.service';
import { AuthService } from '../../services/auth.service';
import { CommentsService } from '../../services/comments.service';
import { SanitizeService } from '../../services/sanitize.service';
import { SuggestionService } from '../../services/suggestion.service';
import { PaginationComponent } from '../shared/pagination.component';
import { LikeButtonComponent } from '../shared/like-button.component';
import { Music, MusicStatus, MusicType, CreateMusicDto, UpdateMusicDto, Comment, SuggestionEntity, Link } from '@library/shared-types';
@Component({
selector: 'app-music-list',
standalone: true,
imports: [CommonModule, FormsModule, PaginationComponent, LikeButtonComponent],
template: `
@if (showAddForm() && authService.isAdmin()) {
}
@if (editingMusic() && authService.isAdmin()) {
}
@if (showSuggestForm() && !authService.isAdmin() && authService.isAuthenticated()) {
}
@if (searchQuery() || selectedTags().length > 0) {
}
@if (showFilters()) {
}
Type:
Status:
@if (loading()) {
Loading music...
} @else if (filteredMusic().length === 0) {
No music found with these filters.
} @else {
@for (music of paginatedMusic(); track music.id) {
@if (music.coverArt) {
![]()
} @else {
@switch (music.type) {
@case (MusicType.album) { 💿 }
@case (MusicType.single) { 🎵 }
@case (MusicType.ep) { 🎶 }
}
}
{{ music.title }}
{{ music.artist }}
{{ getTypeLabel(music.type) }}
{{ getStatusLabel(music.status) }}
@if (music.rating) {
@for (star of [1,2,3,4,5,6,7,8,9,10]; track star) {
★
}
}
@if (music.notes) {
{{ music.notes }}
}
@if (music.tags && music.tags.length > 0) {
@for (tag of music.tags; track tag) {
{{ tag }}
}
}
@if (music.links && music.links.length > 0) {
}
@if (music.dateCompleted) {
Completed: {{ formatDate(music.dateCompleted) }}
}
@if (authService.isAdmin()) {
}
}
}
`,
styles: [`
.container {
max-width: 1200px;
margin: 0 auto;
padding: 2rem;
}
.header-section {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
}
h2 {
margin: 0;
}
.add-form {
background: rgba(255, 255, 255, 0.95);
padding: 1.5rem;
border-radius: 8px;
margin-bottom: 2rem;
border: 2px solid var(--witch-lavender);
backdrop-filter: blur(10px);
}
.suggest-form {
border: 2px solid #74b9ff;
background: #f5faff;
}
.suggest-note {
color: #666;
font-size: 0.9rem;
margin-bottom: 1rem;
font-style: italic;
}
.form-group {
margin-bottom: 1rem;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
}
.form-group input,
.form-group select,
.form-group textarea {
width: 100%;
padding: 0.5rem;
border: 2px solid var(--witch-lavender);
border-radius: 4px;
font-size: 1rem;
background-color: var(--witch-moon);
color: var(--witch-purple);
transition: border-color 0.3s;
}
.form-group input:focus,
.form-group select:focus,
.form-group textarea:focus {
outline: none;
border-color: var(--witch-rose);
box-shadow: 0 0 0 3px rgba(168, 87, 126, 0.2);
}
.form-actions {
display: flex;
gap: 1rem;
margin-top: 1rem;
}
.search-section {
display: flex;
gap: 1rem;
align-items: center;
margin-bottom: 1rem;
flex-wrap: wrap;
}
.search-input {
flex: 1;
min-width: 250px;
padding: 0.5rem 1rem;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 1rem;
}
.search-input:focus {
outline: none;
border-color: #8b5cf6;
}
.advanced-filters {
background: #f8f9fa;
padding: 1rem;
border-radius: 8px;
margin-bottom: 1rem;
}
.filter-group h4 {
margin: 0 0 0.75rem 0;
color: #374151;
font-size: 0.95rem;
font-weight: 600;
}
.tags-filter {
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
}
.tag-checkbox {
display: flex;
align-items: center;
gap: 0.5rem;
cursor: pointer;
padding: 0.25rem 0.75rem;
border: 1px solid #e5e7eb;
border-radius: 20px;
background: white;
transition: all 0.2s;
}
.tag-checkbox:hover {
border-color: #8b5cf6;
background: #f3e8ff;
}
.tag-checkbox input[type="checkbox"] {
width: 16px;
height: 16px;
cursor: pointer;
}
.tag-checkbox span {
font-size: 0.875rem;
color: #374151;
}
.no-tags {
color: #6b7280;
font-style: italic;
font-size: 0.875rem;
}
.filters {
display: flex;
flex-direction: column;
gap: 1rem;
margin-bottom: 2rem;
}
.filter-group {
display: flex;
align-items: center;
gap: 1rem;
flex-wrap: wrap;
}
.filter-group strong {
min-width: 60px;
color: var(--witch-plum);
}
.filter-btn {
padding: 0.5rem 1rem;
background: var(--witch-lavender);
color: var(--witch-purple);
border: none;
border-radius: 4px;
cursor: pointer;
transition: all 0.3s;
}
.filter-btn:hover {
background: var(--witch-mauve);
transform: translateY(-2px);
box-shadow: 0 4px 8px var(--witch-shadow);
}
.filter-btn.active {
background: #74b9ff;
color: white;
}
.loading {
text-align: center;
padding: 2rem;
color: var(--witch-plum);
}
.empty-state {
text-align: center;
padding: 3rem;
color: var(--witch-plum);
}
.music-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 1.5rem;
}
.music-card {
background: rgba(255, 255, 255, 0.95);
border: 2px solid var(--witch-lavender);
border-radius: 8px;
overflow: hidden;
transition: all 0.3s;
backdrop-filter: blur(10px);
}
.music-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px var(--witch-shadow);
border-color: var(--witch-mauve);
}
.music-card.completed {
opacity: 0.9;
border-color: var(--witch-mauve);
}
.music-cover {
width: 100%;
height: 250px;
object-fit: cover;
}
.music-cover.placeholder {
display: flex;
align-items: center;
justify-content: center;
background: var(--witch-lavender);
font-size: 4rem;
}
.music-info {
padding: 1rem;
}
.music-info h3 {
margin: 0 0 0.5rem 0;
font-size: 1.1rem;
}
.artist {
color: var(--witch-plum);
font-weight: 500;
margin: 0.5rem 0;
}
.badges {
display: flex;
gap: 0.5rem;
margin: 0.75rem 0;
}
.type-badge {
display: inline-block;
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-size: 0.8rem;
font-weight: 500;
}
.type-album {
background: var(--witch-purple);
color: var(--witch-moon);
}
.type-single {
background: var(--witch-rose);
color: var(--witch-moon);
}
.type-ep {
background: var(--witch-plum);
color: var(--witch-moon);
}
.status {
display: inline-block;
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-size: 0.8rem;
font-weight: 500;
}
.status-listening {
background: var(--witch-rose);
color: var(--witch-moon);
}
.status-completed {
background: var(--witch-mauve);
color: var(--witch-purple);
}
.status-wantToListen {
background: var(--witch-lavender);
color: var(--witch-purple);
}
.rating {
margin: 0.5rem 0;
}
.rating span {
color: var(--witch-lavender);
font-size: 1.2rem;
}
.rating span.filled {
color: var(--witch-rose);
}
.notes {
font-size: 0.9rem;
color: var(--witch-plum);
margin: 0.5rem 0;
}
.date-completed {
font-size: 0.85rem;
color: var(--witch-plum);
margin-top: 0.5rem;
}
.actions {
margin-top: 1rem;
}
.btn {
padding: 0.5rem 1rem;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 0.9rem;
transition: opacity 0.3s;
}
.btn:hover {
opacity: 0.8;
}
.btn-primary {
background: #74b9ff;
color: white;
}
.btn-primary:hover {
background: #4a9ff5;
transform: translateY(-2px);
box-shadow: 0 4px 8px var(--witch-shadow);
}
.btn-secondary {
background: #6b7280;
color: white;
}
.btn-secondary:hover {
background: #4b5563;
color: white;
}
.btn-danger {
background: #ef4444;
color: white;
}
.btn-danger:hover {
background: #dc2626;
}
.btn-sm {
padding: 0.25rem 0.75rem;
font-size: 0.85rem;
}
.btn-xs { padding: 0.15rem 0.5rem; font-size: 0.75rem; }
.comments-section {
margin-top: 1rem;
border-top: 1px solid var(--witch-lavender);
padding-top: 1rem;
}
.comments-toggle {
width: 100%;
}
.comments-container {
margin-top: 1rem;
}
.comment-form {
margin-bottom: 1rem;
}
.comment-form textarea {
width: 100%;
padding: 0.5rem;
border: 2px solid var(--witch-lavender);
border-radius: 4px;
font-size: 0.9rem;
background-color: var(--witch-moon);
color: var(--witch-purple);
resize: vertical;
margin-bottom: 0.5rem;
}
.comment-form textarea:focus {
outline: none;
border-color: var(--witch-rose);
}
.comment {
background: var(--witch-moon);
border: 1px solid var(--witch-lavender);
border-radius: 4px;
padding: 0.75rem;
margin-bottom: 0.5rem;
}
.comment-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.5rem;
flex-wrap: wrap;
}
.comment-avatar {
width: 24px;
height: 24px;
border-radius: 50%;
}
.comment-author {
font-weight: 500;
color: var(--witch-plum);
}
.discord-badge {
background: #5865f2;
color: white;
padding: 0.1rem 0.4rem;
border-radius: 3px;
font-size: 0.7rem;
font-weight: 500;
}
.vip-badge {
background: linear-gradient(135deg, #ffd700, #ffaa00);
color: #1a1a1a;
padding: 0.1rem 0.4rem;
border-radius: 3px;
font-size: 0.7rem;
font-weight: 600;
}
.mod-badge {
background: linear-gradient(135deg, #00b894, #00cec9);
color: white;
padding: 0.1rem 0.4rem;
border-radius: 3px;
font-size: 0.7rem;
font-weight: 600;
}
.staff-badge {
background: linear-gradient(135deg, #e84393, #fd79a8);
color: white;
padding: 0.1rem 0.4rem;
border-radius: 3px;
font-size: 0.7rem;
font-weight: 600;
}
.comment-date {
font-size: 0.75rem;
color: var(--witch-mauve);
}
.comment-content {
font-size: 0.9rem;
color: var(--witch-purple);
}
.comments-loading,
.no-comments {
text-align: center;
padding: 1rem;
color: var(--witch-mauve);
font-size: 0.9rem;
}
.banned-notice {
background: #fef2f2;
border: 1px solid #fecaca;
color: #991b1b;
padding: 0.75rem 1rem;
border-radius: 4px;
text-align: center;
margin-bottom: 1rem;
font-size: 0.9rem;
}
.comment-edit-form {
margin-top: 0.5rem;
}
.comment-edit-form textarea {
width: 100%;
padding: 0.5rem;
border: 2px solid var(--witch-lavender);
border-radius: 4px;
font-size: 0.9rem;
background-color: var(--witch-moon);
color: var(--witch-purple);
resize: vertical;
}
.comment-edit-form textarea:focus {
outline: none;
border-color: var(--witch-rose);
}
.comment-edit-actions {
display: flex;
gap: 0.5rem;
margin-top: 0.5rem;
}
.image-preview {
margin-top: 0.5rem;
display: flex;
align-items: center;
gap: 1rem;
}
.image-preview img {
max-width: 100px;
max-height: 100px;
border-radius: 4px;
border: 2px solid var(--witch-lavender);
}
.error-text {
color: var(--witch-rose);
font-size: 0.875rem;
display: block;
margin-top: 0.25rem;
}
input[type="file"] {
padding: 0.5rem;
border: 2px dashed var(--witch-lavender);
border-radius: 4px;
background: var(--witch-moon);
cursor: pointer;
}
input[type="file"]:hover {
border-color: var(--witch-rose);
}
.tags-input-container {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
padding: 0.5rem;
border: 2px solid var(--witch-lavender);
border-radius: 4px;
background: var(--witch-moon);
}
.tags-input-container input {
flex: 1;
min-width: 150px;
border: none;
padding: 0.25rem;
font-size: 0.9rem;
background: transparent;
}
.tags-input-container input:focus {
outline: none;
}
.tag {
display: inline-flex;
align-items: center;
gap: 0.25rem;
background: #74b9ff;
color: white;
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-size: 0.85rem;
}
.tag-remove {
background: none;
border: none;
color: white;
cursor: pointer;
padding: 0;
font-size: 1rem;
line-height: 1;
}
.tag-remove:hover {
opacity: 0.8;
}
.tags-display {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin: 0.5rem 0;
}
.tag-chip {
background: rgba(116, 185, 255, 0.2);
color: #74b9ff;
padding: 0.2rem 0.5rem;
border-radius: 4px;
font-size: 0.8rem;
}
.links-list {
margin-bottom: 0.5rem;
}
.link-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.5rem;
background: var(--witch-moon);
border-radius: 4px;
margin-bottom: 0.25rem;
}
.link-add-form {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
.link-add-form input {
flex: 1;
min-width: 120px;
}
.links-display {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin: 0.5rem 0;
}
.external-link {
color: #74b9ff;
text-decoration: none;
font-size: 0.85rem;
padding: 0.2rem 0.5rem;
background: rgba(116, 185, 255, 0.1);
border-radius: 4px;
transition: background 0.2s;
}
.external-link:hover {
background: rgba(116, 185, 255, 0.2);
text-decoration: underline;
}
`]
})
export class MusicListComponent implements OnInit {
musicService = inject(MusicService);
authService = inject(AuthService);
commentsService = inject(CommentsService);
sanitizeService = inject(SanitizeService);
suggestionService = inject(SuggestionService);
music = signal([]);
loading = signal(true);
showAddForm = signal(false);
editingMusic = signal(null);
typeFilter = signal<'all' | MusicType>('all');
statusFilter = signal<'all' | MusicStatus>('all');
// Pagination state
currentPage = signal(1);
pageSize = signal(25);
// Search and filter state
searchQuery = signal('');
selectedTags = signal([]);
showFilters = signal(false);
// Comments state
comments = signal>({});
commentsLoading = signal>({});
expandedComments = signal>({});
newCommentContent: Record = {};
editingCommentId = signal(null);
editCommentContent = '';
// Image upload state
newMusicImagePreview = signal(null);
editMusicImagePreview = signal(null);
suggestMusicImagePreview = signal(null);
imageError = signal(null);
private readonly MAX_IMAGE_SIZE = 500 * 1024; // 500KB
// Suggestion state
showSuggestForm = signal(false);
suggestedMusic: { title: string; artist: string; type: MusicType; notes?: string; coverArt?: string } = {
title: '',
artist: '',
type: MusicType.album,
notes: '',
coverArt: undefined
};
// Expose enums to template
MusicType = MusicType;
MusicStatus = MusicStatus;
// Computed signals for reactive count updates (type)
albumCount = computed(() => this.music().filter(m => m.type === MusicType.album).length);
singleCount = computed(() => this.music().filter(m => m.type === MusicType.single).length);
epCount = computed(() => this.music().filter(m => m.type === MusicType.ep).length);
// Computed signals for reactive count updates (status)
listeningCount = computed(() => this.music().filter(m => m.status === MusicStatus.listening).length);
completedCount = computed(() => this.music().filter(m => m.status === MusicStatus.completed).length);
wantToListenCount = computed(() => this.music().filter(m => m.status === MusicStatus.wantToListen).length);
allTags = computed(() => {
const tagsSet = new Set();
this.music().forEach(music => {
music.tags?.forEach(tag => tagsSet.add(tag));
});
return Array.from(tagsSet).sort();
});
filteredMusic = computed(() => {
let filtered = this.music();
const typeFilter = this.typeFilter();
if (typeFilter !== 'all') {
filtered = filtered.filter(music => music.type === typeFilter);
}
const statusFilter = this.statusFilter();
if (statusFilter !== 'all') {
filtered = filtered.filter(music => music.status === statusFilter);
}
// Apply search filter
const searchQuery = this.searchQuery().toLowerCase().trim();
if (searchQuery) {
filtered = filtered.filter(music =>
music.title.toLowerCase().includes(searchQuery) ||
music.artist.toLowerCase().includes(searchQuery) ||
music.notes?.toLowerCase().includes(searchQuery) ||
this.getTypeLabel(music.type).toLowerCase().includes(searchQuery)
);
}
// Apply tag filter
const selectedTags = this.selectedTags();
if (selectedTags.length > 0) {
filtered = filtered.filter(music =>
selectedTags.every(tag => music.tags?.includes(tag))
);
}
return filtered;
});
paginatedMusic = computed(() => {
const music = this.filteredMusic();
const start = (this.currentPage() - 1) * this.pageSize();
const end = start + this.pageSize();
return music.slice(start, end);
});
totalFilteredMusic = computed(() => this.filteredMusic().length);
newMusic: Partial = {
title: '',
artist: '',
type: MusicType.album,
status: MusicStatus.wantToListen,
rating: undefined,
notes: '',
tags: [],
links: []
};
editMusicData: Partial = {};
// Tags and links input state
newTagInput = '';
editTagInput = '';
newLinkTitle = '';
newLinkUrl = '';
editLinkTitle = '';
editLinkUrl = '';
ngOnInit() {
this.loadMusic();
}
loadMusic() {
this.loading.set(true);
this.musicService.getAllMusic().subscribe({
next: (music) => {
this.music.set(music);
this.loading.set(false);
},
error: () => {
this.loading.set(false);
}
});
}
setTypeFilter(filter: 'all' | MusicType) {
this.typeFilter.set(filter);
this.currentPage.set(1); // Reset to first page when filter changes
}
setStatusFilter(filter: 'all' | MusicStatus) {
this.statusFilter.set(filter);
this.currentPage.set(1); // Reset to first page when filter changes
}
toggleTag(tag: string) {
const current = this.selectedTags();
if (current.includes(tag)) {
this.selectedTags.set(current.filter(t => t !== tag));
} else {
this.selectedTags.set([...current, tag]);
}
this.currentPage.set(1); // Reset to first page when tags change
}
clearFilters() {
this.searchQuery.set('');
this.selectedTags.set([]);
this.typeFilter.set('all');
this.statusFilter.set('all');
this.currentPage.set(1);
}
toggleFilters() {
this.showFilters.update(v => !v);
}
onPageChange(page: number) {
this.currentPage.set(page);
}
onPageSizeChange(pageSize: number) {
this.pageSize.set(pageSize);
// Calculate new current page to stay on approximately the same content
const firstItemIndex = (this.currentPage() - 1) * this.pageSize();
const newPage = Math.floor(firstItemIndex / pageSize) + 1;
this.currentPage.set(newPage);
}
getTypeLabel(type: MusicType): string {
switch (type) {
case MusicType.album: return 'Album';
case MusicType.single: return 'Single';
case MusicType.ep: return 'EP';
}
}
getStatusLabel(status: MusicStatus): string {
switch (status) {
case MusicStatus.listening: return 'Currently Listening';
case MusicStatus.completed: return 'Completed';
case MusicStatus.wantToListen: return 'Want to Listen';
}
}
toggleAddForm() {
this.showAddForm.update(v => !v);
if (!this.showAddForm()) {
this.resetForm();
}
}
resetForm() {
this.newMusic = {
title: '',
artist: '',
type: MusicType.album,
status: MusicStatus.wantToListen,
rating: undefined,
notes: '',
coverArt: undefined,
tags: [],
links: []
};
this.newMusicImagePreview.set(null);
this.imageError.set(null);
this.newTagInput = '';
this.newLinkTitle = '';
this.newLinkUrl = '';
}
addTag(target: 'new' | 'edit') {
const input = target === 'new' ? this.newTagInput.trim() : this.editTagInput.trim();
if (!input) return;
if (target === 'new') {
this.newMusic.tags = [...(this.newMusic.tags || []), input];
this.newTagInput = '';
} else {
this.editMusicData.tags = [...(this.editMusicData.tags || []), input];
this.editTagInput = '';
}
}
removeTag(index: number, target: 'new' | 'edit') {
if (target === 'new') {
this.newMusic.tags = (this.newMusic.tags || []).filter((_, i) => i !== index);
} else {
this.editMusicData.tags = (this.editMusicData.tags || []).filter((_, i) => i !== index);
}
}
addLink(target: 'new' | 'edit') {
const title = target === 'new' ? this.newLinkTitle.trim() : this.editLinkTitle.trim();
const url = target === 'new' ? this.newLinkUrl.trim() : this.editLinkUrl.trim();
if (!title || !url) return;
if (target === 'new') {
this.newMusic.links = [...(this.newMusic.links || []), { title, url }];
this.newLinkTitle = '';
this.newLinkUrl = '';
} else {
this.editMusicData.links = [...(this.editMusicData.links || []), { title, url }];
this.editLinkTitle = '';
this.editLinkUrl = '';
}
}
removeLink(index: number, target: 'new' | 'edit') {
if (target === 'new') {
this.newMusic.links = (this.newMusic.links || []).filter((_, i) => i !== index);
} else {
this.editMusicData.links = (this.editMusicData.links || []).filter((_, i) => i !== index);
}
}
addMusic() {
if (!this.newMusic.title || !this.newMusic.artist || !this.newMusic.type || !this.newMusic.status) return;
const musicToAdd: CreateMusicDto = {
title: this.newMusic.title,
artist: this.newMusic.artist,
type: this.newMusic.type,
status: this.newMusic.status,
rating: this.newMusic.rating,
notes: this.newMusic.notes,
coverArt: this.newMusic.coverArt,
tags: this.newMusic.tags || [],
links: this.newMusic.links || []
};
this.musicService.createMusic(musicToAdd).subscribe(() => {
this.loadMusic();
this.toggleAddForm();
});
}
deleteMusic(music: Music) {
if (confirm(`Are you sure you want to delete "${music.title}" by ${music.artist}?`)) {
this.musicService.deleteMusic(music.id).subscribe(() => {
this.loadMusic();
});
}
}
startEdit(music: Music) {
this.editingMusic.set(music);
this.editMusicData = {
title: music.title,
artist: music.artist,
type: music.type,
status: music.status,
rating: music.rating,
notes: music.notes,
coverArt: music.coverArt,
tags: [...(music.tags || [])],
links: [...(music.links || [])]
};
this.editMusicImagePreview.set(music.coverArt || null);
this.showAddForm.set(false);
this.imageError.set(null);
this.editTagInput = '';
this.editLinkTitle = '';
this.editLinkUrl = '';
}
cancelEdit() {
this.editingMusic.set(null);
this.editMusicData = {};
this.editMusicImagePreview.set(null);
this.imageError.set(null);
this.editTagInput = '';
this.editLinkTitle = '';
this.editLinkUrl = '';
}
saveEdit() {
const music = this.editingMusic();
if (!music || !this.editMusicData.title || !this.editMusicData.artist || !this.editMusicData.type || !this.editMusicData.status) return;
this.musicService.updateMusic(music.id, this.editMusicData).subscribe(() => {
this.loadMusic();
this.cancelEdit();
});
}
formatDate(date: Date | string): string {
return new Date(date).toLocaleDateString();
}
// Image handling methods
onImageSelected(event: Event, target: 'new' | 'edit' | 'suggest') {
const input = event.target as HTMLInputElement;
const file = input.files?.[0];
if (!file) return;
this.imageError.set(null);
if (file.size > this.MAX_IMAGE_SIZE) {
this.imageError.set(`Image too large. Maximum size is ${this.MAX_IMAGE_SIZE / 1024}KB.`);
input.value = '';
return;
}
if (!file.type.startsWith('image/')) {
this.imageError.set('Please select an image file.');
input.value = '';
return;
}
const reader = new FileReader();
reader.onload = () => {
const base64 = reader.result as string;
if (target === 'new') {
this.newMusicImagePreview.set(base64);
this.newMusic.coverArt = base64;
} else if (target === 'edit') {
this.editMusicImagePreview.set(base64);
this.editMusicData.coverArt = base64;
} else {
this.suggestMusicImagePreview.set(base64);
this.suggestedMusic.coverArt = base64;
}
};
reader.readAsDataURL(file);
}
clearImage(target: 'new' | 'edit' | 'suggest') {
if (target === 'new') {
this.newMusicImagePreview.set(null);
this.newMusic.coverArt = undefined;
} else if (target === 'edit') {
this.editMusicImagePreview.set(null);
this.editMusicData.coverArt = undefined;
} else {
this.suggestMusicImagePreview.set(null);
this.suggestedMusic.coverArt = undefined;
}
this.imageError.set(null);
}
// Comments methods
toggleComments(musicId: string) {
const expanded = this.expandedComments();
const isCurrentlyExpanded = expanded[musicId];
this.expandedComments.set({
...expanded,
[musicId]: !isCurrentlyExpanded
});
if (!isCurrentlyExpanded && !this.comments()[musicId]) {
this.loadComments(musicId);
}
}
loadComments(musicId: string) {
this.commentsLoading.set({
...this.commentsLoading(),
[musicId]: true
});
this.commentsService.getCommentsForMusic(musicId).subscribe({
next: (comments) => {
this.comments.set({
...this.comments(),
[musicId]: comments
});
this.commentsLoading.set({
...this.commentsLoading(),
[musicId]: false
});
},
error: () => {
this.commentsLoading.set({
...this.commentsLoading(),
[musicId]: false
});
}
});
}
getCommentCount(musicId: string): number {
return this.comments()[musicId]?.length || 0;
}
addComment(musicId: string) {
const content = this.newCommentContent[musicId];
if (!content?.trim()) return;
this.commentsService.addCommentToMusic(musicId, { content }).subscribe({
next: (comment) => {
this.comments.set({
...this.comments(),
[musicId]: [comment, ...(this.comments()[musicId] || [])]
});
this.newCommentContent[musicId] = '';
}
});
}
deleteComment(musicId: string, commentId: string) {
if (!confirm('Are you sure you want to delete this comment?')) return;
this.commentsService.deleteCommentFromMusic(musicId, commentId).subscribe({
next: () => {
this.comments.set({
...this.comments(),
[musicId]: (this.comments()[musicId] || []).filter(c => c.id !== commentId)
});
}
});
}
canEditComment(comment: Comment): boolean {
const user = this.authService.user();
if (!user) return false;
return comment.userId === user.id || this.authService.isAdmin();
}
canDeleteComment(comment: Comment): boolean {
const user = this.authService.user();
if (!user) return false;
return comment.userId === user.id || this.authService.isAdmin();
}
startEditComment(musicId: string, comment: Comment) {
this.editingCommentId.set(comment.id);
this.editCommentContent = comment.rawContent ?? comment.content;
}
cancelCommentEdit() {
this.editingCommentId.set(null);
this.editCommentContent = '';
}
saveCommentEdit(musicId: string, commentId: string) {
if (!this.editCommentContent.trim()) return;
this.commentsService.updateCommentOnMusic(musicId, commentId, this.editCommentContent).subscribe({
next: (updatedComment) => {
this.comments.set({
...this.comments(),
[musicId]: (this.comments()[musicId] || []).map(c =>
c.id === commentId ? updatedComment : c
)
});
this.cancelCommentEdit();
}
});
}
// Suggestion methods
toggleSuggestForm() {
this.showSuggestForm.update(v => !v);
if (!this.showSuggestForm()) {
this.resetSuggestForm();
}
}
resetSuggestForm() {
this.suggestedMusic = {
title: '',
artist: '',
type: MusicType.album,
notes: '',
coverArt: undefined
};
this.suggestMusicImagePreview.set(null);
this.imageError.set(null);
}
async submitSuggestion() {
if (!this.suggestedMusic.title || !this.suggestedMusic.artist) return;
try {
await this.suggestionService.createSuggestion({
entityType: SuggestionEntity.MUSIC,
title: this.suggestedMusic.title,
artist: this.suggestedMusic.artist,
type: this.suggestedMusic.type,
notes: this.suggestedMusic.notes,
coverArt: this.suggestedMusic.coverArt
});
alert('Thank you for your suggestion! It will be reviewed soon.');
this.toggleSuggestForm();
} catch {
alert('Failed to submit suggestion. Please try again.');
}
}
}