Merge branch 'main' of github.com:monochrome-music/monochrome
This commit is contained in:
commit
e013ad0de5
9 changed files with 166 additions and 21 deletions
66
.devcontainer/Dockerfile
Normal file
66
.devcontainer/Dockerfile
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
# ------------------------------------------------------------
|
||||
# Base Image
|
||||
# ------------------------------------------------------------
|
||||
FROM debian:unstable-slim
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
ENV LANG=C.UTF-8
|
||||
ENV LC_ALL=C.UTF-8
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# System Dependencies
|
||||
# ------------------------------------------------------------
|
||||
RUN apt-get update && apt-get upgrade -y && \
|
||||
apt-get install -y --no-install-recommends \
|
||||
curl \
|
||||
ca-certificates \
|
||||
git \
|
||||
build-essential \
|
||||
sudo \
|
||||
fish \
|
||||
unzip \
|
||||
xz-utils \
|
||||
libatomic1 \
|
||||
libc6 \
|
||||
wget && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# Create Non-Root User
|
||||
# ------------------------------------------------------------
|
||||
ARG USERNAME=devuser
|
||||
ARG UID=1000
|
||||
ARG GID=1000
|
||||
|
||||
RUN groupadd --gid ${GID} ${USERNAME} && \
|
||||
useradd --uid ${UID} --gid ${GID} -m -s /usr/bin/fish ${USERNAME} && \
|
||||
echo "${USERNAME} ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers
|
||||
|
||||
USER ${USERNAME}
|
||||
WORKDIR /home/${USERNAME}
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# Install Bun (Non-Root)
|
||||
# ------------------------------------------------------------
|
||||
ENV BUN_INSTALL=/home/${USERNAME}/.bun
|
||||
ENV PATH="${BUN_INSTALL}/bin:${PATH}"
|
||||
|
||||
RUN curl -fsSL https://bun.sh/install | bash
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# Install OpenCode (Proper PATH Handling)
|
||||
# ------------------------------------------------------------
|
||||
RUN curl -fsSL https://opencode.ai/install -o opencode-install && \
|
||||
chmod +x opencode-install && \
|
||||
./opencode-install --yes && \
|
||||
rm opencode-install
|
||||
|
||||
# Add OpenCode to PATH permanently
|
||||
ENV PATH="/home/${USERNAME}/.opencode/bin:${PATH}"
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# Ensure fish is Default Shell
|
||||
# ------------------------------------------------------------
|
||||
ENV SHELL=/usr/bin/fish
|
||||
|
||||
CMD ["fish"]
|
||||
22
.devcontainer/devcontainer.json
Normal file
22
.devcontainer/devcontainer.json
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"name": "debian-bun-fish-devcontainer",
|
||||
"build": {
|
||||
"dockerfile": "Dockerfile"
|
||||
},
|
||||
|
||||
"remoteUser": "devuser",
|
||||
|
||||
"features": {},
|
||||
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"extensions": ["dbaeumer.vscode-eslint", "esbenp.prettier-vscode"]
|
||||
}
|
||||
},
|
||||
|
||||
"postCreateCommand": "bun --version && code --version",
|
||||
|
||||
"remoteEnv": {
|
||||
"SHELL": "/usr/bin/fish"
|
||||
}
|
||||
}
|
||||
2
bun.lock
2
bun.lock
|
|
@ -19,7 +19,7 @@
|
|||
"@neutralinojs/neu": "^11.7.0",
|
||||
"eslint": "^9.39.3",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"globals": "^17.3.0",
|
||||
"globals": "^17.4.0",
|
||||
"htmlhint": "^1.9.1",
|
||||
"prettier": "^3.8.1",
|
||||
"stylelint": "^16.26.1",
|
||||
|
|
|
|||
24
index.html
24
index.html
|
|
@ -87,6 +87,7 @@
|
|||
<li data-action="go-to-album" data-type-filter="track">Go to album</li>
|
||||
<li data-action="copy-link">Copy link</li>
|
||||
<li data-action="open-in-new-tab">Open in new tab</li>
|
||||
<li data-action="open-in-harmony" data-type-filter="album">Open in Harmony</li>
|
||||
<li data-action="track-info" data-type-filter="track">Track info</li>
|
||||
<li data-action="open-original-url" data-type-filter="track">Open original URL</li>
|
||||
<li data-action="download">Download</li>
|
||||
|
|
@ -2710,6 +2711,29 @@
|
|||
</svg>
|
||||
<span>Save</span>
|
||||
</button>
|
||||
<button
|
||||
id="album-menu-btn"
|
||||
class="btn-secondary"
|
||||
data-action="card-menu"
|
||||
data-type="album"
|
||||
title="More options"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<circle cx="12" cy="12" r="1"></circle>
|
||||
<circle cx="12" cy="5" r="1"></circle>
|
||||
<circle cx="12" cy="19" r="1"></circle>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
|
|
|||
21
js/app.js
21
js/app.js
|
|
@ -2323,14 +2323,19 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||
const searchForm = document.getElementById('search-form');
|
||||
const searchInput = document.getElementById('search-input');
|
||||
|
||||
// Setup clear button for search bar
|
||||
ui.setupSearchClearButton(searchInput);
|
||||
|
||||
const performSearch = debounce((query) => {
|
||||
const performSearch = (query) => {
|
||||
if (query) {
|
||||
navigate(`/search/${encodeURIComponent(query)}`);
|
||||
}
|
||||
}, 0);
|
||||
};
|
||||
|
||||
const debouncedSearch = debounce((query) => {
|
||||
if (query && query === searchInput.value.trim()) {
|
||||
performSearch(query);
|
||||
}
|
||||
}, 3000);
|
||||
|
||||
const handleExternalLink = (query) => {
|
||||
const isExternalLink =
|
||||
|
|
@ -2343,9 +2348,7 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||
try {
|
||||
const urlObj = new URL(url);
|
||||
let path = urlObj.pathname;
|
||||
// Remove trailing slashes and get just endpoint/id
|
||||
path = path.replace(/\/+$/, '');
|
||||
// Get just the first two segments (e.g., /album/382839956)
|
||||
const segments = path.split('/').filter((s) => s);
|
||||
if (segments.length >= 2) {
|
||||
path = '/' + segments[0] + '/' + segments[1];
|
||||
|
|
@ -2363,9 +2366,11 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||
const query = e.target.value.trim();
|
||||
if (!query) return;
|
||||
|
||||
if (!handleExternalLink(query)) {
|
||||
performSearch(query);
|
||||
if (handleExternalLink(query)) {
|
||||
return;
|
||||
}
|
||||
|
||||
debouncedSearch(query);
|
||||
});
|
||||
|
||||
searchInput.addEventListener('change', (e) => {
|
||||
|
|
@ -2397,7 +2402,7 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||
|
||||
if (!handleExternalLink(query)) {
|
||||
ui.addToSearchHistory(query);
|
||||
navigate(`/search/${encodeURIComponent(query)}`);
|
||||
performSearch(query);
|
||||
const historyEl = document.getElementById('search-history');
|
||||
if (historyEl) historyEl.style.display = 'none';
|
||||
}
|
||||
|
|
|
|||
18
js/events.js
18
js/events.js
|
|
@ -1200,6 +1200,10 @@ export async function handleTrackAction(
|
|||
|
||||
trackOpenInNewTab(type, item.id || item.uuid);
|
||||
window.open(url, '_blank');
|
||||
} else if (action === 'open-in-harmony') {
|
||||
const albumId = item.id;
|
||||
const harmonyUrl = `https://harmony.pulsewidth.org.uk/release?url=${encodeURIComponent(`https://tidal.com/album/${albumId}`)}>in=®ion=&musicbrainz=&deezer=&itunes=&spotify=&tidal=&beatport=`;
|
||||
window.open(harmonyUrl, '_blank');
|
||||
} else if (action === 'track-info') {
|
||||
// Show detailed track info modal
|
||||
const isTracker = item.isTracker;
|
||||
|
|
@ -1428,9 +1432,11 @@ export async function handleTrackAction(
|
|||
async function updateContextMenuLikeState(contextMenu, contextTrack) {
|
||||
if (!contextMenu || !contextTrack) return;
|
||||
|
||||
const type = contextMenu._contextType || 'track';
|
||||
|
||||
const likeItem = contextMenu.querySelector('li[data-action="toggle-like"]');
|
||||
if (likeItem) {
|
||||
const isLiked = await db.isFavorite('track', contextTrack.id);
|
||||
const isLiked = await db.isFavorite(type, contextTrack.id);
|
||||
likeItem.textContent = isLiked ? 'Unlike' : 'Like';
|
||||
}
|
||||
|
||||
|
|
@ -1455,7 +1461,6 @@ async function updateContextMenuLikeState(contextMenu, contextTrack) {
|
|||
|
||||
// Update block/unblock labels
|
||||
const { contentBlockingSettings } = await import('./storage.js');
|
||||
const type = contextMenu._contextType || 'track';
|
||||
|
||||
const blockTrackItem = contextMenu.querySelector('li[data-action="block-track"]');
|
||||
if (blockTrackItem) {
|
||||
|
|
@ -1572,7 +1577,7 @@ export function initializeTrackInteractions(player, api, mainContent, contextMen
|
|||
return;
|
||||
}
|
||||
|
||||
const cardMenuBtn = e.target.closest('.card-menu-btn');
|
||||
const cardMenuBtn = e.target.closest('.card-menu-btn, #album-menu-btn');
|
||||
if (cardMenuBtn) {
|
||||
e.stopPropagation();
|
||||
const card = cardMenuBtn.closest('.card');
|
||||
|
|
@ -1581,9 +1586,14 @@ export function initializeTrackInteractions(player, api, mainContent, contextMen
|
|||
|
||||
let item = card ? trackDataStore.get(card) : null;
|
||||
|
||||
if (!item) {
|
||||
// Check if item is stored on the button itself (e.g., album page header menu)
|
||||
item = trackDataStore.get(cardMenuBtn);
|
||||
}
|
||||
|
||||
if (!item) {
|
||||
// Fallback: create a shell item
|
||||
item = { id, uuid: id, title: card.querySelector('.card-title')?.textContent || 'Item' };
|
||||
item = { id, uuid: id, title: card?.querySelector('.card-title')?.textContent || 'Item' };
|
||||
}
|
||||
|
||||
if (contextMenu._originalHTML) {
|
||||
|
|
|
|||
19
js/ui.js
19
js/ui.js
|
|
@ -31,6 +31,7 @@ import {
|
|||
homePageSettings,
|
||||
fontSettings,
|
||||
contentBlockingSettings,
|
||||
settingsUiState,
|
||||
} from './storage.js';
|
||||
import { db } from './db.js';
|
||||
import { getVibrantColorFromImage } from './vibrant-color.js';
|
||||
|
|
@ -1448,6 +1449,17 @@ export class UIRenderer {
|
|||
|
||||
if (pageId === 'settings') {
|
||||
this.renderApiSettings();
|
||||
const savedTabName = settingsUiState.getActiveTab();
|
||||
const savedTab = document.querySelector(`.settings-tab[data-tab="${savedTabName}"]`);
|
||||
if (savedTab) {
|
||||
document.querySelectorAll('.settings-tab').forEach((t) => t.classList.remove('active'));
|
||||
document.querySelectorAll('.settings-tab-content').forEach((c) => c.classList.remove('active'));
|
||||
savedTab.classList.add('active');
|
||||
document.getElementById(`settings-tab-${savedTabName}`)?.classList.add('active');
|
||||
}
|
||||
} else {
|
||||
document.querySelectorAll('.settings-tab').forEach((t) => t.classList.remove('active'));
|
||||
document.querySelectorAll('.settings-tab-content').forEach((c) => c.classList.remove('active'));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -2461,6 +2473,13 @@ export class UIRenderer {
|
|||
albumLikeBtn.classList.toggle('active', isLiked);
|
||||
}
|
||||
|
||||
// Store album data for menu button
|
||||
const albumMenuBtn = document.getElementById('album-menu-btn');
|
||||
if (albumMenuBtn) {
|
||||
albumMenuBtn.dataset.id = album.id;
|
||||
trackDataStore.set(albumMenuBtn, album);
|
||||
}
|
||||
|
||||
document.title = `${album.title} - ${album.artist.name}`;
|
||||
|
||||
// "More from Artist" and Related Sections
|
||||
|
|
|
|||
|
|
@ -2401,6 +2401,13 @@ input[type='search']::-webkit-search-cancel-button {
|
|||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.detail-header-actions .card-menu-btn,
|
||||
.detail-header-actions #album-menu-btn {
|
||||
position: static !important;
|
||||
opacity: 1 !important;
|
||||
transform: none !important;
|
||||
}
|
||||
|
||||
.detail-header-actions .btn-primary span,
|
||||
.detail-header-actions .btn-secondary span {
|
||||
display: none;
|
||||
|
|
|
|||
8
todo.md
8
todo.md
|
|
@ -1,8 +0,0 @@
|
|||
# Feature Requests
|
||||
|
||||
Sorted by ease of implementation (easiest to hardest):
|
||||
|
||||
- [ ] effects like reverb, delay, and bitcrushing
|
||||
- [ ] Customizable EQ: Allow users to change the number of EQ bands and their range (-30 to 30), with a drag-to-adjust interface similar to FL Studio's velocity editor
|
||||
|
||||
[ ] SoundCloud support: Integrate SoundCloud through SoundCloak
|
||||
Loading…
Reference in a new issue