New Account System

This commit is contained in:
Samidy 2026-01-16 18:47:28 +03:00
parent 42d1b74df8
commit fa716f002a
14 changed files with 9439 additions and 9535 deletions

View file

@ -1,93 +0,0 @@
# Setting Up Firebase Sync for Monochrome
Follow these steps to enable cross-device synchronization for your library, history, and settings using your own Firebase project.
## 1. Create a Firebase Project
1. Go to the [Firebase Console](https://console.firebase.google.com/).
2. Click **Add project** and give it a name (e.g., "Monochrome Sync").
3. (Optional) Disable Gemini and Google Analytics if you want to keep it simple.
4. Click **Create project**.
## 2. Enable Authentication
1. In the left sidebar, click **Build** > **Authentication**.
2. Click **Get Started**.
3. Go to the **Sign-in method** tab.
4. Select **Google** and enable it.
5. Set your project support email and click **Save**.
### 2.1 Authorized Domains (CRITICAL)
Firebase will block login attempts from unknown domains.
1. In the **Authentication** section, go to the **Settings** tab.
2. Click **Authorized domains** in the left sub-menu.
3. Click **Add domain**.
4. Add your hosting domain (e.g., `julienmaille.github.io`).
- _Note: `localhost` and `127.0.0.1` are usually added by default for local testing._
## 3. Enable Realtime Database
1. In the left sidebar, click **Build** > **Realtime Database**.
2. Click **Create Database**.
3. Choose a location near you and click **Next**.
4. Select **Start in test mode** (we will change the rules in the next step) and click **Enable**.
## 4. Set Security Rules
1. In the Realtime Database section, go to the **Rules** tab.
2. Replace the existing rules with the following to ensure users can only see their own data:
```json
{
"rules": {
"users": {
"$uid": {
".read": "$uid === auth.uid",
".write": "$uid === auth.uid"
}
},
"public_playlists": {
".read": true,
".indexOn": ["uid", "name"],
"$playlistId": {
".write": "auth != null && (!data.exists() || data.child('uid').val() === auth.uid)"
}
}
}
}
```
- **Note:** The `public_playlists` rule allows anyone to read the playlists. The write rule ensures that only authenticated users can publish, and only the owner (creator) of a playlist can modify or delete it.
3. Click **Publish**.
## 5. Get Your Configuration
1. Click the gear icon (⚙️) next to "Project Overview" and select **Project settings**.
2. In the **General** tab, scroll down to "Your apps" and click the **Web icon (`</>`)**.
3. Register the app (e.g., "Monochrome App").
4. You will see a `firebaseConfig` object. It looks like this:
```javascript
const firebaseConfig = {
apiKey: 'AIzaSy...',
authDomain: 'your-project.firebaseapp.com',
databaseURL: 'https://your-project.firebaseio.com',
projectId: 'your-project',
storageBucket: 'your-project.appspot.com',
messagingSenderId: '...',
appId: '...',
};
```
5. **Copy only the part with the curly braces `{ ... }`**.
## 6. Configure Monochrome
1. Open the Monochrome app and go to **Settings**.
2. Find the **Firebase Configuration** section.
3. Paste the JSON object you copied into the textarea.
4. Click **Save & Reload**.
5. Under **Sync & Backup**, click **Connect with Google** and sign in.
**Your library is now synced to the cloud!** Log in on any other device with the same configuration to see your music everywhere.

View file

@ -152,7 +152,7 @@
> >
<div class="info"> <div class="info">
<span class="label">Public Playlist</span> <span class="label">Public Playlist</span>
<span class="description">Visible to anyone with the link</span> <span class="description">Visible to anyone with the link.</span>
</div> </div>
<label class="toggle-switch"> <label class="toggle-switch">
<input type="checkbox" id="playlist-public-toggle" /> <input type="checkbox" id="playlist-public-toggle" />
@ -1099,55 +1099,6 @@
</div> </div>
</div> </div>
<div class="settings-group">
<div class="setting-item">
<div class="info">
<a class="label" id="firebase-config" href="#toggle-firebase-config-btn"
>ADVANCED: Firebase Configuration</a
>
<span class="description"
>Select which database you would like to connect to. <br />
Monochrome-owned database is default.</span
>
</div>
<div>
<button id="toggle-firebase-config-btn" class="btn-secondary">
Custom Configuration
</button>
<div class="firebase-settings-wrapper">
<div id="custom-firebase-config-container" class="custom-firebase-config">
<p class="config-help-text">
Default shared instance is active.
<a
href="https://github.com/SamidyFR/monochrome/blob/main/firebase-setup.md"
target="_blank"
class="text-link"
>Override below only if needed.</a
>
</p>
<textarea
id="firebase-config-input"
class="template-input"
rows="5"
placeholder='{ "apiKey": "...", "authDomain": "...", ... }'
></textarea>
<div class="firebase-controls-container">
<button id="save-firebase-config-btn" class="btn-secondary">
Save & Reload
</button>
<button id="share-firebase-config-btn" class="btn-secondary">
Share
</button>
<button id="clear-firebase-config-btn" class="btn-secondary danger">
Clear Config
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="settings-group"> <div class="settings-group">
<div class="setting-item"> <div class="setting-item">
<div class="info"> <div class="info">
@ -1460,7 +1411,7 @@
</a> </a>
</div> </div>
<div class="about-footer"> <div class="about-footer">
<p class="version">Version 1.5.0</p> <p class="version">Version 1.6.0</p>
<p class="disclaimer"> <p class="disclaimer">
This is an independent client and is not affiliated with or endorsed by TIDAL or any This is an independent client and is not affiliated with or endorsed by TIDAL or any
music streaming service. music streaming service.
@ -1542,27 +1493,10 @@
} }
</script> </script>
<p style="padding-top: 50px; text-align: center; color: #8b8b93"> <p style="padding-top: 50px; text-align: center; color: #8b8b93">
We only store music data and a randomized ID to find out which Google account is which. We only store music data and a randomized ID to find out which Google/Email account is which.
<br /> <br />
All data is anonymous. We do not store anything like emails, usernames, or anything All data is anonymous. We do not store anything like emails, usernames, or anything
sensitive. <br /> sensitive. <br />
<br />
However, if you want complete control over your data, we allow you to use your own firebase
configuration.
</p>
<div
style="
display: flex;
gap: 50px;
align-items: center;
justify-content: center;
padding-top: 25px;
"
>
<a id="advanced-config-link" class="btn-secondary" href="#settings"
>Advanced: Custom Configuration</a
>
</div>
</div> </div>
</div> </div>
<div id="page-donate" class="page"> <div id="page-donate" class="page">
@ -1857,6 +1791,7 @@
</footer> </footer>
</div> </div>
<script src="https://unpkg.com/@studio-freight/lenis"></script> <script src="https://unpkg.com/@studio-freight/lenis"></script>
<script src="https://cdn.jsdelivr.net/npm/pocketbase@0.21.3/dist/pocketbase.umd.js"></script>
<script type="module" src="js/app.js"></script> <script type="module" src="js/app.js"></script>
</body> </body>
</html> </html>

View file

@ -7,12 +7,12 @@ import {
signInWithEmailAndPassword, signInWithEmailAndPassword,
createUserWithEmailAndPassword, createUserWithEmailAndPassword,
} from 'https://www.gstatic.com/firebasejs/10.7.1/firebase-auth.js'; } from 'https://www.gstatic.com/firebasejs/10.7.1/firebase-auth.js';
import { syncManager } from './sync.js';
export class AuthManager { export class AuthManager {
constructor() { constructor() {
this.user = null; this.user = null;
this.unsubscribe = null; this.unsubscribe = null;
this.authListeners = [];
this.init(); this.init();
} }
@ -23,16 +23,18 @@ export class AuthManager {
this.user = user; this.user = user;
this.updateUI(user); this.updateUI(user);
if (user) { this.authListeners.forEach((listener) => listener(user));
console.log('User logged in:', user.uid);
syncManager.initialize(user);
} else {
console.log('User logged out');
syncManager.disconnect();
}
}); });
} }
onAuthStateChanged(callback) {
this.authListeners.push(callback);
// If we already have a user state, trigger immediately
if (this.user !== null) {
callback(this.user);
}
}
async signInWithGoogle() { async signInWithGoogle() {
if (!auth) { if (!auth) {
alert('Firebase is not configured. Please check console.'); alert('Firebase is not configured. Please check console.');

480
js/accounts/pocketbase.js Normal file
View file

@ -0,0 +1,480 @@
import { db } from '../db.js';
import { authManager } from './auth.js';
const PUBLIC_COLLECTION = 'public_playlists';
const POCKETBASE_URL = 'https://monodb.samidy.com';
const pb = new PocketBase(POCKETBASE_URL);
pb.autoCancellation(false);
const syncManager = {
pb: pb,
_userRecordCache: null,
_isSyncing: false,
async _getUserRecord(uid) {
if (!uid) {
console.warn('_getUserRecord called with no UID.');
return null;
}
if (this._userRecordCache && this._userRecordCache.firebase_id === uid) {
return this._userRecordCache;
}
try {
const record = await this.pb.collection('DB_users').getFirstListItem(
`firebase_id="${uid}"`,
{ f_id: uid }
);
this._userRecordCache = record;
return record;
} catch (error) {
if (error.status === 404) {
try {
const newRecord = await this.pb.collection('DB_users').create({
firebase_id: uid,
library: {},
history: [],
}, { f_id: uid });
this._userRecordCache = newRecord;
return newRecord;
} catch (createError) {
console.error('Failed to create user record in PocketBase:', createError);
return null;
}
}
console.error('Failed to get user record from PocketBase:', error);
return null;
}
},
async getUserData() {
const user = authManager.user;
if (!user) return null;
const record = await this._getUserRecord(user.uid);
if (!record) return null;
let library = record.library || {};
if (typeof library === 'string') {
try {
library = JSON.parse(library);
} catch (e) {
console.error('Failed to parse library JSON:', e);
library = {};
}
}
let history = record.history || [];
if (typeof history === 'string') {
try {
history = JSON.parse(history);
} catch (e) {
console.error('Failed to parse history JSON:', e);
history = [];
}
}
let userPlaylists = record.user_playlists || {};
if (typeof userPlaylists === 'string') {
try {
userPlaylists = JSON.parse(userPlaylists);
} catch (e) {
console.error('Failed to parse user_playlists JSON:', e);
userPlaylists = {};
}
}
return { library, history, userPlaylists };
},
async _updateUserJSON(uid, field, data) {
const record = await this._getUserRecord(uid);
if (!record) {
console.error('Cannot update: no user record found');
return;
}
try {
const updated = await this.pb.collection('DB_users').update(
record.id,
{ [field]: data },
{ f_id: uid }
);
this._userRecordCache = updated;
} catch (error) {
console.error(`Failed to sync ${field} to PocketBase:`, error);
}
},
async syncLibraryItem(type, item, added) {
const user = authManager.user;
if (!user) return;
const record = await this._getUserRecord(user.uid);
if (!record) return;
let library = record.library || {};
if (typeof library === 'string') {
try {
library = JSON.parse(library);
} catch (e) {
console.error('Library field is not valid JSON', e);
library = {};
}
}
const pluralType = type === 'mix' ? 'mixes' : `${type}s`;
const key = type === 'playlist' ? item.uuid : item.id;
if (!library[pluralType]) {
library[pluralType] = {};
}
if (added) {
library[pluralType][key] = this._minifyItem(type, item);
} else {
delete library[pluralType][key];
}
await this._updateUserJSON(user.uid, 'library', library);
},
_minifyItem(type, item) {
if (!item) return item;
const base = {
id: item.id,
addedAt: item.addedAt || Date.now(),
};
if (type === 'track') {
return {
...base,
title: item.title || null,
duration: item.duration || null,
explicit: item.explicit || false,
artist: item.artist || (item.artists && item.artists.length > 0 ? item.artists[0] : null) || null,
artists: item.artists?.map((a) => ({ id: a.id, name: a.name || null })) || [],
album: item.album
? {
id: item.album.id,
title: item.album.title || null,
cover: item.album.cover || null,
releaseDate: item.album.releaseDate || null,
vibrantColor: item.album.vibrantColor || null,
artist: item.album.artist || null,
numberOfTracks: item.album.numberOfTracks || null,
}
: null,
copyright: item.copyright || null,
isrc: item.isrc || null,
trackNumber: item.trackNumber || null,
streamStartDate: item.streamStartDate || null,
version: item.version || null,
mixes: item.mixes || null,
};
}
if (type === 'album') {
return {
...base,
title: item.title || null,
cover: item.cover || null,
releaseDate: item.releaseDate || null,
explicit: item.explicit || false,
artist: item.artist
? { name: item.artist.name || null, id: item.artist.id }
: item.artists?.[0]
? { name: item.artists[0].name || null, id: item.artists[0].id }
: null,
type: item.type || null,
numberOfTracks: item.numberOfTracks || null,
};
}
if (type === 'artist') {
return {
...base,
name: item.name || null,
picture: item.picture || item.image || null,
};
}
if (type === 'playlist') {
return {
uuid: item.uuid || item.id,
addedAt: item.addedAt || Date.now(),
title: item.title || item.name || null,
image: item.image || item.squareImage || item.cover || null,
numberOfTracks: item.numberOfTracks || (item.tracks ? item.tracks.length : 0),
user: item.user ? { name: item.user.name || null } : null,
};
}
if (type === 'mix') {
return {
id: item.id,
addedAt: item.addedAt || Date.now(),
title: item.title,
subTitle: item.subTitle,
mixType: item.mixType,
cover: item.cover,
};
}
return item;
},
async syncHistoryItem(historyEntry) {
const user = authManager.user;
if (!user) return;
const record = await this._getUserRecord(user.uid);
if (!record) return;
let history = record.history || [];
if (typeof history === 'string') {
try {
history = JSON.parse(history);
} catch (e) {
console.error('History field is not valid JSON', e);
history = [];
}
}
const newHistory = [historyEntry, ...history].slice(0, 100);
await this._updateUserJSON(user.uid, 'history', newHistory);
},
async syncUserPlaylist(playlist, action) {
const user = authManager.user;
if (!user) return;
const record = await this._getUserRecord(user.uid);
if (!record) return;
let userPlaylists = record.user_playlists || {};
if (typeof userPlaylists === 'string') {
try {
userPlaylists = JSON.parse(userPlaylists);
} catch (e) {
console.error('user_playlists field is not valid JSON', e);
userPlaylists = {};
}
}
if (action === 'delete') {
delete userPlaylists[playlist.id];
} else {
userPlaylists[playlist.id] = {
id: playlist.id,
name: playlist.name,
cover: playlist.cover || null,
tracks: playlist.tracks ? playlist.tracks.map(t => this._minifyItem('track', t)) : [],
createdAt: playlist.createdAt || Date.now(),
updatedAt: playlist.updatedAt || Date.now(),
numberOfTracks: playlist.tracks ? playlist.tracks.length : 0,
images: playlist.images || [],
isPublic: playlist.isPublic || false,
};
}
await this._updateUserJSON(user.uid, 'user_playlists', userPlaylists);
},
async getPublicPlaylist(uuid) {
try {
const record = await this.pb.collection(PUBLIC_COLLECTION).getFirstListItem(
`uuid="${uuid}"`,
{ p_id: uuid }
);
let rawCover = record.image || record.cover || record.playlist_cover || '';
let extraData = record.data;
if (typeof extraData === 'string') {
try { extraData = JSON.parse(extraData); } catch(e) {}
}
if (!rawCover && extraData && typeof extraData === 'object') {
rawCover = extraData.cover || extraData.image || '';
}
let finalCover = rawCover;
if (rawCover && !rawCover.startsWith('http') && !rawCover.startsWith('data:')) {
finalCover = this.pb.files.getUrl(record, rawCover);
}
let images = [];
let tracks = record.tracks || [];
if (typeof tracks === 'string') {
try {
tracks = JSON.parse(tracks);
} catch (e) {
console.error('Failed to parse tracks JSON:', e);
tracks = [];
}
}
if (!finalCover && tracks && tracks.length > 0) {
const uniqueCovers = [];
const seenCovers = new Set();
for (const track of tracks) {
const c = track.album?.cover;
if (c && !seenCovers.has(c)) {
seenCovers.add(c);
uniqueCovers.push(c);
if (uniqueCovers.length >= 4) break;
}
}
images = uniqueCovers;
}
let finalTitle = record.title || record.name || record.playlist_name;
if (!finalTitle && extraData && typeof extraData === 'object') {
finalTitle = extraData.title || extraData.name;
}
if (!finalTitle) finalTitle = 'Untitled Playlist';
return {
...record,
id: record.uuid,
name: finalTitle,
title: finalTitle,
cover: finalCover,
image: finalCover,
tracks: tracks,
images: images,
numberOfTracks: tracks.length,
type: 'user-playlist',
isPublic: true,
user: { name: 'Community Playlist' }
};
} catch (error) {
if (error.status === 404) return null;
console.error('Failed to fetch public playlist:', error);
throw error;
}
},
async publishPlaylist(playlist) {
if (!playlist || !playlist.id) return;
const uid = authManager.user?.uid;
if (!uid) return;
const data = {
uuid: playlist.id,
uid: uid,
title: playlist.name,
name: playlist.name,
playlist_name: playlist.name,
image: playlist.cover,
cover: playlist.cover,
playlist_cover: playlist.cover,
tracks: playlist.tracks,
isPublic: true,
data: {
title: playlist.name,
cover: playlist.cover
}
};
try {
const existing = await this.pb.collection(PUBLIC_COLLECTION).getList(1, 1, {
filter: `uuid="${playlist.id}"`,
p_id: playlist.id
});
if (existing.items.length > 0) {
await this.pb.collection(PUBLIC_COLLECTION).update(existing.items[0].id, data);
} else {
await this.pb.collection(PUBLIC_COLLECTION).create(data);
}
} catch (error) {
console.error('Failed to publish playlist:', error);
}
},
async unpublishPlaylist(uuid) {
const uid = authManager.user?.uid;
if (!uid) return;
try {
const existing = await this.pb.collection('public_playlists').getList(1, 1, {
filter: `uuid="${uuid}"`,
p_id: uuid
});
if (existing.items && existing.items.length > 0) {
await this.pb.collection('public_playlists').delete(existing.items[0].id, { p_id: uuid });
}
} catch (error) {
console.error('Failed to unpublish playlist:', error);
}
},
async clearCloudData() {
const user = authManager.user;
if (!user) return;
try {
const record = await this._getUserRecord(user.uid);
if (record) {
await this.pb.collection('DB_users').delete(record.id, { f_id: user.uid });
this._userRecordCache = null;
alert('Cloud data cleared successfully.');
}
} catch (error) {
console.error('Failed to clear cloud data!', error);
alert('Failed to clear cloud data! :( Check console for details.');
}
},
async onAuthStateChanged(user) {
if (user) {
if (this._isSyncing) return;
this._isSyncing = true;
try {
const data = await this.getUserData();
if (data) {
const convertedData = {
favorites_tracks: data.library.tracks ? Object.values(data.library.tracks).filter(t => t && typeof t === 'object') : [],
favorites_albums: data.library.albums ? Object.values(data.library.albums).filter(a => a && typeof a === 'object') : [],
favorites_artists: data.library.artists ? Object.values(data.library.artists).filter(a => a && typeof a === 'object') : [],
favorites_playlists: data.library.playlists ? Object.values(data.library.playlists).filter(p => p && typeof p === 'object') : [],
favorites_mixes: data.library.mixes ? Object.values(data.library.mixes).filter(m => m && typeof m === 'object') : [],
history_tracks: data.history || [],
user_playlists: data.userPlaylists ? Object.values(data.userPlaylists).filter(p => p && typeof p === 'object') : [],
};
await db.importData(convertedData);
await new Promise(resolve => setTimeout(resolve, 300));
window.dispatchEvent(new CustomEvent('library-changed'));
window.dispatchEvent(new CustomEvent('history-changed'));
window.dispatchEvent(new HashChangeEvent('hashchange'));
}
} catch (error) {
console.error('Error during PocketBase sync!', error);
} finally {
this._isSyncing = false;
}
} else {
this._userRecordCache = null;
this._isSyncing = false;
}
},
};
if (pb) {
authManager.onAuthStateChanged(syncManager.onAuthStateChanged.bind(syncManager));
}
export { pb, syncManager };

View file

@ -25,7 +25,7 @@ import { downloadAlbumAsZip, downloadDiscography, downloadPlaylistAsZip } from '
import { debounce, SVG_PLAY } from './utils.js'; import { debounce, SVG_PLAY } from './utils.js';
import { sidePanelManager } from './side-panel.js'; import { sidePanelManager } from './side-panel.js';
import { db } from './db.js'; import { db } from './db.js';
import { syncManager } from './firebase/sync.js'; import { syncManager } from './accounts/pocketbase.js';
import { registerSW } from 'virtual:pwa-register'; import { registerSW } from 'virtual:pwa-register';
import './smooth-scrolling.js'; import './smooth-scrolling.js';
import { readTrackMetadata } from './metadata.js'; import { readTrackMetadata } from './metadata.js';
@ -584,7 +584,7 @@ document.addEventListener('DOMContentLoaded', async () => {
const publicToggle = document.getElementById('playlist-public-toggle'); const publicToggle = document.getElementById('playlist-public-toggle');
const shareBtn = document.getElementById('playlist-share-btn'); const shareBtn = document.getElementById('playlist-share-btn');
// Check if actually public in Firebase to be sure (async) or trust local flag // Check if actually public in Pocketbase to be sure (async) or trust local flag
// We trust local flag for UI speed, but could verify. // We trust local flag for UI speed, but could verify.
if (publicToggle) publicToggle.checked = !!playlist.isPublic; if (publicToggle) publicToggle.checked = !!playlist.isPublic;
@ -684,7 +684,7 @@ document.addEventListener('DOMContentLoaded', async () => {
if (userPlaylist) { if (userPlaylist) {
tracks = userPlaylist.tracks; tracks = userPlaylist.tracks;
} else { } else {
// Try API, if fail, try Public Firebase // Try API, if fail, try Public Pocketbase
try { try {
const { tracks: apiTracks } = await api.getPlaylist(playlistId); const { tracks: apiTracks } = await api.getPlaylist(playlistId);
tracks = apiTracks; tracks = apiTracks;
@ -990,7 +990,7 @@ document.addEventListener('DOMContentLoaded', async () => {
}, 3000); }, 3000);
} }
// Listener for Firebase Sync updates // Listener for Pocketbase Sync updates
window.addEventListener('library-changed', () => { window.addEventListener('library-changed', () => {
const hash = window.location.hash; const hash = window.location.hash;
if (hash === '#library') { if (hash === '#library') {

249
js/db.js
View file

@ -141,23 +141,28 @@ export class MusicDatabase {
} }
} }
async getFavorites(type) { async getFavorites(type) {
const plural = type === 'mix' ? 'mixes' : `${type}s`; const plural = type === 'mix' ? 'mixes' : `${type}s`;
const storeName = `favorites_${plural}`; const storeName = `favorites_${plural}`;
const db = await this.open(); const db = await this.open();
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const transaction = db.transaction(storeName, 'readonly'); const transaction = db.transaction(storeName, 'readonly');
const store = transaction.objectStore(storeName); const store = transaction.objectStore(storeName);
const index = store.index('addedAt');
const request = index.getAll(); // Returns sorted by addedAt ascending const request = store.getAll();
request.onsuccess = () => { request.onsuccess = () => {
// Reverse to show newest first const results = request.result;
resolve(request.result.reverse()); results.sort((a, b) => {
}; const aTime = a.addedAt || 0;
request.onerror = () => reject(request.error); const bTime = b.addedAt || 0;
}); return bTime - aTime; // Newest first
} });
resolve(results);
};
request.onerror = () => reject(request.error);
});
}
_minifyItem(type, item) { _minifyItem(type, item) {
if (!item) return item; if (!item) return item;
@ -276,92 +281,105 @@ export class MusicDatabase {
return data; return data;
} }
async importData(data, clear = false) { async importData(data, clear = false) {
const db = await this.open(); const db = await this.open();
const importStore = async (storeName, items) => {
if (items === undefined) return false;
let itemsArray = Array.isArray(items) ? items : Object.values(items || {});
console.log(`Importing to ${storeName}: ${itemsArray.length} items`);
if (itemsArray.length === 0) {
if (clear) {
return new Promise((resolve, reject) => {
const transaction = db.transaction(storeName, 'readwrite');
const store = transaction.objectStore(storeName);
const countReq = store.count();
countReq.onsuccess = () => {
if (countReq.result > 0) {
store.clear();
}
};
transaction.oncomplete = () => {
resolve(countReq.result > 0);
};
transaction.onerror = () => reject(transaction.error);
});
}
return false;
}
return new Promise((resolve, reject) => {
const transaction = db.transaction(storeName, 'readwrite');
const store = transaction.objectStore(storeName);
let hasChanges = false;
// force clear on first sync
console.log(`Clearing ${storeName} to Make Sure Everythings Good`);
store.clear();
hasChanges = true;
itemsArray.forEach((item) => {
if (item.id && typeof item.id === 'string' && !isNaN(item.id)) {
item.id = parseInt(item.id, 10);
}
if (item.album?.id && typeof item.album.id === 'string' && !isNaN(item.album.id)) {
item.album.id = parseInt(item.album.id, 10);
}
if (item.artists) {
item.artists.forEach(artist => {
if (artist.id && typeof artist.id === 'string' && !isNaN(artist.id)) {
artist.id = parseInt(artist.id, 10);
}
});
}
console.log(`${storeName}: Adding item with ID ${item.id || item.uuid || item.timestamp}`);
store.put(item);
});
transaction.oncomplete = () => {
console.log(`${storeName}: Imported ${itemsArray.length} items`);
resolve(true);
};
transaction.onerror = (event) => {
console.error(`${storeName}: Transaction error:`, event.target.error);
reject(transaction.error);
};
});
};
console.log('Starting import with data:', {
tracks: data.favorites_tracks?.length || 0,
albums: data.favorites_albums?.length || 0,
artists: data.favorites_artists?.length || 0,
playlists: data.favorites_playlists?.length || 0,
mixes: data.favorites_mixes?.length || 0,
history: data.history_tracks?.length || 0,
userPlaylists: data.user_playlists?.length || 0,
});
const results = await Promise.all([
importStore('favorites_tracks', data.favorites_tracks),
importStore('favorites_albums', data.favorites_albums),
importStore('favorites_artists', data.favorites_artists),
importStore('favorites_playlists', data.favorites_playlists),
importStore('favorites_mixes', data.favorites_mixes),
importStore('history_tracks', data.history_tracks),
data.user_playlists ? importStore('user_playlists', data.user_playlists) : Promise.resolve(false),
]);
console.log('Import results:', results);
return results.some((r) => r);
}
const importStore = async (storeName, items) => {
if (items === undefined) return false;
let itemsArray = Array.isArray(items) ? items : Object.values(items || {});
if (itemsArray.length === 0) {
if (clear) {
return new Promise((resolve) => {
const transaction = db.transaction(storeName, 'readwrite');
const store = transaction.objectStore(storeName);
const countReq = store.count();
countReq.onsuccess = () => {
if (countReq.result > 0) {
store.clear();
resolve(true);
} else {
resolve(false);
}
};
countReq.onerror = () => resolve(false);
});
}
return false;
}
return new Promise((resolve, reject) => {
const transaction = db.transaction(storeName, 'readwrite');
const store = transaction.objectStore(storeName);
let hasChanges = false;
if (clear) {
store.clear();
hasChanges = true;
}
let pending = itemsArray.length;
itemsArray.forEach((item) => {
if (clear) {
store.put(item);
pending--;
if (pending === 0) resolve(true);
return;
}
let key;
if (storeName === 'favorites_playlists') key = item.uuid;
else if (storeName === 'history_tracks') key = item.timestamp;
else key = item.id;
const getReq = store.get(key);
getReq.onsuccess = () => {
const existing = getReq.result;
if (!existing || JSON.stringify(existing) !== JSON.stringify(item)) {
store.put(item);
hasChanges = true;
}
pending--;
if (pending === 0) resolve(hasChanges);
};
getReq.onerror = () => {
store.put(item);
hasChanges = true;
pending--;
if (pending === 0) resolve(hasChanges);
};
});
transaction.onerror = () => reject(transaction.error);
});
};
const results = await Promise.all([
importStore('favorites_tracks', data.favorites_tracks),
importStore('favorites_albums', data.favorites_albums),
importStore('favorites_artists', data.favorites_artists),
importStore('favorites_playlists', data.favorites_playlists),
importStore('favorites_mixes', data.favorites_mixes),
importStore('history_tracks', data.history_tracks),
data.user_playlists ? importStore('user_playlists', data.user_playlists) : Promise.resolve(false),
]);
return results.some((r) => r);
}
_updatePlaylistMetadata(playlist) { _updatePlaylistMetadata(playlist) {
playlist.numberOfTracks = playlist.tracks ? playlist.tracks.length : 0; playlist.numberOfTracks = playlist.tracks ? playlist.tracks.length : 0;
@ -383,6 +401,12 @@ export class MusicDatabase {
return playlist; return playlist;
} }
_dispatchPlaylistSync(action, playlist) {
window.dispatchEvent(new CustomEvent('sync-playlist-change', {
detail: { action, playlist }
}));
}
// User Playlists API // User Playlists API
async createPlaylist(name, tracks = [], cover = '') { async createPlaylist(name, tracks = [], cover = '') {
const id = crypto.randomUUID(); const id = crypto.randomUUID();
@ -393,9 +417,15 @@ export class MusicDatabase {
cover: cover, cover: cover,
createdAt: Date.now(), createdAt: Date.now(),
updatedAt: Date.now(), updatedAt: Date.now(),
numberOfTracks: tracks.length,
images: [] // Initialize images
}; };
this._updatePlaylistMetadata(playlist); this._updatePlaylistMetadata(playlist);
await this.performTransaction('user_playlists', 'readwrite', (store) => store.put(playlist)); await this.performTransaction('user_playlists', 'readwrite', (store) => store.put(playlist));
// TRIGGER SYNC
this._dispatchPlaylistSync('create', playlist);
return playlist; return playlist;
} }
@ -409,6 +439,9 @@ export class MusicDatabase {
playlist.updatedAt = Date.now(); playlist.updatedAt = Date.now();
this._updatePlaylistMetadata(playlist); this._updatePlaylistMetadata(playlist);
await this.performTransaction('user_playlists', 'readwrite', (store) => store.put(playlist)); await this.performTransaction('user_playlists', 'readwrite', (store) => store.put(playlist));
this._dispatchPlaylistSync('update', playlist);
return playlist; return playlist;
} }
@ -420,17 +453,33 @@ export class MusicDatabase {
playlist.updatedAt = Date.now(); playlist.updatedAt = Date.now();
this._updatePlaylistMetadata(playlist); this._updatePlaylistMetadata(playlist);
await this.performTransaction('user_playlists', 'readwrite', (store) => store.put(playlist)); await this.performTransaction('user_playlists', 'readwrite', (store) => store.put(playlist));
this._dispatchPlaylistSync('update', playlist);
return playlist; return playlist;
} }
async deletePlaylist(playlistId) { async deletePlaylist(playlistId) {
await this.performTransaction('user_playlists', 'readwrite', (store) => store.delete(playlistId)); await this.performTransaction('user_playlists', 'readwrite', (store) => store.delete(playlistId));
// TRIGGER SYNC (but for deleting)
this._dispatchPlaylistSync('delete', { id: playlistId });
} }
async getPlaylist(playlistId) { async getPlaylist(playlistId) {
return await this.performTransaction('user_playlists', 'readonly', (store) => store.get(playlistId)); return await this.performTransaction('user_playlists', 'readonly', (store) => store.get(playlistId));
} }
async updatePlaylist(playlist) {
playlist.updatedAt = Date.now();
this._updatePlaylistMetadata(playlist);
await this.performTransaction('user_playlists', 'readwrite', (store) => store.put(playlist));
this._dispatchPlaylistSync('update', playlist);
return playlist;
}
async getPlaylists(includeTracks = false) { async getPlaylists(includeTracks = false) {
const db = await this.open(); const db = await this.open();
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {

View file

@ -16,7 +16,7 @@ import { showNotification, downloadTrackWithMetadata } from './downloads.js';
import { lyricsSettings, downloadQualitySettings } from './storage.js'; import { lyricsSettings, downloadQualitySettings } from './storage.js';
import { updateTabTitle } from './router.js'; import { updateTabTitle } from './router.js';
import { db } from './db.js'; import { db } from './db.js';
import { syncManager } from './firebase/sync.js'; import { syncManager } from './accounts/pocketbase.js';
import { waveformGenerator } from './waveform.js'; import { waveformGenerator } from './waveform.js';
let currentWaveformPeaks = null; let currentWaveformPeaks = null;

View file

@ -1,465 +0,0 @@
// js/firebase/sync.js
import { database } from './config.js';
import {
ref,
get,
set,
update,
onValue,
off,
child,
remove,
runTransaction,
onChildAdded,
onChildChanged,
onChildRemoved,
} from 'https://www.gstatic.com/firebasejs/10.7.1/firebase-database.js';
import { db } from '../db.js';
export class SyncManager {
constructor() {
this.user = null;
this.userRef = null;
this.unsubscribeFunctions = [];
this.isSyncing = false;
this.listenersSetup = false;
}
initialize(user) {
if (!database || !user) return;
this.user = user;
this.userRef = ref(database, `users/${user.uid}`);
console.log('Initializing SyncManager for user:', user.uid);
this.performInitialSync();
}
disconnect() {
if (this.userRef) {
// Remove listeners
this.unsubscribeFunctions.forEach((unsub) => unsub());
this.unsubscribeFunctions = [];
}
this.user = null;
this.userRef = null;
this.listenersSetup = false;
console.log('SyncManager disconnected');
}
async performInitialSync() {
if (this.isSyncing) return;
this.isSyncing = true;
try {
console.log('Starting initial sync...');
// 1. Fetch Cloud Data
const snapshot = await get(this.userRef);
const cloudData = snapshot.val() || {};
// 2. Fetch Local Data
const localData = await db.exportData();
// Filter out deleted playlists from local data
if (localData.user_playlists && Array.isArray(localData.user_playlists)) {
const cloudPlaylists = cloudData.user_playlists || {};
localData.user_playlists = localData.user_playlists.filter((p) => {
const cloudP = cloudPlaylists[p.id];
return !cloudP || !cloudP.deleted;
});
}
// 3. Merge Data (Union Strategy)
const mergedData = this.mergeData(localData, cloudData);
// 4. Update Cloud (if different)
// We optimize by just rewriting the whole node for simplicity in Phase 1,
// or we could diff. Rewriting is safer for "Initial Merge".
await update(this.userRef, mergedData);
// 5. Update Local (Import merged data)
// Convert Cloud Schema back to Local Schema for IndexedDB
const importData = {
favorites_tracks: mergedData.library?.tracks ? Object.values(mergedData.library.tracks) : [],
favorites_albums: mergedData.library?.albums ? Object.values(mergedData.library.albums) : [],
favorites_artists: mergedData.library?.artists ? Object.values(mergedData.library.artists) : [],
favorites_playlists: mergedData.library?.playlists ? Object.values(mergedData.library.playlists) : [],
history_tracks: mergedData.history?.recentTracks ? Object.values(mergedData.history.recentTracks) : [],
user_playlists: mergedData.user_playlists ? Object.values(mergedData.user_playlists) : [],
};
await db.importData(importData, true);
console.log('Initial sync complete.');
} catch (error) {
console.error('Initial sync failed:', error);
} finally {
this.setupListeners();
this.isSyncing = false;
}
}
mergeData(local, cloud) {
// Helper to merge lists of objects based on ID/UUID
// We assume 'favorites_*' structure from db.exportData()
const mergeStores = (localItems, cloudItems, idKey = 'id') => {
const map = new Map();
// Add all local items
if (Array.isArray(localItems)) {
localItems.forEach((item) => map.set(item[idKey], item));
} else if (localItems && typeof localItems === 'object') {
// Handle case where cloud stores as object keys
Object.values(localItems).forEach((item) => map.set(item[idKey], item));
}
// Add/Overwrite with cloud items (Union Strategy)
if (cloudItems) {
const processItem = (item, key) => {
if (!item || typeof item !== 'object') return;
if (item.tracks && typeof item.tracks === 'object' && !Array.isArray(item.tracks)) {
item.tracks = Object.values(item.tracks);
}
const id = item[idKey] || key;
const localItem = map.get(id);
if (item.deleted) {
map.delete(id);
} else if (localItem) {
const localTime = localItem.updatedAt || 0;
const cloudTime = item.updatedAt || 0;
if (cloudTime > localTime) {
const localTracks = Array.isArray(localItem.tracks) ? localItem.tracks.length : 0;
const cloudTracks = Array.isArray(item.tracks) ? item.tracks.length : 0;
if (localTracks > 0 && cloudTracks === 0) {
} else {
map.set(id, item);
}
} else if (cloudTime === localTime) {
const localTracks = Array.isArray(localItem.tracks) ? localItem.tracks.length : 0;
const cloudTracks = Array.isArray(item.tracks) ? item.tracks.length : 0;
if (cloudTracks >= localTracks) {
map.set(id, item);
}
}
} else {
map.set(id, item);
}
};
if (Array.isArray(cloudItems)) {
cloudItems.forEach((item) => processItem(item));
} else {
Object.keys(cloudItems).forEach((key) => {
const val = cloudItems[key];
if (typeof val === 'object') {
processItem(val, key);
}
});
}
}
return Array.from(map.values());
};
const merged = {
library: {
tracks: this.arrayToObject(mergeStores(local.favorites_tracks, cloud.library?.tracks), 'id'),
albums: this.arrayToObject(mergeStores(local.favorites_albums, cloud.library?.albums), 'id'),
artists: this.arrayToObject(mergeStores(local.favorites_artists, cloud.library?.artists), 'id'),
playlists: this.arrayToObject(
mergeStores(local.favorites_playlists, cloud.library?.playlists, 'uuid'),
'uuid'
),
},
history: {
recentTracks: this.arrayToObject(
mergeStores(local.history_tracks, cloud.history?.recentTracks, 'timestamp'),
'timestamp'
),
},
user_playlists: this.arrayToObject(mergeStores(local.user_playlists, cloud.user_playlists), 'id'),
// Settings are NOT synced (device specific)
lastUpdated: Date.now(),
};
// Transform back to local structure for db.importData
return merged;
}
// Helper to convert array to object with keys
arrayToObject(arr, keyField) {
const obj = {};
arr.forEach((item) => {
if (item && item[keyField]) {
obj[item[keyField]] = item;
}
});
return obj;
}
setupListeners() {
if (!this.userRef || this.listenersSetup) return;
this.listenersSetup = true;
const setupLibraryListener = (nodeName, storeName) => {
const nodeRef = child(this.userRef, `library/${nodeName}`);
const handleAddOrChange = (snapshot) => {
if (this.isSyncing) return;
const val = snapshot.val();
if (val) {
const importData = {};
importData[storeName] = [val];
db.importData(importData, false).then((changed) => {
if (changed) window.dispatchEvent(new Event('library-changed'));
});
}
};
const handleRemove = (snapshot) => {
if (this.isSyncing) return;
const key = snapshot.key;
if (key) {
db.performTransaction(storeName, 'readwrite', (store) => store.delete(key)).then(() => {
window.dispatchEvent(new Event('library-changed'));
});
}
};
const unsubAdd = onChildAdded(nodeRef, handleAddOrChange);
const unsubChange = onChildChanged(nodeRef, handleAddOrChange);
const unsubRemove = onChildRemoved(nodeRef, handleRemove);
this.unsubscribeFunctions.push(() => off(nodeRef, 'child_added', unsubAdd));
this.unsubscribeFunctions.push(() => off(nodeRef, 'child_changed', unsubChange));
this.unsubscribeFunctions.push(() => off(nodeRef, 'child_removed', unsubRemove));
};
setupLibraryListener('tracks', 'favorites_tracks');
setupLibraryListener('albums', 'favorites_albums');
setupLibraryListener('artists', 'favorites_artists');
setupLibraryListener('playlists', 'favorites_playlists');
// Listen for changes in history
const historyRef = child(this.userRef, 'history/recentTracks');
const unsubHistoryAdd = onChildAdded(historyRef, (snapshot) => {
if (this.isSyncing) return;
const val = snapshot.val();
if (val) {
db.importData({ history_tracks: [val] }, false).then((changed) => {
if (changed) window.dispatchEvent(new Event('history-changed'));
});
}
});
this.unsubscribeFunctions.push(() => off(historyRef, 'child_added', unsubHistoryAdd));
// Listen for changes in user playlists
const userPlaylistsRef = child(this.userRef, 'user_playlists');
const handlePlaylistUpdate = (snapshot) => {
if (this.isSyncing) return;
const val = snapshot.val();
if (val && val.deleted) {
db.deletePlaylist(val.id).then(() => {
window.dispatchEvent(new Event('library-changed'));
});
return;
}
if (val) {
if (val.tracks && typeof val.tracks === 'object' && !Array.isArray(val.tracks)) {
val.tracks = Object.values(val.tracks);
}
const importData = {
user_playlists: [val],
};
db.importData(importData, false).then((changed) => {
// Notify UI to refresh library
if (changed) window.dispatchEvent(new Event('library-changed'));
});
}
};
const unsubChildAdded = onChildAdded(userPlaylistsRef, handlePlaylistUpdate);
const unsubChildChanged = onChildChanged(userPlaylistsRef, handlePlaylistUpdate);
const unsubChildRemoved = onChildRemoved(userPlaylistsRef, (snapshot) => {
if (this.isSyncing) return;
const key = snapshot.key;
if (key) {
db.deletePlaylist(key).then(() => {
window.dispatchEvent(new Event('library-changed'));
});
}
});
this.unsubscribeFunctions.push(() => {
off(userPlaylistsRef, 'child_added', unsubChildAdded);
off(userPlaylistsRef, 'child_changed', unsubChildChanged);
off(userPlaylistsRef, 'child_removed', unsubChildRemoved);
});
}
// --- Public API for Broadcasters ---
async syncLibraryItem(type, item, isAdded) {
if (!this.user || !this.userRef) return;
// type: 'track', 'album', 'artist', 'playlist'
// item: the object (minified preferably)
// isAdded: boolean
const categoryMap = {
track: 'tracks',
album: 'albums',
artist: 'artists',
playlist: 'playlists',
};
const category = categoryMap[type];
if (!category) return;
const id = type === 'playlist' ? item.uuid : item.id;
const path = `library/${category}/${id}`;
const itemRef = child(this.userRef, path);
if (isAdded) {
// Minify to ensure consistency and reduce bandwidth
// We use the db helper to ensure consistent structure
const minified = db._minifyItem(type, item);
// Ensure addedAt is present. If the passed item didn't have it (e.g. from player),
// we add it now. Ideally this matches local DB time, but a small diff is negligible.
const entry = {
...minified,
addedAt: item.addedAt || minified.addedAt || Date.now(),
};
await set(itemRef, this.sanitizeForFirebase(entry));
} else {
await remove(itemRef);
}
}
async syncHistoryItem(track) {
if (!this.user || !this.userRef || !track.timestamp) return;
const itemRef = child(this.userRef, `history/recentTracks/${track.timestamp}`);
try {
await set(itemRef, this.sanitizeForFirebase(track));
const localHistory = await db.getHistory();
if (localHistory.length > 50) {
const toRemove = localHistory.slice(50);
const updates = {};
toRemove.forEach((t) => {
updates[`history/recentTracks/${t.timestamp}`] = null;
});
await update(this.userRef, updates);
}
} catch (error) {
console.error('Failed to sync history item:', error);
}
}
async syncUserPlaylist(playlist, action) {
if (!this.user || !this.userRef) return;
const id = playlist.id;
const path = `user_playlists/${id}`;
const itemRef = child(this.userRef, path);
if (action === 'create' || action === 'update') {
const dataToSync = {
...playlist,
updatedAt: Date.now(),
};
await set(itemRef, this.sanitizeForFirebase(dataToSync));
} else if (action === 'delete') {
await update(itemRef, { deleted: true, updatedAt: Date.now() });
}
}
async clearCloudData() {
if (!this.user || !this.userRef) {
throw new Error('Not authenticated');
}
await remove(this.userRef);
}
// Public Playlist API
async publishPlaylist(playlist) {
if (!this.user) throw new Error('Not authenticated');
const minified = db._minifyItem('playlist', playlist);
const playlistId = playlist.id || playlist.uuid;
if (!playlistId) throw new Error('Invalid playlist ID');
// Ensure playlist has necessary data
const publicData = {
...minified,
uid: this.user.uid,
originalId: playlistId,
publishedAt: Date.now(),
tracks: playlist.tracks ? playlist.tracks.map((t) => db._minifyItem('track', t)) : [],
};
// Use a global 'public_playlists' node
const publicRef = ref(database, `public_playlists/${playlistId}`);
await set(publicRef, this.sanitizeForFirebase(publicData));
}
async unpublishPlaylist(playlistId) {
if (!this.user) throw new Error('Not authenticated');
const publicRef = ref(database, `public_playlists/${playlistId}`);
await remove(publicRef);
}
async getPublicPlaylist(playlistId) {
if (!database) {
console.warn('[Sync] Database not initialized, cannot fetch public playlist');
return null;
}
try {
const publicRef = ref(database, `public_playlists/${playlistId}`);
const snapshot = await get(publicRef);
if (!snapshot.exists()) {
console.warn(`[Sync] Public playlist ${playlistId} not found in database.`);
return null;
}
const data = snapshot.val();
console.log(`[Sync] Public playlist fetch for ${playlistId}: Found`);
return data;
} catch (error) {
console.error('[Sync] Failed to fetch public playlist:', error);
return null;
}
}
sanitizeForFirebase(obj) {
if (obj === undefined) return null;
if (obj === null) return null;
if (typeof obj !== 'object') return obj;
if (Array.isArray(obj)) {
return obj.map((v) => this.sanitizeForFirebase(v));
}
const newObj = {};
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
const val = this.sanitizeForFirebase(obj[key]);
if (val !== undefined) {
newObj[key] = val;
}
}
}
return newObj;
}
}
export const syncManager = new SyncManager();

View file

@ -13,14 +13,12 @@ import {
downloadQualitySettings, downloadQualitySettings,
} from './storage.js'; } from './storage.js';
import { db } from './db.js'; import { db } from './db.js';
import { authManager } from './firebase/auth.js'; import { authManager } from './accounts/auth.js';
import { syncManager } from './firebase/sync.js'; import { syncManager } from './accounts/pocketbase.js';
import { initializeFirebaseSettingsUI } from './firebase/config.js';
export function initializeSettings(scrobbler, player, api, ui) { export function initializeSettings(scrobbler, player, api, ui) {
// Initialize Firebase UI & Settings // Initialize account system UI & Settings
authManager.updateUI(authManager.user); authManager.updateUI(authManager.user);
initializeFirebaseSettingsUI();
// Email Auth UI Logic // Email Auth UI Logic
const toggleEmailBtn = document.getElementById('toggle-email-auth-btn'); const toggleEmailBtn = document.getElementById('toggle-email-auth-btn');

View file

@ -81,8 +81,8 @@ export function initializeUIInteractions(player, api) {
const likeBtn = container.querySelector('#like-queue-btn'); const likeBtn = container.querySelector('#like-queue-btn');
if (likeBtn) { if (likeBtn) {
likeBtn.addEventListener('click', async () => { likeBtn.addEventListener('click', async () => {
const { db } = await import('./db.js'); const { db } = await import('./db.js'); // Already imported
const { syncManager } = await import('./firebase/sync.js'); const { syncManager } = await import('./accounts/pocketbase.js');
const { showNotification } = await import('./downloads.js'); const { showNotification } = await import('./downloads.js');
let addedCount = 0; let addedCount = 0;
@ -107,8 +107,8 @@ export function initializeUIInteractions(player, api) {
const addToPlaylistBtn = container.querySelector('#add-queue-to-playlist-btn'); const addToPlaylistBtn = container.querySelector('#add-queue-to-playlist-btn');
if (addToPlaylistBtn) { if (addToPlaylistBtn) {
addToPlaylistBtn.addEventListener('click', async () => { addToPlaylistBtn.addEventListener('click', async () => {
const { db } = await import('./db.js'); const { db } = await import('./db.js'); // Already imported
const { syncManager } = await import('./firebase/sync.js'); const { syncManager } = await import('./accounts/pocketbase.js');
const { showNotification } = await import('./downloads.js'); const { showNotification } = await import('./downloads.js');
const playlists = await db.getPlaylists(); const playlists = await db.getPlaylists();
@ -259,8 +259,8 @@ export function initializeUIInteractions(player, api) {
e.stopPropagation(); e.stopPropagation();
const track = player.getCurrentQueue()[index]; const track = player.getCurrentQueue()[index];
if (track) { if (track) {
const { db } = await import('./db.js'); const { db } = await import('./db.js'); // Already imported
const { syncManager } = await import('./firebase/sync.js'); const { syncManager } = await import('./accounts/pocketbase.js');
const { showNotification } = await import('./downloads.js'); const { showNotification } = await import('./downloads.js');
const added = await db.toggleFavorite('track', track); const added = await db.toggleFavorite('track', track);

View file

@ -18,7 +18,7 @@ import { openLyricsPanel } from './lyrics.js';
import { recentActivityManager, backgroundSettings, trackListSettings, cardSettings } from './storage.js'; import { recentActivityManager, backgroundSettings, trackListSettings, cardSettings } from './storage.js';
import { db } from './db.js'; import { db } from './db.js';
import { getVibrantColorFromImage } from './vibrant-color.js'; import { getVibrantColorFromImage } from './vibrant-color.js';
import { syncManager } from './firebase/sync.js'; import { syncManager } from './accounts/pocketbase.js';
export class UIRenderer { export class UIRenderer {
constructor(api, player) { constructor(api, player) {
@ -1366,12 +1366,12 @@ export class UIRenderer {
ownedPlaylist = await db.getPlaylist(playlistId); ownedPlaylist = await db.getPlaylist(playlistId);
playlistData = ownedPlaylist; playlistData = ownedPlaylist;
// If not in local DB, check if it's a public Firebase playlist // If not in local DB, check if it's a public Pocketbase playlist
if (!playlistData) { if (!playlistData) {
try { try {
playlistData = await syncManager.getPublicPlaylist(playlistId); playlistData = await syncManager.getPublicPlaylist(playlistId);
} catch (e) { } catch (e) {
console.warn('Failed to check public Firebase playlists:', e); console.warn('Failed to check public pocketbase playlists:', e);
} }
} }
} }
@ -1379,7 +1379,7 @@ export class UIRenderer {
if (playlistData) { if (playlistData) {
// ... (rest of the logic) // ... (rest of the logic)
// Render user or public firebase playlist // Render user or public Pocketbase playlist
imageEl.src = playlistData.cover || 'assets/appicon.png'; imageEl.src = playlistData.cover || 'assets/appicon.png';
imageEl.style.backgroundColor = ''; imageEl.style.backgroundColor = '';

17555
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -41,5 +41,8 @@
"overrides": { "overrides": {
"sourcemap-codec": "npm:@jridgewell/sourcemap-codec@^1.4.14", "sourcemap-codec": "npm:@jridgewell/sourcemap-codec@^1.4.14",
"source-map": "^0.7.4" "source-map": "^0.7.4"
},
"dependencies": {
"pocketbase": "^0.26.5"
} }
} }