mirror of
https://github.com/FoxxMD/multi-scrobbler.git
synced 2026-09-03 05:10:00 +03:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
12f284dbd7 |
@@ -3,6 +3,8 @@ slug: /
|
||||
title: Overview
|
||||
sidebar_position: 1
|
||||
---
|
||||
import Carousel from '@site/src/components/Carousel/NextCarousel';
|
||||
import BrowserOnly from '@docusaurus/BrowserOnly';
|
||||
|
||||

|
||||

|
||||
@@ -64,6 +66,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?**
|
||||
|
||||
Generated
+30
@@ -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",
|
||||
|
||||
@@ -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 |
Reference in New Issue
Block a user