From d06784958d73159b5e4abafe5114804662114ed7 Mon Sep 17 00:00:00 2001 From: Ryan Schanzenbacher Date: Thu, 9 Jul 2026 22:15:53 -0400 Subject: move ubersicht modules in tree --- .../lib/components/data/youtube-music.jsx | 191 +++++++++++++++++++++ 1 file changed, 191 insertions(+) create mode 100755 users/ryan/modules/simple-bar/simple-bar-source/lib/components/data/youtube-music.jsx (limited to 'users/ryan/modules/simple-bar/simple-bar-source/lib/components/data/youtube-music.jsx') diff --git a/users/ryan/modules/simple-bar/simple-bar-source/lib/components/data/youtube-music.jsx b/users/ryan/modules/simple-bar/simple-bar-source/lib/components/data/youtube-music.jsx new file mode 100755 index 0000000..bf586b1 --- /dev/null +++ b/users/ryan/modules/simple-bar/simple-bar-source/lib/components/data/youtube-music.jsx @@ -0,0 +1,191 @@ +import * as Uebersicht from "uebersicht"; +import * as DataWidget from "./data-widget.jsx"; +import * as DataWidgetLoader from "./data-widget-loader.jsx"; +import useWidgetRefresh from "../../hooks/use-widget-refresh"; +import useServerSocket from "../../hooks/use-server-socket"; +import { useSimpleBarContext } from "../simple-bar-context.jsx"; +import * as Icons from "../icons/icons.jsx"; +import * as Utils from "../../utils"; + +export { youtubeMusicStyles as styles } from "../../styles/components/data/youtube-music"; + +const { React } = Uebersicht; + +const DEFAULT_REFRESH_FREQUENCY = 10000; + +/** + * YouTube Music Widget component. + * @returns {JSX.Element} The YouTube Music widget. + */ +export const Widget = React.memo(() => { + const { displayIndex, settings } = useSimpleBarContext(); + const { + widgets: { youtubeMusicWidget }, + youtubeMusicWidgetOptions: { + refreshFrequency, + showSpecter, + showOnDisplay, + youtubeMusicPort, + showIcon, + }, + } = settings; + + // Determine the refresh frequency for the widget + const refresh = React.useMemo( + () => + Utils.getRefreshFrequency(refreshFrequency, DEFAULT_REFRESH_FREQUENCY), + [refreshFrequency], + ); + + // Determine if the widget should be visible on the current display + const visible = + Utils.isVisibleOnDisplay(displayIndex, showOnDisplay) && youtubeMusicWidget; + + const [state, setState] = React.useState(); + const [cachedAccessToken, setCachedAccessToken] = React.useState(); + const [loading, setLoading] = React.useState(visible); + + const apiUrl = `http://localhost:${youtubeMusicPort}`; + + /** + * Resets the widget state. + */ + const resetWidget = () => { + setState(undefined); + setCachedAccessToken(undefined); + setLoading(false); + }; + + /** + * Fetches the access token for the YouTube Music API. + * @returns {Promise} The access token. + */ + const getAccessToken = React.useCallback(async () => { + // As of 2024/12/01, the generated access tokens don't expire + if (cachedAccessToken) return cachedAccessToken; + + const response = await fetch(`${apiUrl}/auth/simple-bar`, { + method: "POST", + }); + const json = await response.json(); + setCachedAccessToken(json.accessToken); + return json.accessToken; + }, [apiUrl, cachedAccessToken]); + + /** + * Fetches data from a specified route. + * @param {string} route - The API route to fetch data from. + * @param {string} [method="POST"] - The HTTP method to use. + * @returns {Promise} The fetch response. + */ + const fetchRoute = React.useCallback( + async (route, method = "POST") => { + const headers = {}; + const url = new URL(route, apiUrl); + + // All routes under /api are protected + const needsAuthentication = url.pathname.startsWith("/api"); + if (needsAuthentication) { + const accessToken = await getAccessToken(); + if (!accessToken) { + throw new Error("Failed to get access token"); + } + headers.Authorization = `Bearer ${accessToken}`; + } + + return await fetch(url, { method, headers }); + }, + [apiUrl, getAccessToken], + ); + + /** + * Refreshes the widget state by fetching the current song info. + */ + const refreshState = React.useCallback(async () => { + if (!visible) return; + + try { + const response = await fetchRoute("/api/v1/song-info", "GET"); + const json = await response.json(); + setState(json); + } catch { + // most likely due to offline server, reset state + resetWidget(); + } + }, [visible, fetchRoute]); + + // Use server socket to listen for updates + useServerSocket( + "youtube-music", + visible, + refreshState, + resetWidget, + setLoading, + ); + // Use widget refresh hook to periodically refresh the state + useWidgetRefresh(visible, refreshState, refresh); + + if (loading) return ; + + /** + * Handles click event to toggle play/pause. + * @param {React.MouseEvent} e - The click event. + */ + const onClick = async (e) => { + Utils.clickEffect(e); + await fetchRoute("/api/v1/toggle-play"); + await refreshState(); + }; + + /** + * Handles right-click event to skip to the next song. + * @param {React.MouseEvent} e - The right-click event. + */ + const onRightClick = async (e) => { + Utils.clickEffect(e); + await fetchRoute("/api/v1/next"); + await refreshState(); + }; + + /** + * Handles middle-click event to open YouTube Music app. + * @param {React.MouseEvent} e - The middle-click event. + */ + const onMiddleClick = async (e) => { + Utils.clickEffect(e); + Uebersicht.run(`open -a 'YouTube Music'`); + await refreshState(); + }; + + const classes = Utils.classNames("youtube-music", {}); + if (!state) return null; + const { title, artist, isPaused } = state; + if (!title) return null; + + const label = artist.length ? `${title} - ${artist}` : title; + const Icon = getIcon(isPaused); + + return ( + + {label} + + ); +}); + +Widget.displayName = "YouTube Music"; + +/** + * Returns the appropriate icon based on the play/pause state. + * @param {boolean} isPaused - Whether the music is paused. + * @returns {JSX.Element} The icon component. + */ +function getIcon(isPaused) { + return isPaused ? Icons.Paused : Icons.Playing; +} -- cgit v1.3