diff --git a/packages/react/package-lock.json b/packages/react/package-lock.json index a5b016637..9dda0e3bb 100644 --- a/packages/react/package-lock.json +++ b/packages/react/package-lock.json @@ -85,7 +85,7 @@ "type-fest": "^4.18.3", "use-seconds": "^1.7.0", "usehooks-ts": "^3.1.0", - "vaul": "^0.9.1", + "vaul": "^1.1.0", "virtua": "^0.33.7", "zod": "^3.23.8" }, @@ -10856,12 +10856,12 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, "node_modules/vaul": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/vaul/-/vaul-0.9.1.tgz", - "integrity": "sha512-fAhd7i4RNMinx+WEm6pF3nOl78DFkAazcN04ElLPFF9BMCNGbY/kou8UMhIcicm0rJCNePJP0Yyza60gGOD0Jw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/vaul/-/vaul-1.1.0.tgz", + "integrity": "sha512-YhO/bikcauk48hzhMhvIvT+U87cuCbNbKk9fF4Ou5UkI9t2KkBMernmdP37pCzF15hrv55fcny1YhexK8h6GVQ==", "license": "MIT", "dependencies": { - "@radix-ui/react-dialog": "^1.0.4" + "@radix-ui/react-dialog": "^1.1.1" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0", diff --git a/packages/react/package.json b/packages/react/package.json index 8b6a003a8..75a49478e 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -92,7 +92,7 @@ "type-fest": "^4.18.3", "use-seconds": "^1.7.0", "usehooks-ts": "^3.1.0", - "vaul": "^0.9.1", + "vaul": "^1.1.0", "virtua": "^0.33.7", "zod": "^3.23.8" }, diff --git a/packages/react/src/components/about/request/AddSubber.tsx b/packages/react/src/components/about/request/AddSubber.tsx index 7cc9b660c..0d9454246 100644 --- a/packages/react/src/components/about/request/AddSubber.tsx +++ b/packages/react/src/components/about/request/AddSubber.tsx @@ -131,8 +131,10 @@ export function AddSubberForm() { onSubmit={form.handleSubmit(onSubmit, onInvalid)} className="flex flex-col gap-4" > - -
+ +
+
+
{" "}
- -
+ +
+
+
- -
+ +
+
+
{" "} ( Channel @@ -136,12 +139,15 @@ export function ModifyInfoForm() { form={form} value={field.value} name="channel.name" - onSelect={({ name, id }) => { + type="any_channel" + onSelect={({ name, id, type }) => { + form.setValue("channel.id", id); form.setValue("channel.name", name); form.setValue( "channel.link", `https://www.youtube.com/channel/${id}`, ); + form.setValue("channel.type", type); // need to modify server to emit this. }} /> @@ -149,35 +155,41 @@ export function ModifyInfoForm() { )} /> - ( - - {t("channelRequest.ChannelLanguageLabel")} - - - - - - )} - /> - ( - - {t("channelRequest.VtuberGroupLabel")} - - - - - {t("channelRequest.VtuberGroupHint")} - - - - )} - /> + {channelType === "subber" && ( + ( + + + {t("channelRequest.ChannelLanguageLabel")} + + + + + + + )} + /> + )} + {channelType === "vtuber" && ( + ( + + {t("channelRequest.VtuberGroupLabel")} + + + + + {t("channelRequest.VtuberGroupHint")} + + + + )} + /> + )} ; value: FieldPathValue; onSelect: (value: SearchAutoCompleteChannel) => void; + type?: "vtuber" | "any_channel" | "clipper"; } export function ChannelPicker< T extends FieldValues, FieldName extends FieldPath, ->({ name, form, value, onSelect }: VtuberPickerProps) { +>({ + name, + form, + value, + onSelect, + type = "vtuber", +}: VtuberPickerProps) { const { t } = useTranslation(); const currentValue = useAtomValue(currentValueAtom); const [debouncedValue, setDebouncedValue] = useAtom(debouncedValueAtom); const { data, mutate, isPending } = useSearchAutoCompleteMutation(); useEffect(() => { - if (debouncedValue) mutate({ q: debouncedValue, n: 10, t: "vtuber" }); + if (debouncedValue && debouncedValue.length > 1) + mutate({ q: debouncedValue, n: 10, t: type }); // eslint-disable-next-line react-hooks/exhaustive-deps }, [debouncedValue]); @@ -85,9 +93,16 @@ export function ChannelPicker< })} /> - {t("component.channelPicker.notFound")} + {isPending && ( +
+
+
+ )} + + {!isPending && t("component.channelPicker.notFound")} + - {data?.vtuber?.map((channel) => ( + {data?.[type]?.map((channel) => ( ))} - {isPending && ( - -
- - )} diff --git a/packages/react/src/components/layout/Frame.tsx b/packages/react/src/components/layout/Frame.tsx index c8402fcd2..c3a82e372 100644 --- a/packages/react/src/components/layout/Frame.tsx +++ b/packages/react/src/components/layout/Frame.tsx @@ -12,11 +12,11 @@ import { } from "@/hooks/useFrame"; import { darkAtom } from "@/hooks/useTheme"; import { Toaster } from "@/shadcn/ui/toaster"; -import { orgAtom } from "@/store/org"; +import { orgAtom, orgRankingAtom } from "@/store/org"; import { miniPlayerAtom } from "@/store/player"; import clsx from "clsx"; import { useAtom, useAtomValue, useSetAtom } from "jotai"; -import { Suspense, useEffect } from "react"; +import { Suspense, useEffect, useState } from "react"; import { ErrorBoundary } from "react-error-boundary"; import { Outlet, useLocation, useNavigate } from "react-router-dom"; import { ErrorFallback } from "../common/ErrorFallback"; @@ -27,6 +27,8 @@ import SelectionFooter from "./SelectionFooter"; import { selectionModeAtom } from "@/hooks/useVideoSelection"; import { videoReportAtom } from "@/store/video"; import React from "react"; +import { useOrgs } from "@/services/orgs.service"; +import { useTimeout } from "usehooks-ts"; export function LocationAwareReactivity() { const location = useLocation(); @@ -53,6 +55,26 @@ export function LocationAwareReactivity() { return <>; } +export function GlobalReactivity() { + const [wait, setWait] = useState(false); + useTimeout(() => setWait(true), 1000); + const { data, isError } = useOrgs({ enabled: wait }); + const updateOrgRanking = useSetAtom(orgRankingAtom); + useEffect(() => { + if (data && data.length > 0) { + console.log("updating org ranking"); + updateOrgRanking((orgs: Org[]) => { + return orgs + .map((org) => data.find((x) => x.name === org.name)) + .filter((x): x is Org => !!x); + }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [data]); + + return <>; +} + const LazyVideoReportDialog = React.lazy(() => import("../video/VideoReport")); export function Frame() { @@ -109,6 +131,7 @@ export function Frame() { {isMobile &&
} {miniPlayer && } +
); } diff --git a/packages/react/src/components/settings/ToggleableFeature.tsx b/packages/react/src/components/settings/ToggleableFeature.tsx new file mode 100644 index 000000000..9ee5056f7 --- /dev/null +++ b/packages/react/src/components/settings/ToggleableFeature.tsx @@ -0,0 +1,113 @@ +import { cn } from "@/lib/utils"; +import { Checkbox } from "@/shadcn/ui/checkbox"; +import { Label } from "@/shadcn/ui/label"; +import React from "react"; + +interface BaseToggleableFeatureProps { + id: string; + checked: boolean; + onCheckedChange: (checked: boolean) => void; + label: string; +} + +interface IconToggleableFeatureProps extends BaseToggleableFeatureProps { + variant: "icon"; + icon: string; +} + +interface BasicToggleableFeatureProps extends BaseToggleableFeatureProps { + variant: "basic"; +} + +type ToggleableFeatureProps = + | IconToggleableFeatureProps + | BasicToggleableFeatureProps; + +interface ToggleableFeatureGroupProps { + features: ToggleableFeatureProps[]; + showDividers?: boolean; +} + +// Individual feature toggle component +const ToggleableFeatureSetting = (props: ToggleableFeatureProps) => { + if (props.variant === "icon") { + const { id, checked, onCheckedChange, label, icon } = props; + return ( +
+
+
+
+ + onCheckedChange(!checked)} + className="h-6 w-6" + /> +
+ ); + } + + const { id, checked, onCheckedChange, label } = props; + return ( +
+ onCheckedChange(!checked)} + /> + +
+ ); +}; + +// Group component to handle multiple toggles +// Group component to handle multiple toggles +export const ToggleableFeatureGroup = ({ + features, + showDividers = false, +}: ToggleableFeatureGroupProps) => { + return ( +
+ {features.map((feature, index) => ( + + + {showDividers && index < features.length - 1 && ( +
+ )} + + ))} +
+ ); +}; + +export default ToggleableFeatureGroup; diff --git a/packages/react/src/components/sidebar/sidebar.tsx b/packages/react/src/components/sidebar/sidebar.tsx index bbbb58600..725505f36 100644 --- a/packages/react/src/components/sidebar/sidebar.tsx +++ b/packages/react/src/components/sidebar/sidebar.tsx @@ -24,7 +24,6 @@ import { useOnClickOutside } from "usehooks-ts"; import { orgRankingAtom } from "@/store/org"; import { TLDexLogo } from "../common/TLDexLogo"; import { getThumbnailForOrg } from "@/lib/thumb"; -import { useOrgs } from "@/services/orgs.service"; export function Sidebar() { const { t } = useTranslation(); @@ -37,7 +36,6 @@ export function Sidebar() { const isMobile = useAtomValue(isMobileAtom); const toggle = useSetAtom(toggleSidebarAtom); const fs = useAtomValue(sidebarShouldBeFullscreenAtom); - const { data: orgs, isError } = useOrgs({ enabled: open }); const handleClickOutside = useCallback(() => { floating && open && setOpen(false); @@ -87,7 +85,7 @@ export function Sidebar() {
`${videoId}/${tldexState.liveTlLang}` as RoomIDString, + [videoId, tldexState.liveTlLang], + ); const { chatDB } = useSocket(roomID); return ( diff --git a/packages/react/src/components/tldex/new-editor/VideoIdInput.tsx b/packages/react/src/components/tldex/new-editor/VideoIdInput.tsx index 7604f0940..f880644e7 100644 --- a/packages/react/src/components/tldex/new-editor/VideoIdInput.tsx +++ b/packages/react/src/components/tldex/new-editor/VideoIdInput.tsx @@ -14,11 +14,11 @@ import { import { CLIPPER_LANGS } from "@/lib/consts"; import { useAtomValue } from "jotai"; import { userAtom } from "@/store/auth"; -import { tldexSettngsAtom } from "@/store/tldex"; +import { tldexLanguageAtom } from "@/store/tldex"; export function VideoIdInput() { const [searchParams] = useSearchParams(); - const tldexDefaults = useAtomValue(tldexSettngsAtom); + const liveTlLang = useAtomValue(tldexLanguageAtom); const id = searchParams.get("id") || ""; const editorLanguage = searchParams.get("tleditor-language"); const creditName = searchParams.get("caption-by"); @@ -26,7 +26,7 @@ export function VideoIdInput() { const [urlField, setUrlField] = useState(id); const [language, setLanguage] = useState( - editorLanguage || tldexDefaults.liveTlLang, + editorLanguage || liveTlLang, ); const [caption, setCaption] = useState( creditName || user?.username || "", diff --git a/packages/react/src/components/video/VideoCard.tsx b/packages/react/src/components/video/VideoCard.tsx index 08ac718be..a639cba63 100644 --- a/packages/react/src/components/video/VideoCard.tsx +++ b/packages/react/src/components/video/VideoCard.tsx @@ -17,6 +17,7 @@ import { } from "@/hooks/useVideoSelection"; import { isMobileAtom } from "@/hooks/useFrame"; import { ChannelImg } from "../channel/ChannelImg"; +import { tldexLanguageAtom } from "@/store/tldex"; export type VideoCardType = VideoRef & Partial & @@ -203,6 +204,9 @@ export function VideoCard({ [size, onClick, selectionMode, selectedSet, video.id], ); + const tlLang = useAtomValue(tldexLanguageAtom); + const tlcount = video.live_tl_count?.[tlLang] ?? 0; + const chName = usePreferredName(video.channel); return ( @@ -239,12 +243,21 @@ export function VideoCard({ {video.songcount && (
 {video.songcount} )} + {tlcount > 0 && ( + +
+  {tlcount} + + )} {showDuration && }
diff --git a/packages/react/src/hooks/useChatDB.ts b/packages/react/src/hooks/useChatDB.ts index 165896d65..f4bd30e4b 100644 --- a/packages/react/src/hooks/useChatDB.ts +++ b/packages/react/src/hooks/useChatDB.ts @@ -1,6 +1,6 @@ import { useAtom, useAtomValue, useStore } from "jotai"; import { useClient } from "./useClient"; -import { tldexSettngsAtom } from "@/store/tldex"; +import { tldexSettingsAtom } from "@/store/tldex"; import { roomToLang, roomToVideoID, toParsedMessage } from "@/lib/socket"; import { roomsAtom, videoToRoomAtom } from "@/store/chat"; import { useEffect } from "react"; @@ -11,7 +11,7 @@ export function useChatDB(roomId: RoomIDString) { const queryClient = useQueryClient(); const client = useClient(); const store = useStore(); - const tldexState = useAtomValue(tldexSettngsAtom); + const tldexState = useAtomValue(tldexSettingsAtom); const [room, setRoom] = useAtom(roomsAtom(roomId)); const [videoToRoom, setVideoToRoom] = useAtom( videoToRoomAtom(roomToVideoID(roomId)), diff --git a/packages/react/src/locales/en/ui.yml b/packages/react/src/locales/en/ui.yml index a2bbed49d..60f28399a 100644 --- a/packages/react/src/locales/en/ui.yml +++ b/packages/react/src/locales/en/ui.yml @@ -371,7 +371,8 @@ views: defaultHomepage: lastVisitedOrgHome: Last Visited Org Home favoritesWhenLoggedIn: Favorites (When Logged In) - hideFeaturesLabel: Hide Features + hideFeaturesLabel: Advanced + videoFilter: Filter Videos showTimezonesOnHover: Show Timezones on hover orgs: Starred Orgs filterDeadStreams: Hide Streams that don't go live for >2 hours diff --git a/packages/react/src/routes/about/request.tsx b/packages/react/src/routes/about/request.tsx index 4580aa29e..d32757768 100644 --- a/packages/react/src/routes/about/request.tsx +++ b/packages/react/src/routes/about/request.tsx @@ -46,6 +46,7 @@ export function AboutRequest() { +
{/* Making some space */}
{type === "addVtuber" && } {type === "addSubber" && } {type === "modifyInfo" && } diff --git a/packages/react/src/routes/settings/appearance.tsx b/packages/react/src/routes/settings/appearance.tsx index 0ffff29f2..bda6a83e5 100644 --- a/packages/react/src/routes/settings/appearance.tsx +++ b/packages/react/src/routes/settings/appearance.tsx @@ -1,7 +1,5 @@ import { SettingsItem } from "@/components/settings/SettingsItem"; import { Button } from "@/shadcn/ui/button"; -import { Checkbox } from "@/shadcn/ui/checkbox"; -import { Label } from "@/shadcn/ui/label"; import { DropdownMenu, DropdownMenuContent, @@ -23,6 +21,7 @@ import { import { useVideoCardSizes } from "@/store/video"; import { hideThumbnailAtom, englishNameAtom } from "@/store/settings"; import { Separator } from "@/shadcn/ui/separator"; +import ToggleableFeatureGroup from "@/components/settings/ToggleableFeature"; export const SettingsTheme = () => { const { t } = useTranslation(); @@ -51,6 +50,35 @@ export const SettingsTheme = () => { }, ] as const; + const gridSizeFeatures = gridSizes.map(({ label, value, icon }) => ({ + id: `gridSize-${value}`, + checked: size === value, + onCheckedChange: () => setSize(value), + label, + variant: "icon" as const, + icon, + })); + + // Then the display preference features + const displayPreferenceFeatures = [ + { + id: "hide_thumbnails", + checked: hideThumbnail, + onCheckedChange: () => setHideThumbnail(!hideThumbnail), + label: t("views.settings.hideVideoThumbnailsLabel"), + variant: "icon" as const, + icon: "i-lucide:image", + }, + { + id: "use_english_names", + checked: useENName, + onCheckedChange: () => setUseENName(!useENName), + label: t("views.settings.useEnglishNameMsg"), + variant: "icon" as const, + icon: "i-lucide:languages", + }, + ]; + return (
{/* Color Pickers */} @@ -116,109 +144,16 @@ export const SettingsTheme = () => { options={THEME_COLORS.concat(THEME_BASE_COLORS)} /> - {/* Grid Size Selection */} - {gridSizes.map(({ label, value, icon }) => ( -
-