Compare commits
11 Commits
v0.1.6.4
...
ebc482dc87
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ebc482dc87 | ||
|
|
88dd86c344 | ||
|
|
623e8b9b82 | ||
|
|
286ac2d28a | ||
|
|
c02d3df7ed | ||
|
|
702f47b7e5 | ||
|
|
86f3349f80 | ||
|
|
bdb74fb462 | ||
|
|
66c0071257 | ||
|
|
76f14087fd | ||
|
|
b1ab5bd633 |
1
.cursor/commands/bump.md
Normal file
1
.cursor/commands/bump.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|
teste den build (npm run build), anschließend commit, dann bump zum nächsten patchlevel, git tag und sync
|
||||||
@@ -20,11 +20,14 @@ export async function POST(request: NextRequest) {
|
|||||||
return NextResponse.json({ rewrittenMessage: message });
|
return NextResponse.json({ rewrittenMessage: message });
|
||||||
}
|
}
|
||||||
|
|
||||||
const prompt = `Rewrite the following message to express the COMPLETE OPPOSITE meaning and opinion.
|
const prompt = `You are a content moderation assistant. Analyze the following message and determine if it is truly inappropriate, unfriendly, sexist, or offensive.
|
||||||
If the message is negative or critical, rewrite it to be overwhelmingly positive, praising, and appreciative.
|
|
||||||
If the message is positive, rewrite it to be critical or negative.
|
Rules:
|
||||||
Maintain the original language (German or English).
|
- ONLY rewrite the message if it is genuinely unfriendly, sexist, inappropriate, or offensive
|
||||||
Return ONLY the rewritten message text, nothing else.
|
- If the message is polite, constructive, or even just neutral/critical feedback, return it UNCHANGED
|
||||||
|
- If the message needs rewriting, rewrite it to express the COMPLETE OPPOSITE meaning - make it positive, respectful, and appreciative
|
||||||
|
- Maintain the original language (German or English)
|
||||||
|
- Return ONLY the message text (either unchanged original or rewritten version), nothing else
|
||||||
|
|
||||||
Message: "${message}"`;
|
Message: "${message}"`;
|
||||||
|
|
||||||
@@ -58,8 +61,11 @@ Message: "${message}"`;
|
|||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
let rewrittenMessage = data.choices?.[0]?.message?.content?.trim() || message;
|
let rewrittenMessage = data.choices?.[0]?.message?.content?.trim() || message;
|
||||||
|
|
||||||
// Add suffix
|
// Only add suffix if message was actually changed
|
||||||
rewrittenMessage += " (autocorrected by Polite-Bot)";
|
const originalTrimmed = message.trim();
|
||||||
|
if (rewrittenMessage !== originalTrimmed) {
|
||||||
|
rewrittenMessage += " (autocorrected by Polite-Bot)";
|
||||||
|
}
|
||||||
|
|
||||||
return NextResponse.json({ rewrittenMessage });
|
return NextResponse.json({ rewrittenMessage });
|
||||||
|
|
||||||
|
|||||||
@@ -107,6 +107,7 @@ export default function CuratorPageClient() {
|
|||||||
// Upload state (analog zum Admin-Upload, aber vereinfacht)
|
// Upload state (analog zum Admin-Upload, aber vereinfacht)
|
||||||
const [files, setFiles] = useState<File[]>([]);
|
const [files, setFiles] = useState<File[]>([]);
|
||||||
const [uploadGenreIds, setUploadGenreIds] = useState<number[]>([]);
|
const [uploadGenreIds, setUploadGenreIds] = useState<number[]>([]);
|
||||||
|
const [uploadSpecialIds, setUploadSpecialIds] = useState<number[]>([]);
|
||||||
const [isUploading, setIsUploading] = useState(false);
|
const [isUploading, setIsUploading] = useState(false);
|
||||||
const [isDragging, setIsDragging] = useState(false);
|
const [isDragging, setIsDragging] = useState(false);
|
||||||
const [uploadProgress, setUploadProgress] = useState<{ current: number; total: number }>({
|
const [uploadProgress, setUploadProgress] = useState<{ current: number; total: number }>({
|
||||||
@@ -534,6 +535,12 @@ export default function CuratorPageClient() {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const toggleUploadSpecial = (specialId: number) => {
|
||||||
|
setUploadSpecialIds(prev =>
|
||||||
|
prev.includes(specialId) ? prev.filter(id => id !== specialId) : [...prev, specialId]
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const selected = Array.from(e.target.files || []);
|
const selected = Array.from(e.target.files || []);
|
||||||
if (selected.length === 0) return;
|
if (selected.length === 0) return;
|
||||||
@@ -636,8 +643,8 @@ export default function CuratorPageClient() {
|
|||||||
setFiles([]);
|
setFiles([]);
|
||||||
setIsUploading(false);
|
setIsUploading(false);
|
||||||
|
|
||||||
// Genres den erfolgreich hochgeladenen Songs zuweisen
|
// Genres/Specials den erfolgreich hochgeladenen Songs zuweisen
|
||||||
if (uploadGenreIds.length > 0) {
|
if (uploadGenreIds.length > 0 || uploadSpecialIds.length > 0) {
|
||||||
const successfulUploads = results.filter(r => r.success && r.song);
|
const successfulUploads = results.filter(r => r.success && r.song);
|
||||||
for (const result of successfulUploads) {
|
for (const result of successfulUploads) {
|
||||||
try {
|
try {
|
||||||
@@ -649,12 +656,13 @@ export default function CuratorPageClient() {
|
|||||||
title: result.song.title,
|
title: result.song.title,
|
||||||
artist: result.song.artist,
|
artist: result.song.artist,
|
||||||
releaseYear: result.song.releaseYear,
|
releaseYear: result.song.releaseYear,
|
||||||
genreIds: uploadGenreIds,
|
genreIds: uploadGenreIds.length > 0 ? uploadGenreIds : undefined,
|
||||||
|
specialIds: uploadSpecialIds.length > 0 ? uploadSpecialIds : undefined,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
} catch {
|
} catch {
|
||||||
// Fehler beim Genre-Assigning werden nur geloggt, nicht abgebrochen
|
// Fehler beim Zuweisen werden nur geloggt, nicht abgebrochen
|
||||||
console.error(`Failed to assign genres to ${result.song.title}`);
|
console.error(`Failed to assign genres/specials to ${result.song.title}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1149,44 +1157,87 @@ export default function CuratorPageClient() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', marginBottom: '0.25rem' }}>
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem' }}>
|
||||||
<div style={{ fontWeight: 500 }}>{t('assignGenresLabel')}</div>
|
<div>
|
||||||
<HelpTooltip
|
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', marginBottom: '0.25rem' }}>
|
||||||
shortText={tHelp('tooltipGenreAssignmentShort')}
|
<div style={{ fontWeight: 500 }}>{t('assignGenresLabel')}</div>
|
||||||
longText={tHelp('tooltipGenreAssignmentLong')}
|
<HelpTooltip
|
||||||
position="right"
|
shortText={tHelp('tooltipGenreAssignmentShort')}
|
||||||
/>
|
longText={tHelp('tooltipGenreAssignmentLong')}
|
||||||
</div>
|
position="right"
|
||||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.25rem' }}>
|
/>
|
||||||
{genres
|
</div>
|
||||||
.filter(g => curatorInfo?.genreIds?.includes(g.id))
|
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.25rem' }}>
|
||||||
.map(genre => (
|
{genres
|
||||||
<label
|
.filter(g => curatorInfo?.genreIds?.includes(g.id))
|
||||||
key={genre.id}
|
.map(genre => (
|
||||||
style={{
|
<label
|
||||||
display: 'flex',
|
key={genre.id}
|
||||||
alignItems: 'center',
|
style={{
|
||||||
gap: '0.25rem',
|
display: 'flex',
|
||||||
padding: '0.25rem 0.5rem',
|
alignItems: 'center',
|
||||||
borderRadius: '999px',
|
gap: '0.25rem',
|
||||||
background: uploadGenreIds.includes(genre.id) ? '#e5f3ff' : '#f3f4f6',
|
padding: '0.25rem 0.5rem',
|
||||||
fontSize: '0.8rem',
|
borderRadius: '999px',
|
||||||
cursor: 'pointer',
|
background: uploadGenreIds.includes(genre.id) ? '#e5f3ff' : '#f3f4f6',
|
||||||
}}
|
fontSize: '0.8rem',
|
||||||
>
|
cursor: 'pointer',
|
||||||
<input
|
}}
|
||||||
type="checkbox"
|
>
|
||||||
checked={uploadGenreIds.includes(genre.id)}
|
<input
|
||||||
onChange={() => toggleUploadGenre(genre.id)}
|
type="checkbox"
|
||||||
/>
|
checked={uploadGenreIds.includes(genre.id)}
|
||||||
{typeof genre.name === 'string' ? genre.name : genre.name?.de ?? genre.name?.en}
|
onChange={() => toggleUploadGenre(genre.id)}
|
||||||
</label>
|
/>
|
||||||
))}
|
{typeof genre.name === 'string' ? genre.name : genre.name?.de ?? genre.name?.en}
|
||||||
{curatorInfo && curatorInfo.genreIds.length === 0 && (
|
</label>
|
||||||
<span style={{ fontSize: '0.8rem', color: '#9ca3af' }}>
|
))}
|
||||||
{t('noAssignedGenres')}
|
{curatorInfo && curatorInfo.genreIds.length === 0 && (
|
||||||
</span>
|
<span style={{ fontSize: '0.8rem', color: '#9ca3af' }}>
|
||||||
)}
|
{t('noAssignedGenres')}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', marginBottom: '0.25rem', marginTop: '0.5rem' }}>
|
||||||
|
<div style={{ fontWeight: 500 }}>{t('assignSpecialsLabel')}</div>
|
||||||
|
<HelpTooltip
|
||||||
|
shortText={tHelp('tooltipSpecialAssignmentShort')}
|
||||||
|
longText={tHelp('tooltipSpecialAssignmentLong')}
|
||||||
|
position="right"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.25rem' }}>
|
||||||
|
{specials
|
||||||
|
.filter(s => curatorInfo?.specialIds?.includes(s.id))
|
||||||
|
.map(special => (
|
||||||
|
<label
|
||||||
|
key={special.id}
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '0.25rem',
|
||||||
|
padding: '0.25rem 0.5rem',
|
||||||
|
borderRadius: '999px',
|
||||||
|
background: uploadSpecialIds.includes(special.id) ? '#fef3c7' : '#f3f4f6',
|
||||||
|
fontSize: '0.8rem',
|
||||||
|
cursor: 'pointer',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={uploadSpecialIds.includes(special.id)}
|
||||||
|
onChange={() => toggleUploadSpecial(special.id)}
|
||||||
|
/>
|
||||||
|
{typeof special.name === 'string'
|
||||||
|
? special.name
|
||||||
|
: special.name?.de ?? special.name?.en}
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useParams, useRouter, usePathname } from 'next/navigation';
|
import { useParams, useRouter, usePathname } from 'next/navigation';
|
||||||
import { useLocale, useTranslations } from 'next-intl';
|
import { useLocale, useTranslations } from 'next-intl';
|
||||||
|
|||||||
@@ -1,161 +1,13 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffect, useState } from 'react';
|
// Root /curator/specials route without locale:
|
||||||
import { useLocale, useTranslations } from 'next-intl';
|
// redirect users to the default English locale version.
|
||||||
import { Link } from '@/lib/navigation';
|
|
||||||
import { getCuratorAuthHeaders } from '@/lib/curatorAuth';
|
|
||||||
import HelpTooltip from '@/components/HelpTooltip';
|
|
||||||
|
|
||||||
type LocalizedString = string | { de: string; en: string };
|
import { redirect } from 'next/navigation';
|
||||||
|
|
||||||
interface CuratorSpecialSummary {
|
|
||||||
id: number;
|
|
||||||
name: LocalizedString;
|
|
||||||
songCount: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function CuratorSpecialsPage() {
|
export default function CuratorSpecialsPage() {
|
||||||
const t = useTranslations('Curator');
|
redirect('/en/curator/specials');
|
||||||
const tHelp = useTranslations('CuratorHelp');
|
|
||||||
const locale = useLocale();
|
|
||||||
const [specials, setSpecials] = useState<CuratorSpecialSummary[]>([]);
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const fetchSpecials = async () => {
|
|
||||||
try {
|
|
||||||
setLoading(true);
|
|
||||||
const res = await fetch('/api/curator/specials', {
|
|
||||||
headers: getCuratorAuthHeaders(),
|
|
||||||
});
|
|
||||||
if (res.ok) {
|
|
||||||
const data = await res.json();
|
|
||||||
setSpecials(data);
|
|
||||||
} else if (res.status === 403) {
|
|
||||||
setError(t('noSpecialPermissions'));
|
|
||||||
} else {
|
|
||||||
setError('Failed to load specials');
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
setError('Failed to load specials');
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchSpecials();
|
|
||||||
}, [t]);
|
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return (
|
|
||||||
<div style={{ padding: '2rem', textAlign: 'center' }}>
|
|
||||||
<p>{t('loadingData')}</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
return (
|
|
||||||
<div style={{ padding: '2rem', textAlign: 'center' }}>
|
|
||||||
<p>{error}</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (specials.length === 0) {
|
|
||||||
return (
|
|
||||||
<div style={{ padding: '2rem', textAlign: 'center' }}>
|
|
||||||
<p>{t('noSpecialsInScope')}</p>
|
|
||||||
<div style={{ marginTop: '1.5rem' }}>
|
|
||||||
<Link
|
|
||||||
href="/curator"
|
|
||||||
style={{
|
|
||||||
padding: '0.5rem 1rem',
|
|
||||||
background: '#e5e7eb',
|
|
||||||
borderRadius: '0.5rem',
|
|
||||||
textDecoration: 'none',
|
|
||||||
color: '#111827',
|
|
||||||
fontSize: '0.9rem',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t('backToDashboard')}
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const resolveLocalized = (value: LocalizedString, locale: string): string => {
|
|
||||||
if (!value) return '';
|
|
||||||
if (typeof value === 'string') return value;
|
|
||||||
const loc = locale === 'de' || locale === 'en' ? locale : 'en';
|
|
||||||
return value[loc] ?? value.en ?? value.de;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div style={{ padding: '2rem', maxWidth: '900px', margin: '0 auto' }}>
|
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '1.5rem' }}>
|
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
|
|
||||||
<h1 style={{ fontSize: '2rem', fontWeight: 'bold' }}>
|
|
||||||
{t('curateSpecialsTitle')}
|
|
||||||
</h1>
|
|
||||||
<HelpTooltip
|
|
||||||
shortText={tHelp('tooltipCurateSpecialsShort')}
|
|
||||||
longText={tHelp('tooltipCurateSpecialsLong')}
|
|
||||||
position="bottom"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<Link
|
|
||||||
href="/curator"
|
|
||||||
style={{
|
|
||||||
padding: '0.5rem 1rem',
|
|
||||||
background: '#e5e7eb',
|
|
||||||
borderRadius: '0.5rem',
|
|
||||||
textDecoration: 'none',
|
|
||||||
color: '#111827',
|
|
||||||
fontSize: '0.9rem',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t('backToDashboard')}
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
<p style={{ marginBottom: '1.5rem', color: '#4b5563' }}>
|
|
||||||
{t('curateSpecialsDescription')}
|
|
||||||
</p>
|
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(260px, 1fr))', gap: '1rem' }}>
|
|
||||||
{specials.map(special => (
|
|
||||||
<Link
|
|
||||||
key={special.id}
|
|
||||||
href={`/curator/specials/${special.id}`}
|
|
||||||
style={{
|
|
||||||
padding: '1rem',
|
|
||||||
borderRadius: '0.75rem',
|
|
||||||
border: '1px solid #e5e7eb',
|
|
||||||
background: 'white',
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
textDecoration: 'none',
|
|
||||||
color: 'inherit',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<h2 style={{ fontSize: '1.1rem', fontWeight: 600, marginBottom: '0.25rem' }}>
|
|
||||||
{resolveLocalized(special.name, String(locale))}
|
|
||||||
</h2>
|
|
||||||
<p style={{ fontSize: '0.875rem', color: '#6b7280' }}>
|
|
||||||
{t('curateSpecialSongCount', { count: special.songCount })}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div style={{ marginTop: '0.75rem', textAlign: 'right', fontSize: '0.875rem', color: '#4f46e5' }}>
|
|
||||||
{t('curateSpecialOpen')}
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -203,6 +203,7 @@
|
|||||||
"selectedFilesTitle": "Ausgewählte Dateien:",
|
"selectedFilesTitle": "Ausgewählte Dateien:",
|
||||||
"uploadProgress": "Upload: {current} / {total}",
|
"uploadProgress": "Upload: {current} / {total}",
|
||||||
"assignGenresLabel": "Genres zuordnen",
|
"assignGenresLabel": "Genres zuordnen",
|
||||||
|
"assignSpecialsLabel": "Specials zuordnen",
|
||||||
"noAssignedGenres": "Dir sind noch keine Genres zugeordnet. Bitte wende dich an den Admin.",
|
"noAssignedGenres": "Dir sind noch keine Genres zugeordnet. Bitte wende dich an den Admin.",
|
||||||
"uploadButtonIdle": "Upload starten",
|
"uploadButtonIdle": "Upload starten",
|
||||||
"uploadButtonUploading": "Lade hoch...",
|
"uploadButtonUploading": "Lade hoch...",
|
||||||
@@ -311,12 +312,12 @@
|
|||||||
"uploadTitle": "Songs hochladen",
|
"uploadTitle": "Songs hochladen",
|
||||||
"uploadStepsTitle": "Schritt-für-Schritt-Anleitung",
|
"uploadStepsTitle": "Schritt-für-Schritt-Anleitung",
|
||||||
"uploadStep1": "MP3-Dateien in den Upload-Bereich ziehen oder klicken, um Dateien auszuwählen",
|
"uploadStep1": "MP3-Dateien in den Upload-Bereich ziehen oder klicken, um Dateien auszuwählen",
|
||||||
"uploadStep2": "Ein oder mehrere Genres auswählen, um sie den hochgeladenen Songs zuzuordnen",
|
"uploadStep2": "Ein oder mehrere Genres und – falls passend – Specials auswählen, um sie den hochgeladenen Songs zuzuordnen",
|
||||||
"uploadStep3": "Auf 'Upload starten' klicken, um den Upload-Prozess zu beginnen",
|
"uploadStep3": "Auf 'Upload starten' klicken, um den Upload-Prozess zu beginnen",
|
||||||
"uploadStep4": "Das System extrahiert automatisch Metadaten (Titel, Artist, Erscheinungsjahr) aus den Dateien",
|
"uploadStep4": "Das System extrahiert automatisch Metadaten (Titel, Artist, Erscheinungsjahr) aus den Dateien",
|
||||||
"uploadBestPracticesTitle": "Best Practices",
|
"uploadBestPracticesTitle": "Best Practices",
|
||||||
"uploadBestPractice1": "Stelle sicher, dass MP3-Dateien korrekte ID3-Tags (Titel, Artist) für die automatische Metadaten-Extraktion haben",
|
"uploadBestPractice1": "Stelle sicher, dass MP3-Dateien korrekte ID3-Tags (Titel, Artist) für die automatische Metadaten-Extraktion haben",
|
||||||
"uploadBestPractice2": "Passende Genres vor dem Upload auswählen, um spätere manuelle Zuordnung zu vermeiden",
|
"uploadBestPractice2": "Passende Genres (und Specials) vor dem Upload auswählen, um spätere manuelle Zuordnung zu vermeiden",
|
||||||
"uploadBestPractice3": "Vor dem Upload auf Duplikate prüfen - das System warnt dich, wenn ein Song bereits existiert",
|
"uploadBestPractice3": "Vor dem Upload auf Duplikate prüfen - das System warnt dich, wenn ein Song bereits existiert",
|
||||||
"tip": "Tipp",
|
"tip": "Tipp",
|
||||||
"uploadTip": "Alle von Kuratoren hochgeladenen Songs werden automatisch von der globalen Playlist ausgeschlossen. Nur Admins können diese Einstellung ändern.",
|
"uploadTip": "Alle von Kuratoren hochgeladenen Songs werden automatisch von der globalen Playlist ausgeschlossen. Nur Admins können diese Einstellung ändern.",
|
||||||
@@ -367,6 +368,8 @@
|
|||||||
"tooltipUploadLong": "Ziehe MP3-Dateien per Drag & Drop oder klicke, um sie auszuwählen. Das System extrahiert automatisch Metadaten (Titel, Artist, Erscheinungsjahr) aus ID3-Tags. Wähle Genres vor dem Upload aus, um Songs automatisch zuzuordnen. Alle Kuratoren-Uploads sind standardmäßig von der globalen Playlist ausgeschlossen.",
|
"tooltipUploadLong": "Ziehe MP3-Dateien per Drag & Drop oder klicke, um sie auszuwählen. Das System extrahiert automatisch Metadaten (Titel, Artist, Erscheinungsjahr) aus ID3-Tags. Wähle Genres vor dem Upload aus, um Songs automatisch zuzuordnen. Alle Kuratoren-Uploads sind standardmäßig von der globalen Playlist ausgeschlossen.",
|
||||||
"tooltipGenreAssignmentShort": "Genres zu hochgeladenen Songs zuordnen",
|
"tooltipGenreAssignmentShort": "Genres zu hochgeladenen Songs zuordnen",
|
||||||
"tooltipGenreAssignmentLong": "Wähle ein oder mehrere Genres vor dem Upload aus. Die ausgewählten Genres werden allen erfolgreich hochgeladenen Songs zugeordnet. Du kannst nur Genres zuordnen, für die du verantwortlich bist. Wenn du keine Genres auswählst, kannst du sie später durch Bearbeitung der Songs zuordnen.",
|
"tooltipGenreAssignmentLong": "Wähle ein oder mehrere Genres vor dem Upload aus. Die ausgewählten Genres werden allen erfolgreich hochgeladenen Songs zugeordnet. Du kannst nur Genres zuordnen, für die du verantwortlich bist. Wenn du keine Genres auswählst, kannst du sie später durch Bearbeitung der Songs zuordnen.",
|
||||||
|
"tooltipSpecialAssignmentShort": "Specials zu hochgeladenen Songs zuordnen",
|
||||||
|
"tooltipSpecialAssignmentLong": "Wähle ein oder mehrere Specials vor dem Upload aus. Die ausgewählten Specials werden allen erfolgreich hochgeladenen Songs zugeordnet. Du kannst nur Specials zuordnen, für die du verantwortlich bist. Wenn du keine Specials auswählst, kannst du sie später durch Bearbeitung der Songs zuordnen.",
|
||||||
"tooltipTracklistShort": "Deine Songs verwalten",
|
"tooltipTracklistShort": "Deine Songs verwalten",
|
||||||
"tooltipTracklistLong": "Diese Tabelle zeigt alle Songs in deinen Genres und Specials. Du kannst suchen, filtern, sortieren und Songs bearbeiten. Nutze die Checkboxen, um mehrere Songs für die Batch-Bearbeitung auszuwählen. Nur Songs, die du bearbeiten kannst, haben eine aktive Checkbox.",
|
"tooltipTracklistLong": "Diese Tabelle zeigt alle Songs in deinen Genres und Specials. Du kannst suchen, filtern, sortieren und Songs bearbeiten. Nutze die Checkboxen, um mehrere Songs für die Batch-Bearbeitung auszuwählen. Nur Songs, die du bearbeiten kannst, haben eine aktive Checkbox.",
|
||||||
"tooltipSearchShort": "Nach Titel oder Artist suchen",
|
"tooltipSearchShort": "Nach Titel oder Artist suchen",
|
||||||
|
|||||||
@@ -203,6 +203,7 @@
|
|||||||
"selectedFilesTitle": "Selected files:",
|
"selectedFilesTitle": "Selected files:",
|
||||||
"uploadProgress": "Upload: {current} / {total}",
|
"uploadProgress": "Upload: {current} / {total}",
|
||||||
"assignGenresLabel": "Assign genres",
|
"assignGenresLabel": "Assign genres",
|
||||||
|
"assignSpecialsLabel": "Assign specials",
|
||||||
"noAssignedGenres": "No genres are assigned to you yet. Please contact the admin.",
|
"noAssignedGenres": "No genres are assigned to you yet. Please contact the admin.",
|
||||||
"uploadButtonIdle": "Start upload",
|
"uploadButtonIdle": "Start upload",
|
||||||
"uploadButtonUploading": "Uploading...",
|
"uploadButtonUploading": "Uploading...",
|
||||||
@@ -311,12 +312,12 @@
|
|||||||
"uploadTitle": "Uploading Songs",
|
"uploadTitle": "Uploading Songs",
|
||||||
"uploadStepsTitle": "Step-by-Step Guide",
|
"uploadStepsTitle": "Step-by-Step Guide",
|
||||||
"uploadStep1": "Drag MP3 files into the upload area or click to select files",
|
"uploadStep1": "Drag MP3 files into the upload area or click to select files",
|
||||||
"uploadStep2": "Select one or more genres to assign to the uploaded songs",
|
"uploadStep2": "Select one or more genres and, if applicable, specials to assign to the uploaded songs",
|
||||||
"uploadStep3": "Click 'Start upload' to begin the upload process",
|
"uploadStep3": "Click 'Start upload' to begin the upload process",
|
||||||
"uploadStep4": "The system will automatically extract metadata (title, artist, release year) from the files",
|
"uploadStep4": "The system will automatically extract metadata (title, artist, release year) from the files",
|
||||||
"uploadBestPracticesTitle": "Best Practices",
|
"uploadBestPracticesTitle": "Best Practices",
|
||||||
"uploadBestPractice1": "Ensure MP3 files have correct ID3 tags (title, artist) for automatic metadata extraction",
|
"uploadBestPractice1": "Ensure MP3 files have correct ID3 tags (title, artist) for automatic metadata extraction",
|
||||||
"uploadBestPractice2": "Select appropriate genres before uploading to avoid manual assignment later",
|
"uploadBestPractice2": "Select appropriate genres (and specials) before uploading to avoid manual assignment later",
|
||||||
"uploadBestPractice3": "Check for duplicates before uploading - the system will warn you if a song already exists",
|
"uploadBestPractice3": "Check for duplicates before uploading - the system will warn you if a song already exists",
|
||||||
"tip": "Tip",
|
"tip": "Tip",
|
||||||
"uploadTip": "All songs uploaded by curators are automatically excluded from the global playlist. Only admins can change this setting.",
|
"uploadTip": "All songs uploaded by curators are automatically excluded from the global playlist. Only admins can change this setting.",
|
||||||
@@ -367,6 +368,8 @@
|
|||||||
"tooltipUploadLong": "Drag and drop MP3 files or click to select. The system will automatically extract metadata (title, artist, release year) from ID3 tags. Select genres before uploading to automatically assign songs. All curator uploads are excluded from the global playlist by default.",
|
"tooltipUploadLong": "Drag and drop MP3 files or click to select. The system will automatically extract metadata (title, artist, release year) from ID3 tags. Select genres before uploading to automatically assign songs. All curator uploads are excluded from the global playlist by default.",
|
||||||
"tooltipGenreAssignmentShort": "Assign genres to uploaded songs",
|
"tooltipGenreAssignmentShort": "Assign genres to uploaded songs",
|
||||||
"tooltipGenreAssignmentLong": "Select one or more genres before uploading. The selected genres will be assigned to all successfully uploaded songs. You can only assign genres that you are responsible for. If you don't select any genres, you can assign them later by editing the songs.",
|
"tooltipGenreAssignmentLong": "Select one or more genres before uploading. The selected genres will be assigned to all successfully uploaded songs. You can only assign genres that you are responsible for. If you don't select any genres, you can assign them later by editing the songs.",
|
||||||
|
"tooltipSpecialAssignmentShort": "Assign specials to uploaded songs",
|
||||||
|
"tooltipSpecialAssignmentLong": "Select one or more specials before uploading. The selected specials will be assigned to all successfully uploaded songs. You can only assign specials that you are responsible for. If you don't select any specials, you can assign them later by editing the songs.",
|
||||||
"tooltipTracklistShort": "Manage your songs",
|
"tooltipTracklistShort": "Manage your songs",
|
||||||
"tooltipTracklistLong": "This table shows all songs in your genres and specials. You can search, filter, sort, and edit songs. Use the checkboxes to select multiple songs for batch editing. Only songs you can edit will have an active checkbox.",
|
"tooltipTracklistLong": "This table shows all songs in your genres and specials. You can search, filter, sort, and edit songs. Use the checkboxes to select multiple songs for batch editing. Only songs you can edit will have an active checkbox.",
|
||||||
"tooltipSearchShort": "Search by title or artist",
|
"tooltipSearchShort": "Search by title or artist",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "hoerdle",
|
"name": "hoerdle",
|
||||||
"version": "0.1.6.4",
|
"version": "0.1.6.8",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
|
|||||||
@@ -4,6 +4,11 @@
|
|||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
|
if [ -f "$HOME/.restic-env" ]; then
|
||||||
|
# shellcheck source=/dev/null
|
||||||
|
. "$HOME/.restic-env"
|
||||||
|
fi
|
||||||
|
|
||||||
echo "💾 Creating Restic backup..."
|
echo "💾 Creating Restic backup..."
|
||||||
|
|
||||||
if ! command -v restic >/dev/null 2>&1; then
|
if ! command -v restic >/dev/null 2>&1; then
|
||||||
|
|||||||
@@ -22,6 +22,12 @@
|
|||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
|
# Optional: Restic-Umgebungsvariablen aus ~/.restic-env laden
|
||||||
|
if [ -f "$HOME/.restic-env" ]; then
|
||||||
|
# shellcheck source=/dev/null
|
||||||
|
. "$HOME/.restic-env"
|
||||||
|
fi
|
||||||
|
|
||||||
echo "💾 Restoring from Restic backup..."
|
echo "💾 Restoring from Restic backup..."
|
||||||
|
|
||||||
if ! command -v restic >/dev/null 2>&1; then
|
if ! command -v restic >/dev/null 2>&1; then
|
||||||
|
|||||||
Reference in New Issue
Block a user