Merge branch 'main' of github.com:SamidyFR/monochrome
This commit is contained in:
commit
1658684197
4 changed files with 301 additions and 108 deletions
238
index.html
238
index.html
|
|
@ -308,22 +308,45 @@
|
||||||
<div id="custom-db-modal" class="modal">
|
<div id="custom-db-modal" class="modal">
|
||||||
<div class="modal-overlay"></div>
|
<div class="modal-overlay"></div>
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;">
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem">
|
||||||
<h3 style="margin: 0;">Custom Database/Auth</h3>
|
<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>
|
<button
|
||||||
|
id="custom-db-reset"
|
||||||
|
class="btn-secondary danger"
|
||||||
|
style="padding: 0.4rem 0.8rem; font-size: 0.8rem"
|
||||||
|
>
|
||||||
|
Reset to Defaults
|
||||||
|
</button>
|
||||||
</div>
|
</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.
|
Configure custom PocketBase and Firebase instances. Leave empty to use defaults.
|
||||||
<br>
|
<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>.
|
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>
|
</p>
|
||||||
<div style="margin-bottom: 1rem;">
|
<div style="margin-bottom: 1rem">
|
||||||
<label style="display: block; margin-bottom: 0.5rem; font-size: 0.9rem;">PocketBase URL</label>
|
<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">
|
<input
|
||||||
|
type="url"
|
||||||
|
id="custom-pb-url"
|
||||||
|
class="template-input"
|
||||||
|
placeholder="https://monodb.samidy.com"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div style="margin-bottom: 1rem;">
|
<div style="margin-bottom: 1rem">
|
||||||
<label style="display: block; margin-bottom: 0.5rem; font-size: 0.9rem;">Firebase Configuration (JSON)</label>
|
<label style="display: block; margin-bottom: 0.5rem; font-size: 0.9rem"
|
||||||
<textarea id="custom-firebase-config" class="template-input" style="height: 150px; font-family: monospace; font-size: 0.8rem; resize: vertical;" placeholder='{"apiKey": "...", ...}'></textarea>
|
>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>
|
||||||
<div class="modal-actions">
|
<div class="modal-actions">
|
||||||
<button id="custom-db-cancel" class="btn-secondary">Cancel</button>
|
<button id="custom-db-cancel" class="btn-secondary">Cancel</button>
|
||||||
|
|
@ -617,44 +640,147 @@
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div id="page-home" class="page">
|
<div id="page-home" class="page">
|
||||||
<div id="home-welcome" style="display: none; text-align: center; padding: 4rem 2rem;">
|
<div id="home-welcome" style="display: none; text-align: center; padding: 4rem 2rem">
|
||||||
<h2 style="margin-bottom: 1rem;">Welcome to Monochrome</h2>
|
<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>
|
<p style="color: var(--muted-foreground)">
|
||||||
|
You haven't listened to anything yet. Search for your favorite songs to get started!
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="home-content" style="display: none;">
|
<div id="home-content" style="display: none">
|
||||||
<section class="content-section">
|
<section class="content-section">
|
||||||
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 1rem;">
|
<div
|
||||||
<h2 class="section-title" style="margin-bottom: 0;">Recommended Songs</h2>
|
style="
|
||||||
<button class="btn-secondary" id="refresh-songs-btn" title="Refresh" style="padding: 4px 8px;">
|
display: flex;
|
||||||
<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>
|
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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="track-list" id="home-recommended-songs"></div>
|
<div class="track-list" id="home-recommended-songs"></div>
|
||||||
</section>
|
</section>
|
||||||
<section class="content-section">
|
<section class="content-section">
|
||||||
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 1rem;">
|
<div
|
||||||
<h2 class="section-title" style="margin-bottom: 0;">Recommended Albums</h2>
|
style="
|
||||||
<button class="btn-secondary" id="refresh-albums-btn" title="Refresh" style="padding: 4px 8px;">
|
display: flex;
|
||||||
<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>
|
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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-grid" id="home-recommended-albums"></div>
|
<div class="card-grid" id="home-recommended-albums"></div>
|
||||||
</section>
|
</section>
|
||||||
<section class="content-section">
|
<section class="content-section">
|
||||||
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 1rem;">
|
<div
|
||||||
<h2 class="section-title" style="margin-bottom: 0;">Recommended Artists</h2>
|
style="
|
||||||
<button class="btn-secondary" id="refresh-artists-btn" title="Refresh" style="padding: 4px 8px;">
|
display: flex;
|
||||||
<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>
|
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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-grid" id="home-recommended-artists"></div>
|
<div class="card-grid" id="home-recommended-artists"></div>
|
||||||
</section>
|
</section>
|
||||||
<section class="content-section">
|
<section class="content-section">
|
||||||
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 1rem;">
|
<div
|
||||||
<h2 class="section-title" style="margin-bottom: 0;">Jump Back In</h2>
|
style="
|
||||||
<button class="btn-secondary" id="clear-recent-btn" title="Clear History" style="padding: 4px 8px;">
|
display: flex;
|
||||||
<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>
|
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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-grid" id="home-recent-mixed"></div>
|
<div class="card-grid" id="home-recent-mixed"></div>
|
||||||
|
|
@ -705,9 +831,38 @@
|
||||||
<button class="search-tab" data-tab="local">Local Files</button>
|
<button class="search-tab" data-tab="local">Local Files</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="search-tab-content active" id="library-tab-tracks">
|
<div class="search-tab-content active" id="library-tab-tracks">
|
||||||
<div style="display: flex; justify-content: flex-start; margin-bottom: 0.5rem;">
|
<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">
|
<button
|
||||||
<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>
|
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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="track-list" id="library-tracks-container"></div>
|
<div class="track-list" id="library-tracks-container"></div>
|
||||||
|
|
@ -1602,16 +1757,21 @@
|
||||||
sensitive. <br />
|
sensitive. <br />
|
||||||
</p>
|
</p>
|
||||||
<p style="padding-top: 50px; text-align: center; color: #8b8b93">
|
<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>
|
</p>
|
||||||
<div style="
|
<div
|
||||||
|
style="
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 50px;
|
gap: 50px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding-top: 25px;
|
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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
18
js/events.js
18
js/events.js
|
|
@ -1,5 +1,15 @@
|
||||||
//js/events.js
|
//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 { lastFMStorage, waveformSettings } from './storage.js';
|
||||||
import { showNotification, downloadTrackWithMetadata } from './downloads.js';
|
import { showNotification, downloadTrackWithMetadata } from './downloads.js';
|
||||||
import { downloadQualitySettings } from './storage.js';
|
import { downloadQualitySettings } from './storage.js';
|
||||||
|
|
@ -705,9 +715,9 @@ export async function handleTrackAction(
|
||||||
const handleOptionClick = async (e) => {
|
const handleOptionClick = async (e) => {
|
||||||
const removeBtn = e.target.closest('.remove-from-playlist-btn-modal');
|
const removeBtn = e.target.closest('.remove-from-playlist-btn-modal');
|
||||||
const option = e.target.closest('.modal-option');
|
const option = e.target.closest('.modal-option');
|
||||||
|
|
||||||
if (!option) return;
|
if (!option) return;
|
||||||
|
|
||||||
const playlistId = option.dataset.id;
|
const playlistId = option.dataset.id;
|
||||||
|
|
||||||
if (removeBtn) {
|
if (removeBtn) {
|
||||||
|
|
@ -719,7 +729,7 @@ export async function handleTrackAction(
|
||||||
await renderModal();
|
await renderModal();
|
||||||
} else {
|
} else {
|
||||||
if (option.classList.contains('already-contains')) return;
|
if (option.classList.contains('already-contains')) return;
|
||||||
|
|
||||||
await db.addTrackToPlaylist(playlistId, item);
|
await db.addTrackToPlaylist(playlistId, item);
|
||||||
const updatedPlaylist = await db.getPlaylist(playlistId);
|
const updatedPlaylist = await db.getPlaylist(playlistId);
|
||||||
syncManager.syncUserPlaylist(updatedPlaylist, 'update');
|
syncManager.syncUserPlaylist(updatedPlaylist, 'update');
|
||||||
|
|
|
||||||
122
js/ui.js
122
js/ui.js
|
|
@ -836,14 +836,14 @@ export class UIRenderer {
|
||||||
|
|
||||||
async renderHomePage() {
|
async renderHomePage() {
|
||||||
this.showPage('home');
|
this.showPage('home');
|
||||||
|
|
||||||
const welcomeEl = document.getElementById('home-welcome');
|
const welcomeEl = document.getElementById('home-welcome');
|
||||||
const contentEl = document.getElementById('home-content');
|
const contentEl = document.getElementById('home-content');
|
||||||
|
|
||||||
const history = await db.getHistory();
|
const history = await db.getHistory();
|
||||||
const favorites = await db.getFavorites('track');
|
const favorites = await db.getFavorites('track');
|
||||||
const playlists = await db.getPlaylists(true);
|
const playlists = await db.getPlaylists(true);
|
||||||
|
|
||||||
if (history.length === 0 && favorites.length === 0 && playlists.length === 0) {
|
if (history.length === 0 && favorites.length === 0 && playlists.length === 0) {
|
||||||
if (welcomeEl) welcomeEl.style.display = 'block';
|
if (welcomeEl) welcomeEl.style.display = 'block';
|
||||||
if (contentEl) contentEl.style.display = 'none';
|
if (contentEl) contentEl.style.display = 'none';
|
||||||
|
|
@ -861,12 +861,13 @@ export class UIRenderer {
|
||||||
if (refreshSongsBtn) refreshSongsBtn.onclick = () => this.renderHomeSongs(true);
|
if (refreshSongsBtn) refreshSongsBtn.onclick = () => this.renderHomeSongs(true);
|
||||||
if (refreshAlbumsBtn) refreshAlbumsBtn.onclick = () => this.renderHomeAlbums(true);
|
if (refreshAlbumsBtn) refreshAlbumsBtn.onclick = () => this.renderHomeAlbums(true);
|
||||||
if (refreshArtistsBtn) refreshArtistsBtn.onclick = () => this.renderHomeArtists(true);
|
if (refreshArtistsBtn) refreshArtistsBtn.onclick = () => this.renderHomeArtists(true);
|
||||||
if (clearRecentBtn) clearRecentBtn.onclick = () => {
|
if (clearRecentBtn)
|
||||||
if (confirm('Clear recent activity?')) {
|
clearRecentBtn.onclick = () => {
|
||||||
recentActivityManager.clear();
|
if (confirm('Clear recent activity?')) {
|
||||||
this.renderHomeRecent();
|
recentActivityManager.clear();
|
||||||
}
|
this.renderHomeRecent();
|
||||||
};
|
}
|
||||||
|
};
|
||||||
|
|
||||||
this.renderHomeSongs();
|
this.renderHomeSongs();
|
||||||
this.renderHomeAlbums();
|
this.renderHomeAlbums();
|
||||||
|
|
@ -878,18 +879,18 @@ export class UIRenderer {
|
||||||
const history = await db.getHistory();
|
const history = await db.getHistory();
|
||||||
const favorites = await db.getFavorites('track');
|
const favorites = await db.getFavorites('track');
|
||||||
const playlists = await db.getPlaylists(true);
|
const playlists = await db.getPlaylists(true);
|
||||||
const playlistTracks = playlists.flatMap(p => p.tracks || []);
|
const playlistTracks = playlists.flatMap((p) => p.tracks || []);
|
||||||
|
|
||||||
// Prioritize: Playlists > Favorites > History
|
// Prioritize: Playlists > Favorites > History
|
||||||
// Take random samples from each to form seeds
|
// Take random samples from each to form seeds
|
||||||
const shuffle = (arr) => [...arr].sort(() => Math.random() - 0.5);
|
const shuffle = (arr) => [...arr].sort(() => Math.random() - 0.5);
|
||||||
|
|
||||||
const seeds = [
|
const seeds = [
|
||||||
...shuffle(playlistTracks).slice(0, 20),
|
...shuffle(playlistTracks).slice(0, 20),
|
||||||
...shuffle(favorites).slice(0, 20),
|
...shuffle(favorites).slice(0, 20),
|
||||||
...shuffle(history).slice(0, 10)
|
...shuffle(history).slice(0, 10),
|
||||||
];
|
];
|
||||||
|
|
||||||
return shuffle(seeds);
|
return shuffle(seeds);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -903,7 +904,7 @@ export class UIRenderer {
|
||||||
const seeds = await this.getSeeds();
|
const seeds = await this.getSeeds();
|
||||||
const trackSeeds = seeds.slice(0, 5);
|
const trackSeeds = seeds.slice(0, 5);
|
||||||
const recommendedTracks = await this.api.getRecommendedTracksForPlaylist(trackSeeds, 20);
|
const recommendedTracks = await this.api.getRecommendedTracksForPlaylist(trackSeeds, 20);
|
||||||
|
|
||||||
const filteredTracks = await this.filterUserContent(recommendedTracks, 'track');
|
const filteredTracks = await this.filterUserContent(recommendedTracks, 'track');
|
||||||
|
|
||||||
if (filteredTracks.length > 0) {
|
if (filteredTracks.length > 0) {
|
||||||
|
|
@ -926,14 +927,17 @@ export class UIRenderer {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const seeds = await this.getSeeds();
|
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) {
|
if (albumSeed) {
|
||||||
const similarAlbums = await this.api.getSimilarAlbums(albumSeed.album.id);
|
const similarAlbums = await this.api.getSimilarAlbums(albumSeed.album.id);
|
||||||
const filteredAlbums = await this.filterUserContent(similarAlbums, 'album');
|
const filteredAlbums = await this.filterUserContent(similarAlbums, 'album');
|
||||||
|
|
||||||
if (filteredAlbums.length > 0) {
|
if (filteredAlbums.length > 0) {
|
||||||
albumsContainer.innerHTML = filteredAlbums.slice(0, 12).map(a => this.createAlbumCardHTML(a)).join('');
|
albumsContainer.innerHTML = filteredAlbums
|
||||||
filteredAlbums.slice(0, 12).forEach(a => {
|
.slice(0, 12)
|
||||||
|
.map((a) => this.createAlbumCardHTML(a))
|
||||||
|
.join('');
|
||||||
|
filteredAlbums.slice(0, 12).forEach((a) => {
|
||||||
const el = albumsContainer.querySelector(`[data-album-id="${a.id}"]`);
|
const el = albumsContainer.querySelector(`[data-album-id="${a.id}"]`);
|
||||||
if (el) {
|
if (el) {
|
||||||
trackDataStore.set(el, a);
|
trackDataStore.set(el, a);
|
||||||
|
|
@ -961,16 +965,19 @@ export class UIRenderer {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const seeds = await this.getSeeds();
|
const seeds = await this.getSeeds();
|
||||||
const artistSeed = seeds.find(t => (t.artist && t.artist.id) || (t.artists && t.artists.length > 0));
|
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 artistId = artistSeed ? artistSeed.artist?.id || artistSeed.artists?.[0]?.id : null;
|
||||||
|
|
||||||
if (artistId) {
|
if (artistId) {
|
||||||
const similarArtists = await this.api.getSimilarArtists(artistId);
|
const similarArtists = await this.api.getSimilarArtists(artistId);
|
||||||
const filteredArtists = await this.filterUserContent(similarArtists, 'artist');
|
const filteredArtists = await this.filterUserContent(similarArtists, 'artist');
|
||||||
|
|
||||||
if (filteredArtists.length > 0) {
|
if (filteredArtists.length > 0) {
|
||||||
artistsContainer.innerHTML = filteredArtists.slice(0, 12).map(a => this.createArtistCardHTML(a)).join('');
|
artistsContainer.innerHTML = filteredArtists
|
||||||
filteredArtists.slice(0, 12).forEach(a => {
|
.slice(0, 12)
|
||||||
|
.map((a) => this.createArtistCardHTML(a))
|
||||||
|
.join('');
|
||||||
|
filteredArtists.slice(0, 12).forEach((a) => {
|
||||||
const el = artistsContainer.querySelector(`[data-artist-id="${a.id}"]`);
|
const el = artistsContainer.querySelector(`[data-artist-id="${a.id}"]`);
|
||||||
if (el) {
|
if (el) {
|
||||||
trackDataStore.set(el, a);
|
trackDataStore.set(el, a);
|
||||||
|
|
@ -981,7 +988,9 @@ export class UIRenderer {
|
||||||
artistsContainer.innerHTML = createPlaceholder('No artist recommendations found.');
|
artistsContainer.innerHTML = createPlaceholder('No artist recommendations found.');
|
||||||
}
|
}
|
||||||
} else {
|
} 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) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
|
|
@ -995,36 +1004,43 @@ export class UIRenderer {
|
||||||
if (recentContainer) {
|
if (recentContainer) {
|
||||||
const recents = recentActivityManager.getRecents();
|
const recents = recentActivityManager.getRecents();
|
||||||
const items = [];
|
const items = [];
|
||||||
|
|
||||||
if (recents.albums) items.push(...recents.albums.slice(0, 4).map(i => ({...i, _kind: 'album'})));
|
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.playlists)
|
||||||
if (recents.mixes) items.push(...recents.mixes.slice(0, 4).map(i => ({...i, _kind: 'mix'})));
|
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);
|
items.sort(() => Math.random() - 0.5);
|
||||||
const displayItems = items.slice(0, 6);
|
const displayItems = items.slice(0, 6);
|
||||||
|
|
||||||
if (displayItems.length > 0) {
|
if (displayItems.length > 0) {
|
||||||
recentContainer.innerHTML = displayItems.map(item => {
|
recentContainer.innerHTML = displayItems
|
||||||
if (item._kind === 'album') return this.createAlbumCardHTML(item);
|
.map((item) => {
|
||||||
if (item._kind === 'playlist') {
|
if (item._kind === 'album') return this.createAlbumCardHTML(item);
|
||||||
if (item.isUserPlaylist) return this.createUserPlaylistCardHTML(item);
|
if (item._kind === 'playlist') {
|
||||||
return this.createPlaylistCardHTML(item);
|
if (item.isUserPlaylist) return this.createUserPlaylistCardHTML(item);
|
||||||
}
|
return this.createPlaylistCardHTML(item);
|
||||||
if (item._kind === 'mix') return this.createMixCardHTML(item);
|
}
|
||||||
return '';
|
if (item._kind === 'mix') return this.createMixCardHTML(item);
|
||||||
}).join('');
|
return '';
|
||||||
|
})
|
||||||
|
.join('');
|
||||||
|
|
||||||
displayItems.forEach(item => {
|
displayItems.forEach((item) => {
|
||||||
let selector = '';
|
let selector = '';
|
||||||
if (item._kind === 'album') selector = `[data-album-id="${item.id}"]`;
|
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}"]`;
|
else if (item._kind === 'mix') selector = `[data-mix-id="${item.id}"]`;
|
||||||
|
|
||||||
const el = recentContainer.querySelector(selector);
|
const el = recentContainer.querySelector(selector);
|
||||||
if (el) {
|
if (el) {
|
||||||
trackDataStore.set(el, item);
|
trackDataStore.set(el, item);
|
||||||
if (item._kind === 'album') this.updateLikeState(el, 'album', item.id);
|
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);
|
if (item._kind === 'mix') this.updateLikeState(el, 'mix', item.id);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -1038,19 +1054,19 @@ export class UIRenderer {
|
||||||
if (!items || items.length === 0) return [];
|
if (!items || items.length === 0) return [];
|
||||||
|
|
||||||
const favorites = await db.getFavorites(type);
|
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 likedTracks = await db.getFavorites('track');
|
||||||
const playlists = await db.getPlaylists(true);
|
const playlists = await db.getPlaylists(true);
|
||||||
|
|
||||||
const userTracksMap = new Map();
|
const userTracksMap = new Map();
|
||||||
likedTracks.forEach(t => userTracksMap.set(t.id, t));
|
likedTracks.forEach((t) => userTracksMap.set(t.id, t));
|
||||||
playlists.forEach(p => {
|
playlists.forEach((p) => {
|
||||||
if (p.tracks) p.tracks.forEach(t => userTracksMap.set(t.id, t));
|
if (p.tracks) p.tracks.forEach((t) => userTracksMap.set(t.id, t));
|
||||||
});
|
});
|
||||||
|
|
||||||
if (type === 'track') {
|
if (type === 'track') {
|
||||||
return items.filter(item => !userTracksMap.has(item.id));
|
return items.filter((item) => !userTracksMap.has(item.id));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === 'album') {
|
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;
|
if (favoriteIds.has(item.id)) return false;
|
||||||
|
|
||||||
const userCount = albumTrackCounts.get(item.id) || 0;
|
const userCount = albumTrackCounts.get(item.id) || 0;
|
||||||
const total = item.numberOfTracks;
|
const total = item.numberOfTracks;
|
||||||
|
|
||||||
if (total && total > 0) {
|
if (total && total > 0) {
|
||||||
if ((userCount / total) > 0.5) return false;
|
if (userCount / total > 0.5) return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return items.filter(item => !favoriteIds.has(item.id));
|
return items.filter((item) => !favoriteIds.has(item.id));
|
||||||
}
|
}
|
||||||
|
|
||||||
async renderSearchPage(query) {
|
async renderSearchPage(query) {
|
||||||
|
|
|
||||||
|
|
@ -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 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.**
|
**This Guide Assumes You're Doing everything On Your Local Machine. This is still fully possible on a VPS Though.**
|
||||||
|
|
||||||
### Required:
|
### Required:
|
||||||
|
|
||||||
- A Computer (this computer will be the one hosting the database)
|
- A Computer (this computer will be the one hosting the database)
|
||||||
- Firebase Account (Only Used For Authentication)
|
- 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)
|
- [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/))
|
- Domain (you can get one for free at [DigitalPlat](https://domain.digitalplat.org/))
|
||||||
|
|
||||||
|
|
||||||
### Step 1: Setup Firebase Authentication
|
### 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**.
|
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**.
|
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**.
|
4. Set your project support email and click **Save**.
|
||||||
|
|
||||||
### Step 1.1: Authorize The Domain
|
### 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.
|
1. In the **Authentication** section, go to the **Settings** tab.
|
||||||
2. Click **Authorized domains** in the left sub-menu.
|
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._
|
- _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
|
### Step 2: PocketBase Setup
|
||||||
|
|
||||||
1. download [PocketBase](https://pocketbase.io) and follow their setup guide.
|
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.
|
2. make 2 collections: `DB_users` and `public_playlists`. do NOT use the normal "users" collection.
|
||||||
3. Add these fields to `DB_users`:
|
3. Add these fields to `DB_users`:
|
||||||
|
|
||||||
- name: `firebase_id` type: `Plain Text`
|
- name: `firebase_id` type: `Plain Text`
|
||||||
- name: `lastUpdated` type: `Number`
|
- name: `lastUpdated` type: `Number`
|
||||||
- name: `history` type: `JSON`
|
- name: `history` type: `JSON`
|
||||||
- name: `library` type: `JSON`
|
- name: `library` type: `JSON`
|
||||||
- name: `user_playlists` type: `JSON`
|
- name: `user_playlists` type: `JSON`
|
||||||
- name: `deleted_playlists` type: `JSON`
|
- name: `deleted_playlists` type: `JSON`
|
||||||
|
|
||||||
4. Add these fields to `public_playlists`:
|
4. Add these fields to `public_playlists`:
|
||||||
|
|
||||||
- name: `firebase_id` type: `Plain Text`
|
- name: `firebase_id` type: `Plain Text`
|
||||||
- name: `addedAt` type: `Number`
|
- name: `addedAt` type: `Number`
|
||||||
- name: `numberOfTracks` 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: `uuid` type: `Plain Text`
|
||||||
- name: `tracks` type: `JSON`
|
- name: `tracks` type: `JSON`
|
||||||
- name: `image` type: `URL`
|
- name: `image` type: `URL`
|
||||||
|
|
||||||
5. edit the `API Rules` for both `DB_users` and `public_playlists` to these:
|
5. edit the `API Rules` for both `DB_users` and `public_playlists` to these:
|
||||||
|
|
||||||
#### `DB_users`
|
#### `DB_users`
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
#### `public_playlists`
|
#### `public_playlists`
|
||||||

|
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
Now that you have setup collections, rules and fields, we can now work on putting them out on the internet.
|
Now that you have setup collections, rules and fields, we can now work on putting them out on the internet.
|
||||||
|
|
||||||
|
|
||||||
### Step 3: Cloudflared
|
### 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.
|
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).
|
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.
|
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.
|
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`).
|
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
|
### Step 4: Getting Configurations
|
||||||
|
|
||||||
You are almost done, now you just need to get configurations so you can add them to monochrome.
|
You are almost done, now you just need to get configurations so you can add them to monochrome.
|
||||||
|
|
||||||
first, get your authentication config:
|
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 (`</>`)**.
|
3. In the **General** tab, scroll down to "Your apps" and click the **Web icon (`</>`)**.
|
||||||
4. Register the app (e.g., "Monochrome Auth").
|
4. Register the app (e.g., "Monochrome Auth").
|
||||||
5. You will see a `firebaseConfig` object. It looks like this:
|
5. You will see a `firebaseConfig` object. It looks like this:
|
||||||
|
|
||||||
```
|
```
|
||||||
const firebaseConfig = {
|
const firebaseConfig = {
|
||||||
apiKey: 'AIzaSy...',
|
apiKey: 'AIzaSy...',
|
||||||
|
|
@ -96,14 +103,14 @@ const firebaseConfig = {
|
||||||
appId: '...',
|
appId: '...',
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
6. **Copy only the part with the curly braces `{ ... }`**.
|
6. **Copy only the part with the curly braces `{ ... }`**.
|
||||||
|
|
||||||
For The Database:
|
For The Database:
|
||||||
just copy the link for your database.
|
just copy the link for your database.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### Step 5: Linking with monochrome
|
### Step 5: Linking with monochrome
|
||||||
|
|
||||||
now all you need to do is add your configurations in monochrome.
|
now all you need to do is add your configurations in monochrome.
|
||||||
|
|
||||||
1. Go to settings 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.
|
4. in the authentication config input window, input the JSON object you got from firebase.
|
||||||
5. Click "Save"
|
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.
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue