Merge branch 'main' of github.com:SamidyFR/monochrome

This commit is contained in:
Samidy 2026-01-17 22:45:52 +03:00
commit 1658684197
4 changed files with 301 additions and 108 deletions

View file

@ -308,22 +308,45 @@
<div id="custom-db-modal" class="modal">
<div class="modal-overlay"></div>
<div class="modal-content">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;">
<h3 style="margin: 0;">Custom Database/Auth</h3>
<button id="custom-db-reset" class="btn-secondary danger" style="padding: 0.4rem 0.8rem; font-size: 0.8rem;">Reset to Defaults</button>
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem">
<h3 style="margin: 0">Custom Database/Auth</h3>
<button
id="custom-db-reset"
class="btn-secondary danger"
style="padding: 0.4rem 0.8rem; font-size: 0.8rem"
>
Reset to Defaults
</button>
</div>
<p style="font-size: 0.9rem; color: var(--muted-foreground); margin-bottom: 1rem;">
<p style="font-size: 0.9rem; color: var(--muted-foreground); margin-bottom: 1rem">
Configure custom PocketBase and Firebase instances. Leave empty to use defaults.
<br>
A Guide To Set This Up Can Be Found <a href="https://github.com/SamidyFR/monochrome/blob/main/self-hosted-database.md" style="text-decoration: underline;">Here</a>.
<br />
A Guide To Set This Up Can Be Found
<a
href="https://github.com/SamidyFR/monochrome/blob/main/self-hosted-database.md"
style="text-decoration: underline"
>Here</a
>.
</p>
<div style="margin-bottom: 1rem;">
<label style="display: block; margin-bottom: 0.5rem; font-size: 0.9rem;">PocketBase URL</label>
<input type="url" id="custom-pb-url" class="template-input" placeholder="https://monodb.samidy.com">
<div style="margin-bottom: 1rem">
<label style="display: block; margin-bottom: 0.5rem; font-size: 0.9rem">PocketBase URL</label>
<input
type="url"
id="custom-pb-url"
class="template-input"
placeholder="https://monodb.samidy.com"
/>
</div>
<div style="margin-bottom: 1rem;">
<label style="display: block; margin-bottom: 0.5rem; font-size: 0.9rem;">Firebase Configuration (JSON)</label>
<textarea id="custom-firebase-config" class="template-input" style="height: 150px; font-family: monospace; font-size: 0.8rem; resize: vertical;" placeholder='{"apiKey": "...", ...}'></textarea>
<div style="margin-bottom: 1rem">
<label style="display: block; margin-bottom: 0.5rem; font-size: 0.9rem"
>Firebase Configuration (JSON)</label
>
<textarea
id="custom-firebase-config"
class="template-input"
style="height: 150px; font-family: monospace; font-size: 0.8rem; resize: vertical"
placeholder='{"apiKey": "...", ...}'
></textarea>
</div>
<div class="modal-actions">
<button id="custom-db-cancel" class="btn-secondary">Cancel</button>
@ -617,44 +640,147 @@
</header>
<div id="page-home" class="page">
<div id="home-welcome" style="display: none; text-align: center; padding: 4rem 2rem;">
<h2 style="margin-bottom: 1rem;">Welcome to Monochrome</h2>
<p style="color: var(--muted-foreground);">You haven't listened to anything yet. Search for your favorite songs to get started!</p>
<div id="home-welcome" style="display: none; text-align: center; padding: 4rem 2rem">
<h2 style="margin-bottom: 1rem">Welcome to Monochrome</h2>
<p style="color: var(--muted-foreground)">
You haven't listened to anything yet. Search for your favorite songs to get started!
</p>
</div>
<div id="home-content" style="display: none;">
<div id="home-content" style="display: none">
<section class="content-section">
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 1rem;">
<h2 class="section-title" style="margin-bottom: 0;">Recommended Songs</h2>
<button class="btn-secondary" id="refresh-songs-btn" title="Refresh" style="padding: 4px 8px;">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8"/><path d="M21 3v5h-5"/></svg>
<div
style="
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 1rem;
"
>
<h2 class="section-title" style="margin-bottom: 0">Recommended Songs</h2>
<button
class="btn-secondary"
id="refresh-songs-btn"
title="Refresh"
style="padding: 4px 8px"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8" />
<path d="M21 3v5h-5" />
</svg>
</button>
</div>
<div class="track-list" id="home-recommended-songs"></div>
</section>
<section class="content-section">
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 1rem;">
<h2 class="section-title" style="margin-bottom: 0;">Recommended Albums</h2>
<button class="btn-secondary" id="refresh-albums-btn" title="Refresh" style="padding: 4px 8px;">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8"/><path d="M21 3v5h-5"/></svg>
<div
style="
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 1rem;
"
>
<h2 class="section-title" style="margin-bottom: 0">Recommended Albums</h2>
<button
class="btn-secondary"
id="refresh-albums-btn"
title="Refresh"
style="padding: 4px 8px"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8" />
<path d="M21 3v5h-5" />
</svg>
</button>
</div>
<div class="card-grid" id="home-recommended-albums"></div>
</section>
<section class="content-section">
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 1rem;">
<h2 class="section-title" style="margin-bottom: 0;">Recommended Artists</h2>
<button class="btn-secondary" id="refresh-artists-btn" title="Refresh" style="padding: 4px 8px;">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8"/><path d="M21 3v5h-5"/></svg>
<div
style="
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 1rem;
"
>
<h2 class="section-title" style="margin-bottom: 0">Recommended Artists</h2>
<button
class="btn-secondary"
id="refresh-artists-btn"
title="Refresh"
style="padding: 4px 8px"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8" />
<path d="M21 3v5h-5" />
</svg>
</button>
</div>
<div class="card-grid" id="home-recommended-artists"></div>
</section>
<section class="content-section">
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 1rem;">
<h2 class="section-title" style="margin-bottom: 0;">Jump Back In</h2>
<button class="btn-secondary" id="clear-recent-btn" title="Clear History" style="padding: 4px 8px;">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/></svg>
<div
style="
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 1rem;
"
>
<h2 class="section-title" style="margin-bottom: 0">Jump Back In</h2>
<button
class="btn-secondary"
id="clear-recent-btn"
title="Clear History"
style="padding: 4px 8px"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M3 6h18" />
<path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6" />
<path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2" />
</svg>
</button>
</div>
<div class="card-grid" id="home-recent-mixed"></div>
@ -705,9 +831,38 @@
<button class="search-tab" data-tab="local">Local Files</button>
</div>
<div class="search-tab-content active" id="library-tab-tracks">
<div style="display: flex; justify-content: flex-start; margin-bottom: 0.5rem;">
<button id="shuffle-liked-tracks-btn" class="btn-secondary" style="display: none; width: 32px; height: 32px; padding: 0; align-items: center; justify-content: center; border-radius: 50%;" title="Shuffle Liked Tracks">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m18 14 4 4-4 4"/><path d="m18 2 4 4-4 4"/><path d="M2 18h1.973a4 4 0 0 0 3.3-1.7l5.454-8.6a4 4 0 0 1 3.3-1.7H22"/><path d="M2 6h1.972a4 4 0 0 1 3.6 2.2"/><path d="M22 18h-6.041a4 4 0 0 1-3.3-1.8l-.359-.45"/></svg>
<div style="display: flex; justify-content: flex-start; margin-bottom: 0.5rem">
<button
id="shuffle-liked-tracks-btn"
class="btn-secondary"
style="
display: none;
width: 32px;
height: 32px;
padding: 0;
align-items: center;
justify-content: center;
border-radius: 50%;
"
title="Shuffle Liked Tracks"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="m18 14 4 4-4 4" />
<path d="m18 2 4 4-4 4" />
<path d="M2 18h1.973a4 4 0 0 0 3.3-1.7l5.454-8.6a4 4 0 0 1 3.3-1.7H22" />
<path d="M2 6h1.972a4 4 0 0 1 3.6 2.2" />
<path d="M22 18h-6.041a4 4 0 0 1-3.3-1.8l-.359-.45" />
</svg>
</button>
</div>
<div class="track-list" id="library-tracks-container"></div>
@ -1602,16 +1757,21 @@
sensitive. <br />
</p>
<p style="padding-top: 50px; text-align: center; color: #8b8b93">
However, if you want complete control over your data, we allow you to use your own Database Configuration.
However, if you want complete control over your data, we allow you to use your own Database
Configuration.
</p>
<div style="
<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>
"
>
<a id="advanced-config-link" class="btn-secondary" href="#settings"
>Advanced: Custom Configuration</a
>
</div>
</div>
</div>

View file

@ -1,5 +1,15 @@
//js/events.js
import { SVG_PLAY, SVG_PAUSE, SVG_VOLUME, SVG_MUTE, REPEAT_MODE, trackDataStore, formatTime, SVG_BIN, escapeHtml } from './utils.js';
import {
SVG_PLAY,
SVG_PAUSE,
SVG_VOLUME,
SVG_MUTE,
REPEAT_MODE,
trackDataStore,
formatTime,
SVG_BIN,
escapeHtml,
} from './utils.js';
import { lastFMStorage, waveformSettings } from './storage.js';
import { showNotification, downloadTrackWithMetadata } from './downloads.js';
import { downloadQualitySettings } from './storage.js';
@ -705,9 +715,9 @@ export async function handleTrackAction(
const handleOptionClick = async (e) => {
const removeBtn = e.target.closest('.remove-from-playlist-btn-modal');
const option = e.target.closest('.modal-option');
if (!option) return;
const playlistId = option.dataset.id;
if (removeBtn) {
@ -719,7 +729,7 @@ export async function handleTrackAction(
await renderModal();
} else {
if (option.classList.contains('already-contains')) return;
await db.addTrackToPlaylist(playlistId, item);
const updatedPlaylist = await db.getPlaylist(playlistId);
syncManager.syncUserPlaylist(updatedPlaylist, 'update');

122
js/ui.js
View file

@ -836,14 +836,14 @@ export class UIRenderer {
async renderHomePage() {
this.showPage('home');
const welcomeEl = document.getElementById('home-welcome');
const contentEl = document.getElementById('home-content');
const history = await db.getHistory();
const favorites = await db.getFavorites('track');
const playlists = await db.getPlaylists(true);
if (history.length === 0 && favorites.length === 0 && playlists.length === 0) {
if (welcomeEl) welcomeEl.style.display = 'block';
if (contentEl) contentEl.style.display = 'none';
@ -861,12 +861,13 @@ export class UIRenderer {
if (refreshSongsBtn) refreshSongsBtn.onclick = () => this.renderHomeSongs(true);
if (refreshAlbumsBtn) refreshAlbumsBtn.onclick = () => this.renderHomeAlbums(true);
if (refreshArtistsBtn) refreshArtistsBtn.onclick = () => this.renderHomeArtists(true);
if (clearRecentBtn) clearRecentBtn.onclick = () => {
if (confirm('Clear recent activity?')) {
recentActivityManager.clear();
this.renderHomeRecent();
}
};
if (clearRecentBtn)
clearRecentBtn.onclick = () => {
if (confirm('Clear recent activity?')) {
recentActivityManager.clear();
this.renderHomeRecent();
}
};
this.renderHomeSongs();
this.renderHomeAlbums();
@ -878,18 +879,18 @@ export class UIRenderer {
const history = await db.getHistory();
const favorites = await db.getFavorites('track');
const playlists = await db.getPlaylists(true);
const playlistTracks = playlists.flatMap(p => p.tracks || []);
const playlistTracks = playlists.flatMap((p) => p.tracks || []);
// Prioritize: Playlists > Favorites > History
// Take random samples from each to form seeds
const shuffle = (arr) => [...arr].sort(() => Math.random() - 0.5);
const seeds = [
...shuffle(playlistTracks).slice(0, 20),
...shuffle(favorites).slice(0, 20),
...shuffle(history).slice(0, 10)
...shuffle(history).slice(0, 10),
];
return shuffle(seeds);
}
@ -903,7 +904,7 @@ export class UIRenderer {
const seeds = await this.getSeeds();
const trackSeeds = seeds.slice(0, 5);
const recommendedTracks = await this.api.getRecommendedTracksForPlaylist(trackSeeds, 20);
const filteredTracks = await this.filterUserContent(recommendedTracks, 'track');
if (filteredTracks.length > 0) {
@ -926,14 +927,17 @@ export class UIRenderer {
try {
const seeds = await this.getSeeds();
const albumSeed = seeds.find(t => t.album && t.album.id);
const albumSeed = seeds.find((t) => t.album && t.album.id);
if (albumSeed) {
const similarAlbums = await this.api.getSimilarAlbums(albumSeed.album.id);
const filteredAlbums = await this.filterUserContent(similarAlbums, 'album');
if (filteredAlbums.length > 0) {
albumsContainer.innerHTML = filteredAlbums.slice(0, 12).map(a => this.createAlbumCardHTML(a)).join('');
filteredAlbums.slice(0, 12).forEach(a => {
albumsContainer.innerHTML = filteredAlbums
.slice(0, 12)
.map((a) => this.createAlbumCardHTML(a))
.join('');
filteredAlbums.slice(0, 12).forEach((a) => {
const el = albumsContainer.querySelector(`[data-album-id="${a.id}"]`);
if (el) {
trackDataStore.set(el, a);
@ -961,16 +965,19 @@ export class UIRenderer {
try {
const seeds = await this.getSeeds();
const artistSeed = seeds.find(t => (t.artist && t.artist.id) || (t.artists && t.artists.length > 0));
const artistId = artistSeed ? (artistSeed.artist?.id || artistSeed.artists?.[0]?.id) : null;
const artistSeed = seeds.find((t) => (t.artist && t.artist.id) || (t.artists && t.artists.length > 0));
const artistId = artistSeed ? artistSeed.artist?.id || artistSeed.artists?.[0]?.id : null;
if (artistId) {
const similarArtists = await this.api.getSimilarArtists(artistId);
const filteredArtists = await this.filterUserContent(similarArtists, 'artist');
if (filteredArtists.length > 0) {
artistsContainer.innerHTML = filteredArtists.slice(0, 12).map(a => this.createArtistCardHTML(a)).join('');
filteredArtists.slice(0, 12).forEach(a => {
artistsContainer.innerHTML = filteredArtists
.slice(0, 12)
.map((a) => this.createArtistCardHTML(a))
.join('');
filteredArtists.slice(0, 12).forEach((a) => {
const el = artistsContainer.querySelector(`[data-artist-id="${a.id}"]`);
if (el) {
trackDataStore.set(el, a);
@ -981,7 +988,9 @@ export class UIRenderer {
artistsContainer.innerHTML = createPlaceholder('No artist recommendations found.');
}
} else {
artistsContainer.innerHTML = createPlaceholder('Listen to more music to get artist recommendations.');
artistsContainer.innerHTML = createPlaceholder(
'Listen to more music to get artist recommendations.'
);
}
} catch (e) {
console.error(e);
@ -995,36 +1004,43 @@ export class UIRenderer {
if (recentContainer) {
const recents = recentActivityManager.getRecents();
const items = [];
if (recents.albums) items.push(...recents.albums.slice(0, 4).map(i => ({...i, _kind: 'album'})));
if (recents.playlists) items.push(...recents.playlists.slice(0, 4).map(i => ({...i, _kind: 'playlist'})));
if (recents.mixes) items.push(...recents.mixes.slice(0, 4).map(i => ({...i, _kind: 'mix'})));
if (recents.albums) items.push(...recents.albums.slice(0, 4).map((i) => ({ ...i, _kind: 'album' })));
if (recents.playlists)
items.push(...recents.playlists.slice(0, 4).map((i) => ({ ...i, _kind: 'playlist' })));
if (recents.mixes) items.push(...recents.mixes.slice(0, 4).map((i) => ({ ...i, _kind: 'mix' })));
items.sort(() => Math.random() - 0.5);
const displayItems = items.slice(0, 6);
if (displayItems.length > 0) {
recentContainer.innerHTML = displayItems.map(item => {
if (item._kind === 'album') return this.createAlbumCardHTML(item);
if (item._kind === 'playlist') {
if (item.isUserPlaylist) return this.createUserPlaylistCardHTML(item);
return this.createPlaylistCardHTML(item);
}
if (item._kind === 'mix') return this.createMixCardHTML(item);
return '';
}).join('');
recentContainer.innerHTML = displayItems
.map((item) => {
if (item._kind === 'album') return this.createAlbumCardHTML(item);
if (item._kind === 'playlist') {
if (item.isUserPlaylist) return this.createUserPlaylistCardHTML(item);
return this.createPlaylistCardHTML(item);
}
if (item._kind === 'mix') return this.createMixCardHTML(item);
return '';
})
.join('');
displayItems.forEach(item => {
displayItems.forEach((item) => {
let selector = '';
if (item._kind === 'album') selector = `[data-album-id="${item.id}"]`;
else if (item._kind === 'playlist') selector = item.isUserPlaylist ? `[data-user-playlist-id="${item.id}"]` : `[data-playlist-id="${item.uuid}"]`;
else if (item._kind === 'playlist')
selector = item.isUserPlaylist
? `[data-user-playlist-id="${item.id}"]`
: `[data-playlist-id="${item.uuid}"]`;
else if (item._kind === 'mix') selector = `[data-mix-id="${item.id}"]`;
const el = recentContainer.querySelector(selector);
if (el) {
trackDataStore.set(el, item);
if (item._kind === 'album') this.updateLikeState(el, 'album', item.id);
if (item._kind === 'playlist' && !item.isUserPlaylist) this.updateLikeState(el, 'playlist', item.uuid);
if (item._kind === 'playlist' && !item.isUserPlaylist)
this.updateLikeState(el, 'playlist', item.uuid);
if (item._kind === 'mix') this.updateLikeState(el, 'mix', item.id);
}
});
@ -1038,19 +1054,19 @@ export class UIRenderer {
if (!items || items.length === 0) return [];
const favorites = await db.getFavorites(type);
const favoriteIds = new Set(favorites.map(i => i.id));
const favoriteIds = new Set(favorites.map((i) => i.id));
const likedTracks = await db.getFavorites('track');
const playlists = await db.getPlaylists(true);
const userTracksMap = new Map();
likedTracks.forEach(t => userTracksMap.set(t.id, t));
playlists.forEach(p => {
if (p.tracks) p.tracks.forEach(t => userTracksMap.set(t.id, t));
likedTracks.forEach((t) => userTracksMap.set(t.id, t));
playlists.forEach((p) => {
if (p.tracks) p.tracks.forEach((t) => userTracksMap.set(t.id, t));
});
if (type === 'track') {
return items.filter(item => !userTracksMap.has(item.id));
return items.filter((item) => !userTracksMap.has(item.id));
}
if (type === 'album') {
@ -1062,21 +1078,21 @@ export class UIRenderer {
}
}
return items.filter(item => {
return items.filter((item) => {
if (favoriteIds.has(item.id)) return false;
const userCount = albumTrackCounts.get(item.id) || 0;
const total = item.numberOfTracks;
if (total && total > 0) {
if ((userCount / total) > 0.5) return false;
if (userCount / total > 0.5) return false;
}
return true;
});
}
return items.filter(item => !favoriteIds.has(item.id));
return items.filter((item) => !favoriteIds.has(item.id));
}
async renderSearchPage(query) {

View file

@ -1,16 +1,16 @@
This guide will show you how to setup the necessary stuff to be able to use your own authentication system and database for accounts. please do note that you will have to enter the same configurations for each device.
**This Guide Assumes You're Doing everything On Your Local Machine. This is still fully possible on a VPS Though.**
### Required:
- A Computer (this computer will be the one hosting the database)
- Firebase Account (Only Used For Authentication)
- [PocketBase](https://pocketbase.io) (App we use to manage The Database, Install This on the computer you want to host the database on)
- Domain (you can get one for free at [DigitalPlat](https://domain.digitalplat.org/))
### Step 1: Setup Firebase Authentication
Go to the [Firebase Console](https://console.firebase.google.com) and create a new project. then, on the left sidebar, click the **Build** section and select **Authentication**.
1. Click **Get Started**.
@ -19,7 +19,8 @@ Go to the [Firebase Console](https://console.firebase.google.com) and create a n
4. Set your project support email and click **Save**.
### Step 1.1: Authorize The Domain
firebase by default makes you add trusted domains to connect to firebases authentication system, if your domain isnt on there, it wont allow you to login or signup.
firebase by default makes you add trusted domains to connect to firebases authentication system, if your domain isnt on there, it wont allow you to login or signup.
1. In the **Authentication** section, go to the **Settings** tab.
2. Click **Authorized domains** in the left sub-menu.
@ -28,16 +29,20 @@ firebase by default makes you add trusted domains to connect to firebases authen
- _Note: `localhost` is usually added by default for local testing. you likely wont have people abusing your system, so you can leave this in by default._
### Step 2: PocketBase Setup
1. download [PocketBase](https://pocketbase.io) and follow their setup guide.
2. make 2 collections: `DB_users` and `public_playlists`. do NOT use the normal "users" collection.
3. Add these fields to `DB_users`:
- name: `firebase_id` type: `Plain Text`
- name: `lastUpdated` type: `Number`
- name: `history` type: `JSON`
- name: `library` type: `JSON`
- name: `user_playlists` type: `JSON`
- name: `deleted_playlists` type: `JSON`
4. Add these fields to `public_playlists`:
- name: `firebase_id` type: `Plain Text`
- name: `addedAt` type: `Number`
- name: `numberOfTracks` type: `Number`
@ -48,19 +53,21 @@ firebase by default makes you add trusted domains to connect to firebases authen
- name: `uuid` type: `Plain Text`
- name: `tracks` type: `JSON`
- name: `image` type: `URL`
5. edit the `API Rules` for both `DB_users` and `public_playlists` to these:
#### `DB_users`
![DB_users](https://i.ibb.co/WvFgJvFJ/image.png)
#### `public_playlists`
![public_playlists](https://i.ibb.co/WpW7F3kk/image.png)
![public_playlists](https://i.ibb.co/WpW7F3kk/image.png)
Now that you have setup collections, rules and fields, we can now work on putting them out on the internet.
### Step 3: Cloudflared
while you can use the usual `127.0.0.1` link pocketbase gives you, this is a local domain and you cant enter it on any other device, so it would practically be useless. to open this up, while we can port forward, this could be dangerous and attackers could use that as a vulnerability. to securely set this up, we are going to be using cloudflared.
1. Make an account at the [Cloudflare Dashboard](https://dash.cloudflare.com).
@ -71,11 +78,10 @@ while you can use the usual `127.0.0.1` link pocketbase gives you, this is a loc
6. then, you will get a guide on how to install cloudflared and set it up for your machine.
7. You will get a window to setup hostnames, Note that you will require a valid domain as cloudflare doesnt allow `pages.dev` domains. you can get one for free at [DigitalPlat](https://domain.digitalplat.org/), but we will not show you how to set it up and how to connect it to cloudflare.
8. at the "Service" section for the setup hostnames window, select "HTTP" and input the URL for pocketbase (eg. `127.0.0.1:8090`).
after this, your database will be available at the chosen domain.
after this, your database will be available at the chosen domain.
### Step 4: Getting Configurations
You are almost done, now you just need to get configurations so you can add them to monochrome.
first, get your authentication config:
@ -85,6 +91,7 @@ first, get your authentication config:
3. In the **General** tab, scroll down to "Your apps" and click the **Web icon (`</>`)**.
4. Register the app (e.g., "Monochrome Auth").
5. You will see a `firebaseConfig` object. It looks like this:
```
const firebaseConfig = {
apiKey: 'AIzaSy...',
@ -96,14 +103,14 @@ const firebaseConfig = {
appId: '...',
};
```
6. **Copy only the part with the curly braces `{ ... }`**.
For The Database:
just copy the link for your database.
### Step 5: Linking with monochrome
now all you need to do is add your configurations in monochrome.
1. Go to settings in monochrome.
@ -112,4 +119,4 @@ now all you need to do is add your configurations in monochrome.
4. in the authentication config input window, input the JSON object you got from firebase.
5. Click "Save"
Thats it! you now have setup a custom authentication system and database system. do note, on every device you wanna use your custom database on, you will have to repeat step 5 on the given device.
Thats it! you now have setup a custom authentication system and database system. do note, on every device you wanna use your custom database on, you will have to repeat step 5 on the given device.