Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions css/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -1931,6 +1931,54 @@ body.bes-drawer-expanded #pgBd {
flex: 0 0 auto;
}

/* Previous/next album controls */
.bes-album-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
margin-bottom: 12px;
padding: 0 16px 0 20px;
}

.bes-album-toolbar .bes-album-buy {
margin-bottom: 0;
padding: 0;
}

.bes-album-nav {
display: flex;
gap: 4px;
}

.bes-album-nav button {
display: flex;
align-items: center;
gap: 4px;
padding: 4px 8px;
font-size: 13px;
font-weight: 500;
color: var(--bes-text-strong);
background: none;
border: 1px solid transparent;
border-radius: 4px;
cursor: pointer;
}

.bes-album-nav button:hover:not(:disabled) {
background: var(--bes-surface-1);
color: var(--bes-text-max);
}

.bes-album-nav button:disabled {
opacity: 0.3;
cursor: default;
}

.bes-album-nav.bes-loading {
cursor: progress;
}

/* Play status icons in track list */
.bes-player-drawer .play_status {
background: var(--bes-surface-0);
Expand Down
39 changes: 39 additions & 0 deletions src/components/player/albumNav.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { element } from '../dom';

export interface AlbumNavState {
hasPrevious: boolean;
hasNext: boolean;
loading: boolean;
}

const chevron = (points: string): string =>
`<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="${points}"></polyline></svg>`;

export function buildAlbumNav(onPrevious: () => void, onNext: () => void): HTMLElement {
const previous = element('button', {
className: 'bes-album-nav-prev',
html: `${chevron('15 18 9 12 15 6')}<span>Previous album</span>`,
attributes: { type: 'button', title: 'Previous album' }
});
const next = element('button', {
className: 'bes-album-nav-next',
html: `<span>Next album</span>${chevron('9 18 15 12 9 6')}`,
attributes: { type: 'button', title: 'Next album' }
});

previous.addEventListener('click', onPrevious);
next.addEventListener('click', onNext);

return element('div', { className: 'bes-album-nav', children: [previous, next] });
}

export function updateAlbumNav(nav: HTMLElement | null, { hasPrevious, hasNext, loading }: AlbumNavState): void {
if (!nav) return;

const previous = nav.querySelector<HTMLButtonElement>('.bes-album-nav-prev');
const next = nav.querySelector<HTMLButtonElement>('.bes-album-nav-next');

if (previous) previous.disabled = loading || !hasPrevious;
if (next) next.disabled = loading || !hasNext;
nav.classList.toggle('bes-loading', loading);
}
2 changes: 1 addition & 1 deletion src/components/player/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export { createPlayerDrawer } from './drawer';
export { loadAlbumIntoDrawer, loadNextAlbum, loadPreviousAlbum } from './loader';
export { loadAlbumIntoDrawer, loadNextAlbum, loadPreviousAlbum, getLoadedAlbumId } from './loader';
84 changes: 75 additions & 9 deletions src/components/player/loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,22 @@ import { buildDrawerPlayer, buildTrackTable, buildAlbumBuyButton } from './build
import { PlayerCommands, registerPlayerShortcuts } from '../../keyboardShortcuts';
import { drawOverlay, generateAudioFeatures } from '../../audioFeatures';
import { volumeIcon, mutedVolumeIcon } from './icons';
import { replaceChildren } from '../dom';
import { element, replaceChildren } from '../dom';
import { inDrawer, allInDrawer, setText, setStyle } from './query';
import { streamUrlOf, findPlayableTrackAfter, findPlayableTrackBefore } from './trackSelection';
import { applyTrackState, markRowPlayed } from './trackState';
import { buildAlbumNav, updateAlbumNav } from './albumNav';
import {
selectAlbum,
hasNextAlbum,
hasPreviousAlbum,
nextAlbum,
previousAlbum,
albumArtUrlFor
albumArtUrlFor,
canLoadMoreAlbums,
loadMoreAlbums,
revealAlbum,
showAlbumControls
} from '../../discography';

const log = new Logger();
Expand All @@ -31,6 +36,9 @@ let drawerPlayerCreated = false;
let previousVolume = 1.0;
let configPort: chrome.runtime.Port | null = null;
let waveformEnabled = true;
let requestedAlbumId: string | null = null;
let fetchCaching = false;
let albumNavLoading = false;

const ALBUM_LOAD_SETTLE_MS = 300;

Expand Down Expand Up @@ -220,6 +228,8 @@ export async function loadAlbumIntoDrawer(
port?: chrome.runtime.Port
): Promise<void> {
log.info(`Loading album ${albumId} (${albumType}) into drawer`);
requestedAlbumId = albumId;
fetchCaching = enableFetchCaching;

try {
const tralbumDetails = await getTralbumDetails(
Expand All @@ -229,6 +239,11 @@ export async function loadAlbumIntoDrawer(
createFetchFunction(enableFetchCaching)
);

if (requestedAlbumId !== albumId) {
log.debug(`Album ${albumId} arrived after the drawer moved on`);
return;
Comment on lines +242 to +244

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

test for this guard please

}

currentAlbumData = tralbumDetails;
selectAlbum(albumId);

Expand All @@ -243,14 +258,14 @@ export async function loadAlbumIntoDrawer(
if (drawerPlayerCreated) {
replaceChildren(
elements.tracklistContainer,
buildAlbumBuyButton(tralbumDetails),
albumToolbar(buildAlbumBuyButton(tralbumDetails)),
buildTrackTable(tralbumDetails)
);
} else {
const parts = buildDrawerPlayer(tralbumDetails);

mountDrawerPlayer(elements, parts);
replaceChildren(elements.tracklistContainer, parts.albumBuyButton, parts.tracklistElement);
replaceChildren(elements.tracklistContainer, albumToolbar(parts.albumBuyButton), parts.tracklistElement);
drawerPlayerCreated = true;

startPlayerOnce();
Expand All @@ -268,6 +283,50 @@ export async function loadAlbumIntoDrawer(
}
}

export function getLoadedAlbumId(): string | null {
return requestedAlbumId;
}

function hasLaterAlbum(): boolean {
return hasNextAlbum() || canLoadMoreAlbums();
}

function albumToolbar(buyButton: HTMLElement | null): HTMLElement | null {
if (!showAlbumControls()) return buyButton;

const nav = buildAlbumNav(
() => void skipAlbum(loadPreviousAlbum),
() => void skipAlbum(loadNextAlbum)
);
updateAlbumNav(nav, { hasPrevious: hasPreviousAlbum(), hasNext: hasLaterAlbum(), loading: albumNavLoading });

return element('div', { className: 'bes-album-toolbar', children: [nav, buyButton] });
}

function refreshAlbumNav(): void {
updateAlbumNav(inDrawer('.bes-album-nav'), {
hasPrevious: hasPreviousAlbum(),
hasNext: hasLaterAlbum(),
loading: albumNavLoading
});
}

async function skipAlbum(load: () => Promise<boolean>): Promise<void> {
const keepPlaying = Boolean(audioElement && !audioElement.paused);
albumNavLoading = true;
refreshAlbumNav();

try {
if ((await load()) && keepPlaying) resumePlayback();
} catch (error) {
log.error(`Failed to skip album: ${error}`);
} finally {
albumNavLoading = false;
refreshAlbumNav();
if (currentAlbumData?.tracks) updatePrevNextButtons(currentTrackIndex, currentAlbumData.tracks.length);
}
}

function currentTrack(): TralbumTrack | undefined {
return currentAlbumData?.tracks?.[currentTrackIndex];
}
Expand Down Expand Up @@ -334,7 +393,7 @@ function updatePrevNextButtons(index: number, totalTracks: number): void {
const nextButton = inDrawer('.bes-transport-next');

const hasEarlierTrackOrAlbum = index > 0 || hasPreviousAlbum();
const hasLaterTrackOrAlbum = index < totalTracks - 1 || hasNextAlbum();
const hasLaterTrackOrAlbum = index < totalTracks - 1 || hasLaterAlbum();

prevButton?.classList.toggle('bes-hidden', !hasEarlierTrackOrAlbum);
nextButton?.classList.toggle('bes-hidden', !hasLaterTrackOrAlbum);
Expand Down Expand Up @@ -366,7 +425,7 @@ interface TrackStep {
const forward: TrackStep = {
name: 'next',
nextWithinAlbum: findPlayableTrackAfter,
hasAdjacentAlbum: hasNextAlbum,
hasAdjacentAlbum: hasLaterAlbum,
loadAdjacentAlbum: loadNextAlbum,
entryTrack: tracks => findPlayableTrackAfter(tracks, -1)
};
Expand Down Expand Up @@ -395,7 +454,7 @@ async function step(direction: TrackStep, keepPlaying: boolean): Promise<void> {
}

log.info(`No playable track ${direction.name}: loading ${direction.name} album in discography`);
await direction.loadAdjacentAlbum();
if (!(await direction.loadAdjacentAlbum())) return;

setTimeout(() => {
const entry = direction.entryTrack(currentAlbumData?.tracks);
Expand Down Expand Up @@ -600,25 +659,32 @@ function adjustVolumeBy(delta: number): void {
updateMuteButton(audioElement.volume === 0);
}

export async function loadNextAlbum(enableFetchCaching: boolean = false): Promise<boolean> {
export async function loadNextAlbum(enableFetchCaching: boolean = fetchCaching): Promise<boolean> {
if (!hasNextAlbum() && canLoadMoreAlbums()) {
log.info('Reached the end of the loaded albums, loading more');
await loadMoreAlbums();
}

const item = nextAlbum();
if (!item) {
log.info('No next album available');
return false;
}

await loadAlbumIntoDrawer(item.id, item.type, enableFetchCaching);
revealAlbum(item);
return true;
}

export async function loadPreviousAlbum(enableFetchCaching: boolean = false): Promise<boolean> {
export async function loadPreviousAlbum(enableFetchCaching: boolean = fetchCaching): Promise<boolean> {
const item = previousAlbum();
if (!item) {
log.info('No previous album available');
return false;
}

await loadAlbumIntoDrawer(item.id, item.type, enableFetchCaching);
revealAlbum(item);
return true;
}

Expand Down
64 changes: 63 additions & 1 deletion src/discography.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,23 @@ export interface DiscographyItem {
element: Element;
}

export interface AlbumSource {
extract: () => DiscographyItem[];
// Pulls more items into the page (e.g. the next page of a feed); resolves true if any arrived.
loadMore?: () => Promise<boolean>;
reveal?: (item: DiscographyItem) => void;
showAlbumControls?: boolean;
}

let order: DiscographyItem[] = [];
let selectedIndex = -1;
let source: AlbumSource = {
extract: extractDiscographyOrder,
reveal: item => item.element.scrollIntoView?.({ behavior: 'smooth', block: 'center' }),
showAlbumControls: true
};
let pendingLoadMore: Promise<boolean> | null = null;
let sourceExhausted = false;

export function extractDiscographyOrder(): DiscographyItem[] {
const items: DiscographyItem[] = [];
Expand All @@ -33,8 +48,55 @@ export function extractDiscographyOrder(): DiscographyItem[] {
return items;
}

export function setAlbumSource(next: AlbumSource): void {
source = next;
sourceExhausted = false;
updateDiscographyOrder();
}

export function updateDiscographyOrder(): void {
order = extractDiscographyOrder();
const selectedId = order[selectedIndex]?.id;
const previousLength = order.length;

order = source.extract();
if (selectedId) selectedIndex = findAlbumIndexById(selectedId);
if (order.length > previousLength) sourceExhausted = false;
}

export function canLoadMoreAlbums(): boolean {
return selectedIndex !== -1 && Boolean(source.loadMore) && !sourceExhausted;
}

export function loadMoreAlbums(): Promise<boolean> {
if (!source.loadMore || sourceExhausted) return Promise.resolve(false);

pendingLoadMore ??= source
.loadMore()
.catch(error => {
log.warn(`Failed to load more albums: ${error}`);
return false;
})
.then(loaded => {
const previousLength = order.length;
updateDiscographyOrder();

const grew = order.length > previousLength;
if (!loaded && !grew) sourceExhausted = true;
return loaded || grew;
})
.finally(() => {
pendingLoadMore = null;
});

return pendingLoadMore;
}

export function showAlbumControls(): boolean {
return Boolean(source.showAlbumControls) && selectedIndex !== -1 && order.length > 1;
}

export function revealAlbum(item: DiscographyItem): void {
source.reveal?.(item);
}

export function findAlbumIndexById(albumId: string): number {
Expand Down
10 changes: 7 additions & 3 deletions src/label_view.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import Logger from './logger';
import { createPlayerDrawer, loadAlbumIntoDrawer } from './components/player';
import { createPlayerDrawer, loadAlbumIntoDrawer, getLoadedAlbumId } from './components/player';
import { updateDiscographyOrder } from './discography';
import { setFindMusicLinks, FindMusicLink } from './components/player/findMusicLinks';
import { extractBandId } from './utilities';
Expand Down Expand Up @@ -72,7 +72,7 @@ export function fillFrame(
const player = drawer();
const { isOpen, isMinimized } = player.getState();

if (isOpen && previewState.previewId === target.id) {
if (isOpen && getLoadedAlbumId() === target.id) {
if (isMinimized) player.maximizeDrawer();
else player.minimizeDrawer();
return;
Expand Down Expand Up @@ -102,7 +102,11 @@ export function attachPreviewListeners(
previewState: { previewOpen: boolean; previewId?: string },
enableFetchCaching: boolean = false
): void {
root.querySelectorAll('.open-iframe').forEach(button => {
root.querySelectorAll<HTMLElement>('.open-iframe').forEach(button => {
const feedPreviewsBounded = button.dataset.besBound === 'true';
if (feedPreviewsBounded) return;

button.dataset.besBound = 'true';
button.addEventListener('click', event => {
fillFrame(event, previewState, enableFetchCaching, port);
});
Expand Down
Loading
Loading