Something went wrong. Try again.
[READ-ONLY] Mirror of https://github.com/AilaScott/Riverwriter. Riverwriter- Focused Songwriting! riverwriter.neocities.org
music songwriting writing
Something went wrong. Try again.
6.2 kB · 211 lines
JavaScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212const SONGS_KEY = 'riverwriter-songs';
function uid() { return Date.now().toString(36) + Math.random().toString(36).slice(2, 8);}
function emptySong(name) { return { id: uid(), name: name || 'Untitled Song', createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), editorSections: [{ header: 'Verse 1', chordLine: '', lyricLine: '' }], chordGen: { history: [], historyIndex: -1, lastKey: 'C', }, };}
export function getIndex() { try { const raw = localStorage.getItem(SONGS_KEY); return raw ? JSON.parse(raw) : null; } catch { return null; }}
function saveIndex(idx) { try { localStorage.setItem(SONGS_KEY, JSON.stringify(idx)); } catch (e) { console.warn('save failed:', e); }}
export function getActiveSong() { const idx = getIndex(); if (!idx || !idx.songs || idx.songs.length === 0) return null; return idx.songs.find(s => s.id === idx.activeSongId) || idx.songs[0];}
export function setActiveSong(id) { const idx = getIndex(); if (!idx) return; idx.activeSongId = id; saveIndex(idx);}
export function updateActiveSong(data) { const idx = getIndex(); if (!idx) return; const song = idx.songs.find(s => s.id === idx.activeSongId); if (!song) return; song.editorSections = data.editorSections; song.chordGen = data.chordGen; song.updatedAt = new Date().toISOString(); saveIndex(idx);}
export function createSong(name) { const idx = getIndex() || { activeSongId: null, songs: [] }; const song = emptySong(name); idx.songs.push(song); idx.activeSongId = song.id; saveIndex(idx); return song;}
export function deleteSong(id) { const idx = getIndex(); if (!idx) return; idx.songs = idx.songs.filter(s => s.id !== id); if (idx.songs.length === 0) { const song = emptySong('Untitled Song'); idx.songs.push(song); idx.activeSongId = song.id; } else if (idx.activeSongId === id) { idx.activeSongId = idx.songs[0].id; } saveIndex(idx);}
export async function exportSong(song, audioBlob) { const payload = { version: 2, exportedAt: new Date().toISOString(), name: song.name, sections: song.editorSections, };
if (audioBlob) { const { blobToBase64 } = await import('./audio-storage.js'); payload.audioBase64 = await blobToBase64(audioBlob); payload.audioMime = audioBlob.type; }
const blob = new Blob([JSON.stringify(payload, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `${song.name.replace(/\s+/g, '-').toLowerCase()}-${new Date().toISOString().slice(0, 10)}.json`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url);}
export async function importSongFromFile(data, fileName) { let sections = []; let name = fileName.replace(/\.json$/i, '').replace(/[_-]/g, ' ') || 'Imported Song';
if (Array.isArray(data)) { sections = data; } else if (data && Array.isArray(data.sections)) { sections = data.sections; if (data.name) name = data.name; } else { throw new Error('Invalid import format'); }
const sanitized = sections.map(s => ({ header: s.header || '', chordLine: s.chordLine || '', lyricLine: s.lyricLine || '', })); if (sanitized.length === 0) sanitized.push({ header: 'Verse 1', chordLine: '', lyricLine: '' });
const idx = getIndex() || { activeSongId: null, songs: [] }; const song = emptySong(name); song.editorSections = sanitized; song.chordGen = { history: [], historyIndex: -1, lastKey: 'C' }; idx.songs.push(song); idx.activeSongId = song.id; saveIndex(idx);
if (data.audioBase64) { const { base64ToBlob, saveRecording } = await import('./audio-storage.js'); const blob = base64ToBlob(data.audioBase64, data.audioMime || 'audio/webm'); await saveRecording(song.id, blob); }
return song;}
export function migrateOldSave() { try { const old = localStorage.getItem('riverwriter-save'); if (!old) return; const data = JSON.parse(old); const sections = (data.editorSections || []).map(s => ({ header: s.header || '', chordLine: s.chordLine || '', lyricLine: s.lyricLine || '', })); if (sections.length === 0) sections.push({ header: 'Verse 1', chordLine: '', lyricLine: '' });
const idx = getIndex(); if (idx && idx.songs && idx.songs.length > 0) { idx.songs.push(emptySong('Imported Song')); const song = idx.songs[idx.songs.length - 1]; song.editorSections = sections; song.chordGen = data.chordGen || song.chordGen; idx.activeSongId = song.id; saveIndex(idx); } else { const song = emptySong('Imported Song'); song.editorSections = sections; song.chordGen = data.chordGen || song.chordGen; saveIndex({ activeSongId: song.id, songs: [song] }); } localStorage.removeItem('riverwriter-save'); } catch { /* ignore migration failures */ }}
const STREAK_KEY = 'riverwriter-streak';
export function updateStreak() { const today = new Date().toISOString().slice(0, 10); let streak; try { const raw = localStorage.getItem(STREAK_KEY); streak = raw ? JSON.parse(raw) : null; } catch { streak = null; }
if (!streak) { streak = { lastDate: today, count: 1 }; } else if (streak.lastDate !== today) { const last = new Date(streak.lastDate + 'T00:00:00'); const now = new Date(today + 'T00:00:00'); const diff = Math.round((now - last) / (1000 * 60 * 60 * 24)); streak.count = diff === 1 ? streak.count + 1 : 1; streak.lastDate = today; }
try { localStorage.setItem(STREAK_KEY, JSON.stringify(streak)); } catch {} return streak.count;}
const SETTINGS_KEY = 'riverwriter-settings';
export function loadSettings() { try { const raw = localStorage.getItem(SETTINGS_KEY); return raw ? JSON.parse(raw) : { theme: 'default', customBg: null }; } catch { return { theme: 'default', customBg: null }; }}
export function saveSettings(settings) { try { localStorage.setItem(SETTINGS_KEY, JSON.stringify(settings)); } catch (e) { console.warn('settings save failed:', e); }}