Compare commits

...
Author SHA1 Message Date
FoxxMD 1d7dcbd215 docs: Add preview UI callout to landing 2026-07-17 14:44:23 +00:00
13 changed files with 447 additions and 0 deletions
+14
View File
@@ -3,6 +3,8 @@ slug: /
title: Overview
sidebar_position: 1
---
import Carousel from '@site/src/components/Carousel/NextCarousel';
import BrowserOnly from '@docusaurus/BrowserOnly';
![Latest Release](https://img.shields.io/github/v/release/foxxmd/multi-scrobbler)
![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)
@@ -65,6 +67,18 @@ A dockerized app that monitors your music listening activity from *everywhere* a
[**Quick Start Guide**](/quickstart)
<DetailsAdmo type="tip" summary="New UI Preview">
A **full rewrite** of the UI is now available for preview. The new UI modernizes Multi-Scrobbler and gives you in depth insight into how your scrobbles are handled end-to-end.
<Carousel/>
To try out the stable-release preview navigate to `http://yourMSDomain:9078/next`
Or to try out the **latest preview snapshot** see the docker image pinned [in this comment.](https://github.com/FoxxMD/multi-scrobbler/issues/500#issuecomment-4917525794)
</DetailsAdmo>
<img src={require('/img/status-ui.png').default} width="800"/>
**Why should I use this over a browser extension and/or mobile app scrobbler?**
+30
View File
@@ -22,6 +22,8 @@
"clsx": "^2.0.0",
"docusaurus-plugin-sass": "^0.2.6",
"docusaurus-theme-github-codeblock": "^2.0.2",
"embla-carousel": "^9.0.0-rc02",
"embla-carousel-react": "^9.0.0-rc02",
"json5": "^2.2.3",
"micromark-extension-directive": "^3.0.1",
"modern-react-json-editor": "^1.0.2",
@@ -10863,6 +10865,34 @@
"integrity": "sha512-/4t+AS1l4S3ZC0Ja7PHFIWeBIxGA3QGqV8/yKsP36v7NcyUCl+bIcmw6s5zVuMIECWwBrAK/6QLzTmbJChBboQ==",
"license": "ISC"
},
"node_modules/embla-carousel": {
"version": "9.0.0-rc02",
"resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-9.0.0-rc02.tgz",
"integrity": "sha512-GOQBX6v5cNk/ptfuHUoHhW9rvl5KeSGsBoppaUqtihu2kDmQAyzvnH/95r3TaSScFSJTj68V9AM6E5nq3jUVcQ==",
"license": "MIT"
},
"node_modules/embla-carousel-react": {
"version": "9.0.0-rc02",
"resolved": "https://registry.npmjs.org/embla-carousel-react/-/embla-carousel-react-9.0.0-rc02.tgz",
"integrity": "sha512-DpcD0kTv6L31KvBcrx5d57E6T2rP/YceTGAdq7llkpKFdfM1of8YotHVEQTNIU3I9o5mjE/QxLi1ndJmm1DikA==",
"license": "MIT",
"dependencies": {
"embla-carousel": "9.0.0-rc02",
"embla-carousel-reactive-utils": "9.0.0-rc02"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.1 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
}
},
"node_modules/embla-carousel-reactive-utils": {
"version": "9.0.0-rc02",
"resolved": "https://registry.npmjs.org/embla-carousel-reactive-utils/-/embla-carousel-reactive-utils-9.0.0-rc02.tgz",
"integrity": "sha512-H1c+yUbcVST9eJoGU+05L1vSDyBs0hf/Gf1OadHYIJ3asBfyvn6oaGCs2wXq1D8/f09y0wwCxeRxT9UZU/TxJA==",
"license": "MIT",
"peerDependencies": {
"embla-carousel": "9.0.0-rc02"
}
},
"node_modules/emoji-regex": {
"version": "9.2.2",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
+2
View File
@@ -30,6 +30,8 @@
"clsx": "^2.0.0",
"docusaurus-plugin-sass": "^0.2.6",
"docusaurus-theme-github-codeblock": "^2.0.2",
"embla-carousel": "^9.0.0-rc02",
"embla-carousel-react": "^9.0.0-rc02",
"json5": "^2.2.3",
"micromark-extension-directive": "^3.0.1",
"modern-react-json-editor": "^1.0.2",
@@ -0,0 +1,96 @@
import React, {
ComponentPropsWithRef,
useCallback,
useEffect,
useState
} from 'react'
import { EmblaCarouselType } from 'embla-carousel'
type UsePrevNextButtonsType = {
prevBtnDisabled: boolean
nextBtnDisabled: boolean
onPrevButtonClick: () => void
onNextButtonClick: () => void
}
export const usePrevNextButtons = (
emblaApi: EmblaCarouselType | undefined
): UsePrevNextButtonsType => {
const [prevBtnDisabled, setPrevBtnDisabled] = useState(true)
const [nextBtnDisabled, setNextBtnDisabled] = useState(true)
const onPrevButtonClick = useCallback(() => {
if (!emblaApi) return
emblaApi.goToPrev()
}, [emblaApi])
const onNextButtonClick = useCallback(() => {
if (!emblaApi) return
emblaApi.goToNext()
}, [emblaApi])
const onSelect = useCallback((emblaApi: EmblaCarouselType) => {
setPrevBtnDisabled(!emblaApi.canGoToPrev())
setNextBtnDisabled(!emblaApi.canGoToNext())
}, [])
useEffect(() => {
if (!emblaApi) return
onSelect(emblaApi)
emblaApi.on('reinit', onSelect).on('select', onSelect)
}, [emblaApi, onSelect])
return {
prevBtnDisabled,
nextBtnDisabled,
onPrevButtonClick,
onNextButtonClick
}
}
type PropType = ComponentPropsWithRef<'button'>
export const PrevButton = (props: PropType) => {
const { children, disabled, ...restProps } = props
return (
<button
className={'embla__button embla__button--prev'.concat(
disabled ? ' embla__button--disabled' : ''
)}
type="button"
{...restProps}
>
<svg className="embla__button__svg" viewBox="0 0 532 532">
<path
fill="currentColor"
d="M355.66 11.354c13.793-13.805 36.208-13.805 50.001 0 13.785 13.804 13.785 36.238 0 50.034L201.22 266l204.442 204.61c13.785 13.805 13.785 36.239 0 50.044-13.793 13.796-36.208 13.796-50.002 0a5994246.277 5994246.277 0 0 0-229.332-229.454 35.065 35.065 0 0 1-10.326-25.126c0-9.2 3.393-18.26 10.326-25.2C172.192 194.973 332.731 34.31 355.66 11.354Z"
/>
</svg>
{children}
</button>
)
}
export const NextButton = (props: PropType) => {
const { children, disabled, ...restProps } = props
return (
<button
className={'embla__button embla__button--next'.concat(
disabled ? ' embla__button--disabled' : ''
)}
type="button"
{...restProps}
>
<svg className="embla__button__svg" viewBox="0 0 532 532">
<path
fill="currentColor"
d="M176.34 520.646c-13.793 13.805-36.208 13.805-50.001 0-13.785-13.804-13.785-36.238 0-50.034L330.78 266 126.34 61.391c-13.785-13.805-13.785-36.239 0-50.044 13.793-13.796 36.208-13.796 50.002 0 22.928 22.947 206.395 206.507 229.332 229.454a35.065 35.065 0 0 1 10.326 25.126c0 9.2-3.393 18.26-10.326 25.2-45.865 45.901-206.404 206.564-229.332 229.52Z"
/>
</svg>
{children}
</button>
)
}
@@ -0,0 +1,158 @@
:root[data-theme="light"] {
--text-body: rgb(54, 49, 61);
--text-comment: rgb(99, 94, 105);
--text-high-contrast: rgb(49, 49, 49);
--text-medium-contrast: rgb(99, 94, 105);
--text-low-contrast: rgb(116, 109, 118);
--detail-high-contrast: rgb(192, 192, 192);
--detail-medium-contrast: rgb(234, 234, 234);
--detail-low-contrast: rgb(240, 240, 242);
--text-body-rgb-value: 54, 49, 61;
--text-comment-rgb-value: 99, 94, 105;
--text-high-contrast-rgb-value: 49, 49, 49;
--text-medium-contrast-rgb-value: 99, 94, 105;
--text-low-contrast-rgb-value: 116, 109, 118;
--detail-high-contrast-rgb-value: 192, 192, 192;
--detail-medium-contrast-rgb-value: 234, 234, 234;
--detail-low-contrast-rgb-value: 240, 240, 242;
}
:root[data-theme="dark"] {
--text-body: rgb(222, 222, 222);
--text-comment: rgb(170, 170, 170);
--text-high-contrast: rgb(230, 230, 230);
--text-medium-contrast: rgb(202, 202, 202);
--text-low-contrast: rgb(170, 170, 170);
--detail-high-contrast: rgb(101, 101, 101);
--detail-medium-contrast: rgb(25, 25, 25);
--detail-low-contrast: rgb(21, 21, 21);
--text-body-rgb-value: 222, 222, 222;
--text-comment-rgb-value: 170, 170, 170;
--text-high-contrast-rgb-value: 230, 230, 230;
--text-medium-contrast-rgb-value: 202, 202, 202;
--text-low-contrast-rgb-value: 170, 170, 170;
--detail-high-contrast-rgb-value: 101, 101, 101;
--detail-medium-contrast-rgb-value: 25, 25, 25;
--detail-low-contrast-rgb-value: 21, 21, 21;
}
.embla {
max-width: 48rem;
margin: auto;
--slide-height: 33rem;
--slide-spacing: 1rem;
--slide-size: 100%;
margin-bottom: 1rem;
}
.embla__viewport {
overflow: hidden;
}
.embla__container {
display: flex;
touch-action: pan-y pinch-zoom;
margin-left: calc(var(--slide-spacing) * -1);
}
.embla__slide {
transform: translate3d(0, 0, 0);
flex: 0 0 var(--slide-size);
min-width: 0;
padding-left: var(--slide-spacing);
}
.embla__slide__container {
border: 0.15rem solid transparent;
border-radius: 1.2rem;
display: flex;
align-items: normal;
justify-content: center;
height: var(--slide-height);
user-select: none;
overflow:hidden;
flex-wrap: wrap;
}
.imageCaption {
font-style: italic;
font-size: 0.8em;
text-wrap-style: balance;
text-align: center;
}
.embla__controls {
display: grid;
grid-template-columns: auto 1fr;
justify-content: space-between;
gap: 1.2rem;
// margin-top: 1.8rem;
}
.embla__buttons {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 0.6rem;
align-items: center;
}
.embla__button {
-webkit-tap-highlight-color: rgba(var(--text-high-contrast-rgb-value), 0.5);
-webkit-appearance: none;
appearance: none;
background-color: transparent;
touch-action: manipulation;
display: inline-flex;
text-decoration: none;
cursor: pointer;
border: 0;
padding: 0;
margin: 0;
border: 0.2rem solid var(--detail-medium-contrast);
width: 2.5rem;
height: 2.5rem;
z-index: 1;
border-radius: 50%;
color: var(--text-body);
display: flex;
align-items: center;
justify-content: center;
transform: rotate(0deg);
}
.embla__button--disabled {
color: var(--detail-high-contrast);
}
.embla__button__svg {
width: 35%;
height: 35%;
}
.embla__selected-snap-display {
justify-self: flex-end;
align-self: center;
color: var(--text-low-contrast);
font-weight: 600;
}
.embla__slide:nth-child(1) {
flex: 0 0 90%;
}
.embla__slide:nth-child(2) {
flex: 0 0 60%;
}
.embla__slide:nth-child(3) {
flex: 0 0 75%;
}
.embla__slide:nth-child(4) {
flex: 0 0 80%;
}
.embla__slide:nth-child(5) {
flex: 0 0 100%;
}
.embla__slide:nth-child(6) {
flex: 0 0 100%;
}
@@ -0,0 +1,100 @@
import React, {useEffect} from 'react'
import BrowserOnly from "@docusaurus/BrowserOnly"
import './NextCarousel.scss';
import { EmblaOptionsType } from 'embla-carousel'
import useEmblaCarousel from 'embla-carousel-react'
import {
NextButton,
PrevButton,
usePrevNextButtons
} from './Arrows'
import {
SelectedSnapDisplay,
useSelectedSnapDisplay
} from './SnapDisplay'
import ComponentList from '@site/static/img/next/ms-componentlist.png';
import ComponentListDesktop from '@site/static/img/next/ms-componentlistdesktop.png';
import ComponentDetailed from '@site/static/img/next/ms-componentdetailed.png';
import PlaysList from '@site/static/img/next/ms-playlist.jpg';
import Timeline from '@site/static/img/next/ms-mbemptyquery.png';
import Logs from '@site/static/img/next/ms-logs.jpg';
const EmblaCarousel = (props: {options?: EmblaOptionsType}) => {
const { options } = props
const [emblaRef, emblaApi] = useEmblaCarousel(options)
const {
prevBtnDisabled,
nextBtnDisabled,
onPrevButtonClick,
onNextButtonClick
} = usePrevNextButtons(emblaApi)
const { selectedSnap, snapCount } = useSelectedSnapDisplay(emblaApi)
return (
<div className="embla">
<div className="embla__viewport" ref={emblaRef}>
<div className="embla__container">
<div className="embla__slide" key="1">
<div className="embla__slide__container">
<img src={ComponentListDesktop} />
<div className="imageCaption">Source/Client List on desktop</div>
</div>
</div>
<div className="embla__slide" key="2">
<div className="embla__slide__container">
<img src={ComponentList} />
<div className="imageCaption">Source/Client List on mobile</div>
</div>
</div>
<div className="embla__slide" key="3">
<div className="embla__slide__container">
<img src={ComponentDetailed} />
<div className="imageCaption">Source/Client Details</div>
</div>
</div>
<div className="embla__slide" key="4">
<div className="embla__slide__container">
<img src={PlaysList} />
<div className="imageCaption">Search for Plays/Scrobbles with live updates</div>
</div>
</div>
<div className="embla__slide" key="5">
<div className="embla__slide__container">
<img src={Timeline} />
<div className="imageCaption">Scrobble timeline audit trail with detailed errors</div>
</div>
</div>
<div className="embla__slide" key="6">
<div className="embla__slide__container">
<img src={Logs} />
<div className="imageCaption">Floating logs for dev-level troubleshooting</div>
</div>
</div>
</div>
</div>
<div className="embla__controls">
<div className="embla__buttons">
<PrevButton onClick={onPrevButtonClick} disabled={prevBtnDisabled} />
<NextButton onClick={onNextButtonClick} disabled={nextBtnDisabled} />
</div>
<SelectedSnapDisplay
selectedSnap={selectedSnap}
snapCount={snapCount}
/>
</div>
</div>
)
}
const ClientCarousel = () => {
return (
<BrowserOnly>{() => {
return <EmblaCarousel/>
}}</BrowserOnly>
)
}
export default ClientCarousel;
@@ -0,0 +1,47 @@
import React, { useCallback, useEffect, useState } from 'react'
import { EmblaCarouselType } from 'embla-carousel'
type UseSelectedSnapDisplayType = {
selectedSnap: number
snapCount: number
}
export const useSelectedSnapDisplay = (
emblaApi: EmblaCarouselType | undefined
): UseSelectedSnapDisplayType => {
const [selectedSnap, setSelectedSnap] = useState(0)
const [snapCount, setSnapCount] = useState(0)
const updateScrollSnapState = useCallback((emblaApi: EmblaCarouselType) => {
setSnapCount(emblaApi.snapList().length)
setSelectedSnap(emblaApi.selectedSnap())
}, [])
useEffect(() => {
if (!emblaApi) return
updateScrollSnapState(emblaApi)
emblaApi.on('select', updateScrollSnapState)
emblaApi.on('reinit', updateScrollSnapState)
}, [emblaApi, updateScrollSnapState])
return {
selectedSnap,
snapCount
}
}
type PropType = {
selectedSnap: number
snapCount: number
}
export const SelectedSnapDisplay = (props: PropType) => {
const { selectedSnap, snapCount } = props
return (
<div className="embla__selected-snap-display">
{selectedSnap + 1} / {snapCount}
</div>
)
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 304 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB