Merge branch 'main' of github.com:monochrome-music/monochrome

This commit is contained in:
Samidy 2026-03-06 02:14:47 +03:00
commit e013ad0de5
9 changed files with 166 additions and 21 deletions

66
.devcontainer/Dockerfile Normal file
View 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"]

View 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"
}
}

View file

@ -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",

View file

@ -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>

View file

@ -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';
}

View file

@ -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}`)}&gtin=&region=&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) {

View file

@ -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

View file

@ -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;

View file

@ -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