Improve layout (#510)
* fix assessment layout * improve player layout * refactor sidebar * default system theme * may toggle theme * fix calendar in dark theme * fix style in dark mode * improve player layout
This commit is contained in:
Binary file not shown.
|
Before Width: | Height: | Size: 4.0 KiB |
@@ -7,7 +7,7 @@
|
||||
"config": "tailwind.config.js",
|
||||
"css": "src/index.css",
|
||||
"baseColor": "zinc",
|
||||
"cssVariables": false
|
||||
"cssVariables": true
|
||||
},
|
||||
"aliases": {
|
||||
"components": "src/renderer/components",
|
||||
|
||||
@@ -247,13 +247,17 @@
|
||||
"logoutAndRemoveAllPersonalData": "Logout and remove all personal data",
|
||||
"logoutAndRemoveAllPersonalSettings": "Logout and remove all personal settings",
|
||||
"hotkeys": "Hotkeys",
|
||||
"system": "System",
|
||||
"player": "Player",
|
||||
"quitApp": "Quit APP",
|
||||
"openPreferences": "Open preferences",
|
||||
"playOrPause": "Play or pause",
|
||||
"playOrPauseRecording": "Play or pause recording",
|
||||
"startOrStopRecording": "start or stop recording",
|
||||
"appearance": "Appearance",
|
||||
"theme": "Theme",
|
||||
"light": "Light",
|
||||
"dark": "Dark",
|
||||
"system": "System",
|
||||
"about": "About",
|
||||
"currentVersion": "Current version",
|
||||
"checkUpdate": "Check update",
|
||||
|
||||
@@ -254,6 +254,10 @@
|
||||
"playOrPause": "播放/暂停",
|
||||
"playOrPauseRecording": "播放/暂停录音",
|
||||
"startOrStopRecording": "开始/结束录音",
|
||||
"appearance": "外观",
|
||||
"theme": "主题",
|
||||
"light": "浅色",
|
||||
"dark": "深色",
|
||||
"about": "关于",
|
||||
"currentVersion": "当前版本",
|
||||
"checkUpdate": "检查更新",
|
||||
|
||||
@@ -444,10 +444,10 @@ ${log}
|
||||
// Create the browser window.
|
||||
const mainWindow = new BrowserWindow({
|
||||
icon: "./assets/icon.png",
|
||||
width: 1440,
|
||||
height: 900,
|
||||
minWidth: 1024,
|
||||
minHeight: 768,
|
||||
width: 1280,
|
||||
height: 720,
|
||||
minWidth: 720 ,
|
||||
minHeight: 576,
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, "preload.js"),
|
||||
spellcheck: false,
|
||||
|
||||
@@ -32,7 +32,7 @@ function App() {
|
||||
});
|
||||
|
||||
return (
|
||||
<ThemeProvider defaultTheme="light" storageKey="vite-ui-theme">
|
||||
<ThemeProvider defaultTheme="system" storageKey="vite-ui-theme">
|
||||
<AppSettingsProvider>
|
||||
<HotKeysSettingsProvider>
|
||||
<AISettingsProvider>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Link } from "react-router-dom";
|
||||
import { cn } from "@renderer/lib/utils";
|
||||
import { AudioLinesIcon } from "lucide-react";
|
||||
|
||||
export const AudioCard = (props: {
|
||||
audio: Partial<AudioType>;
|
||||
@@ -11,17 +12,21 @@ export const AudioCard = (props: {
|
||||
<div className={cn("w-full", className)}>
|
||||
<Link to={`/audios/${audio.id}`}>
|
||||
<div
|
||||
className="aspect-square border rounded-lg overflow-hidden"
|
||||
className="aspect-square border rounded-lg overflow-hidden flex"
|
||||
style={{
|
||||
borderBottomColor: `#${audio.md5.substr(0, 6)}`,
|
||||
borderBottomWidth: 3,
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={audio.coverUrl ? audio.coverUrl : "./assets/sound-waves.png"}
|
||||
crossOrigin="anonymous"
|
||||
className="hover:scale-105 object-cover w-full h-full"
|
||||
/>
|
||||
{audio.coverUrl ? (
|
||||
<img
|
||||
src={audio.coverUrl}
|
||||
crossOrigin="anonymous"
|
||||
className="hover:scale-105 object-cover w-full h-full"
|
||||
/>
|
||||
) : (
|
||||
<AudioLinesIcon className="hover:scale-105 object-cover w-1/2 h-1/2 m-auto" />
|
||||
)}
|
||||
</div>
|
||||
</Link>
|
||||
<div className="text-sm font-semibold mt-2 max-w-full line-clamp-2 h-10">
|
||||
|
||||
@@ -24,10 +24,12 @@ export const AudioPlayer = (props: { id?: string; md5?: string }) => {
|
||||
if (!layout) return <LoaderSpin />;
|
||||
|
||||
return (
|
||||
<div data-testid="audio-player">
|
||||
<div data-testid="audio-player" className={layout.wrapper}>
|
||||
<div className={`${layout.upperWrapper} mb-4`}>
|
||||
<div className="grid grid-cols-5 xl:grid-cols-3 gap-6 px-6 h-full">
|
||||
<div className={`col-span-2 xl:col-span-1 rounded-lg border shadow-lg ${layout.upperWrapper}`}>
|
||||
<div
|
||||
className={`col-span-2 xl:col-span-1 rounded-lg border shadow-lg ${layout.upperWrapper}`}
|
||||
>
|
||||
<MediaTabs />
|
||||
</div>
|
||||
<div className={`col-span-3 xl:col-span-2 ${layout.upperWrapper}`}>
|
||||
@@ -36,7 +38,7 @@ export const AudioPlayer = (props: { id?: string; md5?: string }) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={`${layout.lowerWrapper} flex flex-col`}>
|
||||
<div className={`flex flex-col`}>
|
||||
<div className={`${layout.playerWrapper} py-2 px-6`}>
|
||||
<MediaCurrentRecording />
|
||||
</div>
|
||||
@@ -45,7 +47,7 @@ export const AudioPlayer = (props: { id?: string; md5?: string }) => {
|
||||
<MediaPlayer />
|
||||
</div>
|
||||
|
||||
<div className={`${layout.panelWrapper} w-full bg-background z-10 shadow-xl`}>
|
||||
<div className={`${layout.panelWrapper} bg-background shadow-xl`}>
|
||||
<MediaPlayerControls />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
TabsTrigger,
|
||||
TabsContent,
|
||||
Separator,
|
||||
ScrollArea,
|
||||
} from "@renderer/components/ui";
|
||||
import { ConversationShortcuts } from "@renderer/components";
|
||||
import { t } from "i18next";
|
||||
@@ -30,42 +31,53 @@ export const MediaCaptionTabs = (props: {
|
||||
caption: TimelineEntry;
|
||||
selectedIndices: number[];
|
||||
toggleRegion: (index: number) => void;
|
||||
children?: React.ReactNode;
|
||||
}) => {
|
||||
const { caption, selectedIndices, toggleRegion } = props;
|
||||
const { caption, selectedIndices, toggleRegion, children } = props;
|
||||
|
||||
const [tab, setTab] = useState<string>("selected");
|
||||
|
||||
if (!caption) return null;
|
||||
|
||||
return (
|
||||
<Tabs value={tab} onValueChange={(value) => setTab(value)} className="">
|
||||
<TabsList className="grid grid-cols-4 gap-4 rounded-none sticky top-0 px-4 mb-4">
|
||||
<TabsTrigger value="selected">{t("captionTabs.selected")}</TabsTrigger>
|
||||
<TabsTrigger value="translation">
|
||||
{t("captionTabs.translation")}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="analysis">{t("captionTabs.analysis")}</TabsTrigger>
|
||||
<TabsTrigger value="note">{t("captionTabs.note")}</TabsTrigger>
|
||||
</TabsList>
|
||||
<ScrollArea className="h-full relative">
|
||||
<Tabs value={tab} onValueChange={(value) => setTab(value)} className="">
|
||||
{children}
|
||||
|
||||
<div className="px-4 pb-4 min-h-32">
|
||||
<SelectedTabContent
|
||||
caption={caption}
|
||||
selectedIndices={selectedIndices}
|
||||
toggleRegion={toggleRegion}
|
||||
/>
|
||||
<div className="px-4 pb-10 min-h-32">
|
||||
<SelectedTabContent
|
||||
caption={caption}
|
||||
selectedIndices={selectedIndices}
|
||||
toggleRegion={toggleRegion}
|
||||
/>
|
||||
|
||||
<TranslationTabContent text={caption.text} />
|
||||
<TranslationTabContent text={caption.text} />
|
||||
|
||||
<AnalysisTabContent text={caption.text} />
|
||||
<AnalysisTabContent text={caption.text} />
|
||||
|
||||
<TabsContent value="note">
|
||||
<div className="text-muted-foreground text-center py-4">
|
||||
Comming soon
|
||||
</div>
|
||||
</TabsContent>
|
||||
</div>
|
||||
</Tabs>
|
||||
<TabsContent value="note">
|
||||
<div className="text-muted-foreground text-center py-4">
|
||||
Comming soon
|
||||
</div>
|
||||
</TabsContent>
|
||||
</div>
|
||||
|
||||
<TabsList className="grid grid-cols-4 gap-4 rounded-none absolute w-full bottom-0 px-4">
|
||||
<TabsTrigger value="selected" className="block truncate px-1">
|
||||
{t("captionTabs.selected")}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="translation" className="block truncate px-1">
|
||||
{t("captionTabs.translation")}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="analysis" className="block truncate px-1">
|
||||
{t("captionTabs.analysis")}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="note" className="block truncate px-1">
|
||||
{t("captionTabs.note")}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
</ScrollArea>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -193,26 +205,28 @@ const SelectedTabContent = (props: {
|
||||
<div className="font-serif text-lg font-semibold tracking-tight">
|
||||
{word.text}
|
||||
</div>
|
||||
{
|
||||
word.timeline.length > 0 && (
|
||||
<div className="text-sm text-serif text-muted-foreground">
|
||||
<span
|
||||
className={`mr-2 font-code ${i === 0 ? "before:content-['/']" : ""
|
||||
}
|
||||
${i === selectedIndices.length - 1
|
||||
? "after:content-['/']"
|
||||
: ""
|
||||
{word.timeline.length > 0 && (
|
||||
<div className="text-sm text-serif text-muted-foreground">
|
||||
<span
|
||||
className={`mr-2 font-code ${
|
||||
i === 0 ? "before:content-['/']" : ""
|
||||
}
|
||||
${
|
||||
i === selectedIndices.length - 1
|
||||
? "after:content-['/']"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
{word.timeline
|
||||
.map((t) =>
|
||||
t.timeline.map((s) => convertIpaToNormal(s.text)).join("")
|
||||
)
|
||||
.join("")}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
>
|
||||
{word.timeline
|
||||
.map((t) =>
|
||||
t.timeline
|
||||
.map((s) => convertIpaToNormal(s.text))
|
||||
.join("")
|
||||
)
|
||||
.join("")}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
@@ -452,7 +466,7 @@ const AnalysisTabContent = (props: { text: string }) => {
|
||||
new URL(props.href ?? "");
|
||||
props.target = "_blank";
|
||||
props.rel = "noopener noreferrer";
|
||||
} catch (e) { }
|
||||
} catch (e) {}
|
||||
|
||||
return <a {...props}>{children}</a>;
|
||||
},
|
||||
|
||||
@@ -272,87 +272,96 @@ export const MediaCaption = () => {
|
||||
|
||||
return (
|
||||
<div className="h-full flex justify-between space-x-4">
|
||||
<ScrollArea className="flex-1 font-serif h-full border shadow-lg rounded-lg">
|
||||
<div className="flex flex-wrap px-4 py-2">
|
||||
{/* use the words splitted by caption text if it is matched with the timeline length, otherwise use the timeline */}
|
||||
{caption.text.split(" ").length !== caption.timeline.length
|
||||
? (caption.timeline || []).map((w, index) => (
|
||||
<div
|
||||
key={index}
|
||||
id={`word-${currentSegmentIndex}-${index}`}
|
||||
className={`pr-2 pb-2 cursor-pointer hover:bg-red-500/10 ${index === activeIndex ? "text-red-500" : ""
|
||||
} ${selectedIndices.includes(index)
|
||||
? "bg-red-500/10 selected"
|
||||
: ""
|
||||
}`}
|
||||
onClick={() => toggleRegion(index)}
|
||||
>
|
||||
<div className="">
|
||||
<div className="text-lg xl:text-xl 2xl:text-2xl">
|
||||
{w.text}
|
||||
</div>
|
||||
{displayIpa && (
|
||||
<div
|
||||
className={`text-sm 2xl:text-base text-muted-foreground font-code ${index === 0 ? "before:content-['/']" : ""
|
||||
}
|
||||
${index === caption.timeline.length - 1
|
||||
? "after:content-['/']"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
{w.timeline
|
||||
.map((t) =>
|
||||
t.timeline
|
||||
.map((s) => convertIpaToNormal(s.text))
|
||||
.join("")
|
||||
)
|
||||
.join(" · ")}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
: caption.text.split(" ").map((word, index) => (
|
||||
<div
|
||||
key={index}
|
||||
id={`word-${currentSegmentIndex}-${index}`}
|
||||
className={`pr-2 pb-2 cursor-pointer hover:bg-red-500/10 ${index === activeIndex ? "text-red-500" : ""
|
||||
} ${selectedIndices.includes(index) ? "bg-red-500/10" : ""}`}
|
||||
onClick={() => toggleRegion(index)}
|
||||
>
|
||||
<div className="">
|
||||
<div className="text-lg xl:text-xl 2xl:text-2xl">
|
||||
{word}
|
||||
</div>
|
||||
{displayIpa && (
|
||||
<div
|
||||
className={`text-sm 2xl:text-base text-muted-foreground font-code ${index === 0 ? "before:content-['/']" : ""
|
||||
}
|
||||
${index === caption.text.split(" ").length - 1
|
||||
? "after:content-['/']"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
{caption.timeline[index].timeline
|
||||
.map((t) =>
|
||||
t.timeline
|
||||
.map((s) => convertIpaToNormal(s.text))
|
||||
.join("")
|
||||
)
|
||||
.join(" · ")}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex-1 font-serif h-full border shadow-lg rounded-lg">
|
||||
<MediaCaptionTabs
|
||||
caption={caption}
|
||||
selectedIndices={selectedIndices}
|
||||
toggleRegion={toggleRegion}
|
||||
/>
|
||||
</ScrollArea>
|
||||
>
|
||||
<div className="flex flex-wrap px-4 py-2 rounded-t-lg bg-muted/50">
|
||||
{/* use the words splitted by caption text if it is matched with the timeline length, otherwise use the timeline */}
|
||||
{caption.text.split(" ").length !== caption.timeline.length
|
||||
? (caption.timeline || []).map((w, index) => (
|
||||
<div
|
||||
key={index}
|
||||
id={`word-${currentSegmentIndex}-${index}`}
|
||||
className={`p-1 pb-2 rounded cursor-pointer hover:bg-red-500/10 ${
|
||||
index === activeIndex ? "text-red-500" : ""
|
||||
} ${
|
||||
selectedIndices.includes(index)
|
||||
? "bg-red-500/10 selected"
|
||||
: ""
|
||||
}`}
|
||||
onClick={() => toggleRegion(index)}
|
||||
>
|
||||
<div className="">
|
||||
<div className="font-serif text-lg xl:text-xl 2xl:text-2xl">
|
||||
{w.text}
|
||||
</div>
|
||||
{displayIpa && (
|
||||
<div
|
||||
className={`text-sm 2xl:text-base text-muted-foreground font-code ${
|
||||
index === 0 ? "before:content-['/']" : ""
|
||||
}
|
||||
${
|
||||
index === caption.timeline.length - 1
|
||||
? "after:content-['/']"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
{w.timeline
|
||||
.map((t) =>
|
||||
t.timeline
|
||||
.map((s) => convertIpaToNormal(s.text))
|
||||
.join("")
|
||||
)
|
||||
.join(" · ")}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
: caption.text.split(" ").map((word, index) => (
|
||||
<div
|
||||
key={index}
|
||||
id={`word-${currentSegmentIndex}-${index}`}
|
||||
className={`p-1 pb-2 rounded cursor-pointer hover:bg-red-500/10 ${
|
||||
index === activeIndex ? "text-red-500" : ""
|
||||
} ${
|
||||
selectedIndices.includes(index) ? "bg-red-500/10" : ""
|
||||
}`}
|
||||
onClick={() => toggleRegion(index)}
|
||||
>
|
||||
<div className="">
|
||||
<div className="text-serif text-lg xl:text-xl 2xl:text-2xl">
|
||||
{word}
|
||||
</div>
|
||||
{displayIpa && (
|
||||
<div
|
||||
className={`text-sm 2xl:text-base text-muted-foreground font-code ${
|
||||
index === 0 ? "before:content-['/']" : ""
|
||||
}
|
||||
${
|
||||
index === caption.text.split(" ").length - 1
|
||||
? "after:content-['/']"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
{caption.timeline[index].timeline
|
||||
.map((t) =>
|
||||
t.timeline
|
||||
.map((s) => convertIpaToNormal(s.text))
|
||||
.join("")
|
||||
)
|
||||
.join(" · ")}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</MediaCaptionTabs>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col space-y-2">
|
||||
<Button
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetClose,
|
||||
ScrollArea,
|
||||
} from "@renderer/components/ui";
|
||||
import {
|
||||
GitCompareIcon,
|
||||
@@ -613,16 +614,16 @@ export const MediaCurrentRecording = () => {
|
||||
<Sheet open={detailIsOpen} onOpenChange={(open) => setDetailIsOpen(open)}>
|
||||
<SheetContent
|
||||
side="bottom"
|
||||
className="rounded-t-2xl shadow-lg"
|
||||
className="rounded-t-2xl shadow-lg max-h-screen overflow-y-scroll"
|
||||
displayClose={false}
|
||||
>
|
||||
<SheetHeader className="flex items-center justify-center -mt-4 mb-2">
|
||||
<SheetClose>
|
||||
<ChevronDownIcon />
|
||||
</SheetClose>
|
||||
</SheetHeader>
|
||||
<SheetHeader className="flex items-center justify-center -mt-4 mb-2">
|
||||
<SheetClose>
|
||||
<ChevronDownIcon />
|
||||
</SheetClose>
|
||||
</SheetHeader>
|
||||
|
||||
<RecordingDetail recording={currentRecording} />
|
||||
<RecordingDetail recording={currentRecording} />
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
</div>
|
||||
|
||||
@@ -24,7 +24,7 @@ export const MediaTabs = () => {
|
||||
return (
|
||||
<ScrollArea className="h-full">
|
||||
<div
|
||||
className={`p-1 bg-muted rounded-t-lg mb-2 text-sm sticky top-0 z-10 grid gap-4 ${media?.mediaType === "Video" ? "grid-cols-4" : "grid-cols-3"
|
||||
className={`p-1 bg-muted rounded-t-lg mb-2 text-sm sticky top-0 z-[1] grid gap-4 ${media?.mediaType === "Video" ? "grid-cols-4" : "grid-cols-3"
|
||||
}`}
|
||||
>
|
||||
{media.mediaType === "Video" && (
|
||||
|
||||
@@ -162,7 +162,7 @@ export const AssistantMessageComponent = (props: {
|
||||
|
||||
{configuration.type === "gpt" && (
|
||||
<Markdown
|
||||
className="message-content select-text prose"
|
||||
className="message-content select-text prose dark:prose-invert"
|
||||
components={{
|
||||
a({ node, children, ...props }) {
|
||||
try {
|
||||
|
||||
@@ -87,7 +87,7 @@ export const UserMessageComponent = (props: {
|
||||
>
|
||||
<div className="flex flex-col gap-2 px-4 py-2 bg-sky-500/30 border-sky-500 rounded-lg shadow-sm w-full">
|
||||
<Markdown
|
||||
className="select-text prose"
|
||||
className="select-text prose dark:prose-invert"
|
||||
components={{
|
||||
a({ node, children, ...props }) {
|
||||
try {
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
DefaultAudioLayout,
|
||||
defaultLayoutIcons,
|
||||
} from "@vidstack/react/player/layouts/default";
|
||||
export const STORAGE_WORKER_ENDPOINT = "https://enjoy-storage.baizhiheizi.com";
|
||||
import { STORAGE_WORKER_ENDPOINT } from "@/constants";
|
||||
import { TimelineEntry } from "echogarden/dist/utilities/Timeline.d.js";
|
||||
import { t } from "i18next";
|
||||
import { XCircleIcon } from "lucide-react";
|
||||
|
||||
@@ -12,7 +12,6 @@ import { formatDateTime } from "@renderer/lib/utils";
|
||||
import { t } from "i18next";
|
||||
import Markdown from "react-markdown";
|
||||
import { Link } from "react-router-dom";
|
||||
import { BotIcon } from "lucide-react";
|
||||
|
||||
export const PostCard = (props: {
|
||||
post: PostType;
|
||||
@@ -22,7 +21,7 @@ export const PostCard = (props: {
|
||||
const { user } = useContext(AppSettingsProviderContext);
|
||||
|
||||
return (
|
||||
<div className="rounded p-4 bg-background space-y-3">
|
||||
<div className="p-4 rounded-lg space-y-3 hover:bg-muted">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Link to={`/users/${post.user.id}`}>
|
||||
@@ -51,7 +50,7 @@ export const PostCard = (props: {
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{t("sharedPrompt")}
|
||||
</div>
|
||||
<Markdown className="prose prose-slate prose-pre:whitespace-pre-line select-text">
|
||||
<Markdown className="prose prose-slate prose-pre:whitespace-pre-line dark:prose-invert select-text">
|
||||
{"```prompt\n" + post.metadata.content + "\n```"}
|
||||
</Markdown>
|
||||
</>
|
||||
@@ -59,16 +58,17 @@ export const PostCard = (props: {
|
||||
|
||||
{post.metadata?.type === "gpt" && (
|
||||
<>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{t("sharedGpt")}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">{t("sharedGpt")}</div>
|
||||
<div className="text-sm">
|
||||
{t('models.conversation.roleDefinition')}:
|
||||
{t("models.conversation.roleDefinition")}:
|
||||
</div>
|
||||
<div className="prose prose-stone prose-pre:whitespace-pre-line select-text">
|
||||
<div className="prose prose-stone prose-pre:whitespace-pre-line dark:prose-invert select-text">
|
||||
<blockquote className="not-italic whitespace-pre-line">
|
||||
<Markdown>
|
||||
{(post.metadata.content as { [key: string]: any }).configuration?.roleDefinition}
|
||||
{
|
||||
(post.metadata.content as { [key: string]: any })
|
||||
.configuration?.roleDefinition
|
||||
}
|
||||
</Markdown>
|
||||
</blockquote>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { PostAudio } from "@renderer/components";
|
||||
import { t } from "i18next";
|
||||
import { MediaPlayer, MediaProvider } from "@vidstack/react";
|
||||
import { MediaPlayer, MediaProvider, PlayerSrc } from "@vidstack/react";
|
||||
import {
|
||||
DefaultVideoLayout,
|
||||
defaultLayoutIcons,
|
||||
@@ -24,7 +24,7 @@ export const PostMedium = (props: { medium: MediumType }) => {
|
||||
medium.extname.replace(".", "") || "mp4"
|
||||
}`,
|
||||
src: medium.sourceUrl,
|
||||
}}
|
||||
} as PlayerSrc}
|
||||
>
|
||||
<MediaProvider />
|
||||
<DefaultVideoLayout icons={defaultLayoutIcons} />
|
||||
|
||||
@@ -160,7 +160,7 @@ export const PostRecording = (props: {
|
||||
|
||||
{recording.referenceText && (
|
||||
<div className="mt-2 bg-muted px-4 py-2 rounded">
|
||||
<div className="text-muted-foreground text-center font-serif">
|
||||
<div className="text-muted-foreground text-center font-serif select-text">
|
||||
{recording.referenceText}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,15 +1,26 @@
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import { AppSettingsProviderContext } from "@renderer/context";
|
||||
import { PostCard, LoaderSpin } from "@renderer/components";
|
||||
import { toast, Button, Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from "@renderer/components//ui";
|
||||
import {
|
||||
toast,
|
||||
Button,
|
||||
Select,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
Separator,
|
||||
} from "@renderer/components//ui";
|
||||
import { t } from "i18next";
|
||||
|
||||
export const Posts = (props: { userId?: string }) => {
|
||||
const { userId } = props;
|
||||
const { webApi } = useContext(AppSettingsProviderContext);
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const [type, setType] = useState<'all' | 'recording' | 'medium' | 'story' | 'prompt' | 'gpt'>("all");
|
||||
const [by, setBy] = useState<'all' | 'following'>("following");
|
||||
const [type, setType] = useState<
|
||||
"all" | "recording" | "medium" | "story" | "prompt" | "gpt"
|
||||
>("all");
|
||||
const [by, setBy] = useState<"all" | "following">("following");
|
||||
const [posts, setPosts] = useState<PostType[]>([]);
|
||||
const [nextPage, setNextPage] = useState(1);
|
||||
|
||||
@@ -34,7 +45,7 @@ export const Posts = (props: { userId?: string }) => {
|
||||
items: 10,
|
||||
userId,
|
||||
by,
|
||||
type
|
||||
type,
|
||||
})
|
||||
.then((res) => {
|
||||
if (page === 1) {
|
||||
@@ -63,40 +74,69 @@ export const Posts = (props: { userId?: string }) => {
|
||||
return (
|
||||
<div className="max-w-screen-sm mx-auto">
|
||||
<div className="flex justify-end space-x-4 py-4">
|
||||
{
|
||||
!userId && <Select value={by} onValueChange={(value: 'all' | 'following') => setBy(value)}>
|
||||
{!userId && (
|
||||
<Select
|
||||
value={by}
|
||||
onValueChange={(value: "all" | "following") => setBy(value)}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem key="following" value="following">{t('following')}</SelectItem>
|
||||
<SelectItem key="all" value="all">{t('allUsers')}</SelectItem>
|
||||
<SelectItem key="following" value="following">
|
||||
{t("following")}
|
||||
</SelectItem>
|
||||
<SelectItem key="all" value="all">
|
||||
{t("allUsers")}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
}
|
||||
)}
|
||||
|
||||
<Select value={type} onValueChange={(value: 'all' | 'recording' | 'medium' | 'story' | 'prompt' | 'gpt') => setType(value)}>
|
||||
<Select
|
||||
value={type}
|
||||
onValueChange={(
|
||||
value: "all" | "recording" | "medium" | "story" | "prompt" | "gpt"
|
||||
) => setType(value)}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem key="all" value="all">{t('allTypes')}</SelectItem>
|
||||
<SelectItem key="recording" value="recording">{t('recordingType')}</SelectItem>
|
||||
<SelectItem key="prompt" value="prompt">{t('promptType')}</SelectItem>
|
||||
<SelectItem key="gpt" value="gpt">{t('gptType')}</SelectItem>
|
||||
<SelectItem key="medium" value="medium">{t('mediumType')}</SelectItem>
|
||||
<SelectItem key="story" value="story">{t('storyType')}</SelectItem>
|
||||
<SelectItem key="all" value="all">
|
||||
{t("allTypes")}
|
||||
</SelectItem>
|
||||
<SelectItem key="recording" value="recording">
|
||||
{t("recordingType")}
|
||||
</SelectItem>
|
||||
<SelectItem key="prompt" value="prompt">
|
||||
{t("promptType")}
|
||||
</SelectItem>
|
||||
<SelectItem key="gpt" value="gpt">
|
||||
{t("gptType")}
|
||||
</SelectItem>
|
||||
<SelectItem key="medium" value="medium">
|
||||
{t("mediumType")}
|
||||
</SelectItem>
|
||||
<SelectItem key="story" value="story">
|
||||
{t("storyType")}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{posts.length === 0 && (
|
||||
<div className="text-center text-muted-foreground py-4">{t("noOneSharedYet")}</div>
|
||||
<div className="text-center text-muted-foreground py-4">
|
||||
{t("noOneSharedYet")}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-6">
|
||||
{posts.map((post) => (
|
||||
<PostCard key={post.id} post={post} handleDelete={handleDelete} />
|
||||
<>
|
||||
<PostCard key={post.id} post={post} handleDelete={handleDelete} />
|
||||
<Separator />
|
||||
</>
|
||||
))}
|
||||
</div>
|
||||
|
||||
|
||||
14
enjoy/src/renderer/components/preferences/appearance.tsx
Normal file
14
enjoy/src/renderer/components/preferences/appearance.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import { t } from "i18next";
|
||||
import { Separator } from "@renderer/components/ui";
|
||||
import { LanguageSettings, ThemeSettings } from "@renderer/components";
|
||||
|
||||
export const Appearance = () => {
|
||||
return (
|
||||
<>
|
||||
<div className="font-semibold mb-4 capitilized">{t("appearance")}</div>
|
||||
<ThemeSettings />
|
||||
<Separator />
|
||||
<LanguageSettings />
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,5 +1,6 @@
|
||||
export * from "./preferences";
|
||||
export * from "./about";
|
||||
export * from "./appearance";
|
||||
|
||||
export * from "./hotkeys";
|
||||
export * from "./hotkeys-settings";
|
||||
@@ -18,4 +19,6 @@ export * from "./balance-settings";
|
||||
export * from "./reset-settings";
|
||||
export * from "./reset-all-settings";
|
||||
|
||||
export * from "./theme-settings";
|
||||
|
||||
export * from "./proxy-settings";
|
||||
|
||||
@@ -2,11 +2,11 @@ import { t } from "i18next";
|
||||
import { Button, ScrollArea, Separator } from "@renderer/components/ui";
|
||||
import {
|
||||
About,
|
||||
Appearance,
|
||||
DefaultEngineSettings,
|
||||
Hotkeys,
|
||||
UserSettings,
|
||||
BalanceSettings,
|
||||
LanguageSettings,
|
||||
LibrarySettings,
|
||||
WhisperSettings,
|
||||
OpenaiSettings,
|
||||
@@ -73,8 +73,6 @@ export const Preferences = () => {
|
||||
<Separator />
|
||||
<BalanceSettings />
|
||||
<Separator />
|
||||
<LanguageSettings />
|
||||
<Separator />
|
||||
</div>
|
||||
),
|
||||
},
|
||||
@@ -83,6 +81,11 @@ export const Preferences = () => {
|
||||
label: t("hotkeys"),
|
||||
component: () => <Hotkeys />,
|
||||
},
|
||||
{
|
||||
value: "appearance",
|
||||
label: t("appearance"),
|
||||
component: () => <Appearance />,
|
||||
},
|
||||
{
|
||||
value: "about",
|
||||
label: t("about"),
|
||||
|
||||
56
enjoy/src/renderer/components/preferences/theme-settings.tsx
Normal file
56
enjoy/src/renderer/components/preferences/theme-settings.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
import { useTheme } from "@renderer/context";
|
||||
import { MoonIcon, SunIcon } from "lucide-react";
|
||||
import {
|
||||
Button,
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@renderer/components/ui";
|
||||
import { t } from "i18next";
|
||||
|
||||
export const ThemeSettings = () => {
|
||||
const { setTheme, theme } = useTheme();
|
||||
|
||||
return (
|
||||
<div className="flex items-start justify-between py-4">
|
||||
<div className="">
|
||||
<div className="mb-2">{t("theme")}</div>
|
||||
<div className="text-sm text-muted-foreground mb-2">{t(theme)}</div>
|
||||
</div>
|
||||
|
||||
<div className="">
|
||||
<div className="flex items-center justify-end space-x-2 mb-2">
|
||||
<Select
|
||||
value={theme}
|
||||
onValueChange={(theme: "light" | "dark" | "system") => {
|
||||
setTheme(theme);
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="text-xs">
|
||||
<SelectValue asChild>
|
||||
<Button variant="ghost" size="icon">
|
||||
<SunIcon className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
|
||||
<MoonIcon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
|
||||
<span className="sr-only">Toggle theme</span>
|
||||
</Button>
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem className="text-xs" value="light">
|
||||
{t("light")}
|
||||
</SelectItem>
|
||||
<SelectItem className="text-xs" value="dark">
|
||||
{t("dark")}
|
||||
</SelectItem>
|
||||
<SelectItem className="text-xs" value="system">
|
||||
{t("system")}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,9 +1,9 @@
|
||||
import { useCallback, useState, useEffect, useContext } from "react";
|
||||
import { AppSettingsProviderContext } from "@/renderer/context";
|
||||
import { AppSettingsProviderContext } from "@renderer/context";
|
||||
import { formatDate, secondsToTimestamp } from "@renderer/lib/utils";
|
||||
import { t } from "i18next";
|
||||
import { Link } from "react-router-dom";
|
||||
import { LoaderIcon } from "lucide-react";
|
||||
import { LoaderIcon, AudioLinesIcon } from "lucide-react";
|
||||
|
||||
export const RecordingActivities = (props: { from: string; to: string }) => {
|
||||
const { from, to } = props;
|
||||
@@ -102,7 +102,7 @@ const Activity = (props: {
|
||||
)}
|
||||
<div className="flex items-center space-x-2 mb-4">
|
||||
{activity.targetType === "Audio" && (
|
||||
<img src="./assets/sound-waves.png" className="w-6 h-6" />
|
||||
<AudioLinesIcon className="w-4 h-4" />
|
||||
)}
|
||||
|
||||
{activity.targetType === "Audio" && (
|
||||
|
||||
@@ -4,7 +4,7 @@ import Calendar, {
|
||||
Skeleton,
|
||||
ThemeInput,
|
||||
} from "react-activity-calendar";
|
||||
import { AppSettingsProviderContext } from "@renderer/context";
|
||||
import { AppSettingsProviderContext, useTheme } from "@renderer/context";
|
||||
import { ScrollArea, Button } from "@renderer/components/ui";
|
||||
import i18next, { t } from "i18next";
|
||||
import dayjs, { Dayjs } from "dayjs";
|
||||
@@ -22,6 +22,7 @@ export const RecordingCalendar = (props: {
|
||||
onSelectRange?: (from: string, to: string) => void;
|
||||
}) => {
|
||||
const { onSelectRange } = props;
|
||||
const { colorScheme } = useTheme();
|
||||
|
||||
dayjs.extend(localeData);
|
||||
dayjs.locale(i18next.resolvedLanguage?.toLowerCase() || "en");
|
||||
@@ -135,6 +136,7 @@ export const RecordingCalendar = (props: {
|
||||
}),
|
||||
}}
|
||||
theme={DEFAULT_THEME}
|
||||
colorScheme={colorScheme as 'light' | 'dark'}
|
||||
renderBlock={(block, activity) =>
|
||||
React.cloneElement(block, {
|
||||
...block.props,
|
||||
|
||||
@@ -19,7 +19,6 @@ import {
|
||||
import { useLocation, Link } from "react-router-dom";
|
||||
import { t } from "i18next";
|
||||
import { Preferences } from "@renderer/components";
|
||||
import { Tooltip } from "react-tooltip";
|
||||
|
||||
export const Sidebar = () => {
|
||||
const location = useLocation();
|
||||
@@ -27,27 +26,28 @@ export const Sidebar = () => {
|
||||
|
||||
return (
|
||||
<div
|
||||
className="h-[100vh] w-20 xl:w-48 2xl:w-64 transition-all relative"
|
||||
className="h-[100vh] w-20 xl:w-48 transition-all relative"
|
||||
data-testid="sidebar"
|
||||
>
|
||||
<div className="fixed top-0 left-0 h-full w-20 xl:w-48 2xl:w-64">
|
||||
<div className="fixed top-0 left-0 h-full w-20 xl:w-48 bg-muted">
|
||||
<ScrollArea className="w-full h-full">
|
||||
<div className="px-1 xl:px-3 pt-6 mb-2 flex items-center space-x-1 justify-center">
|
||||
<div className="py-4 flex items-center space-x-1 justify-center">
|
||||
<img src="./assets/logo-light.svg" className="w-8 h-8" />
|
||||
<span className="hidden xl:block text-xl font-semibold text-[#4797F5]">
|
||||
ENJOY
|
||||
</span>
|
||||
</div>
|
||||
<div className="xl:px-3 py-4">
|
||||
<div className="xl:px-3 py-2">
|
||||
<div className="xl:pl-3">
|
||||
<Link
|
||||
to="/"
|
||||
data-tooltip-id="sidebar-tooltip"
|
||||
data-tooltip-id="global-tooltip"
|
||||
data-tooltip-content={t("sidebar.home")}
|
||||
className="block"
|
||||
data-tooltip-place="right"
|
||||
className="block px-2"
|
||||
>
|
||||
<Button
|
||||
variant={activeTab === "" ? "secondary" : "ghost"}
|
||||
variant={activeTab === "/" ? "default" : "ghost"}
|
||||
className="w-full xl:justify-start"
|
||||
>
|
||||
<HomeIcon className="xl:mr-2 h-5 w-5" />
|
||||
@@ -57,15 +57,16 @@ export const Sidebar = () => {
|
||||
|
||||
<Link
|
||||
to="/conversations"
|
||||
data-tooltip-id="sidebar-tooltip"
|
||||
data-tooltip-id="global-tooltip"
|
||||
data-tooltip-content={t("sidebar.aiAssistant")}
|
||||
data-tooltip-place="right"
|
||||
data-testid="sidebar-conversations"
|
||||
className="block"
|
||||
className="block px-2"
|
||||
>
|
||||
<Button
|
||||
variant={
|
||||
activeTab.startsWith("conversations")
|
||||
? "secondary"
|
||||
activeTab.startsWith("/conversations")
|
||||
? "default"
|
||||
: "ghost"
|
||||
}
|
||||
className="w-full xl:justify-start"
|
||||
@@ -79,12 +80,13 @@ export const Sidebar = () => {
|
||||
|
||||
<Link
|
||||
to="/community"
|
||||
data-tooltip-id="sidebar-tooltip"
|
||||
data-tooltip-id="global-tooltip"
|
||||
data-tooltip-content={t("sidebar.community")}
|
||||
className="block"
|
||||
data-tooltip-place="right"
|
||||
className="block px-2"
|
||||
>
|
||||
<Button
|
||||
variant={activeTab === "" ? "secondary" : "ghost"}
|
||||
variant={activeTab === "/community" ? "default" : "ghost"}
|
||||
className="w-full xl:justify-start"
|
||||
>
|
||||
<UsersRoundIcon className="xl:mr-2 h-5 w-5" />
|
||||
@@ -96,162 +98,139 @@ export const Sidebar = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 xl:space-y-4">
|
||||
<div className="xl:px-3 py-2">
|
||||
<h3 className="hidden xl:block mb-2 px-4 text-lg font-semibold tracking-tight">
|
||||
{t("sidebar.library")}
|
||||
</h3>
|
||||
<div className="xl:pl-3 space-y-2">
|
||||
<Link
|
||||
to="/audios"
|
||||
data-tooltip-id="sidebar-tooltip"
|
||||
data-tooltip-content={t("sidebar.audios")}
|
||||
className="block"
|
||||
<div className="xl:px-3 py-2">
|
||||
<h3 className="hidden xl:block mb-2 px-4 text-lg font-semibold tracking-tight">
|
||||
{t("sidebar.library")}
|
||||
</h3>
|
||||
<div className="xl:pl-3 space-y-2">
|
||||
<Link
|
||||
to="/audios"
|
||||
data-tooltip-id="global-tooltip"
|
||||
data-tooltip-content={t("sidebar.audios")}
|
||||
data-tooltip-place="right"
|
||||
className="block px-2"
|
||||
>
|
||||
<Button
|
||||
variant={
|
||||
activeTab.startsWith("/audios") ? "default" : "ghost"
|
||||
}
|
||||
className="w-full xl:justify-start"
|
||||
>
|
||||
<Button
|
||||
variant={
|
||||
activeTab.startsWith("/audios") ? "secondary" : "ghost"
|
||||
}
|
||||
className="w-full xl:justify-start"
|
||||
>
|
||||
<HeadphonesIcon className="xl:mr-2 h-5 w-5" />
|
||||
<span className="hidden xl:block">
|
||||
{t("sidebar.audios")}
|
||||
</span>
|
||||
</Button>
|
||||
</Link>
|
||||
<HeadphonesIcon className="xl:mr-2 h-5 w-5" />
|
||||
<span className="hidden xl:block">{t("sidebar.audios")}</span>
|
||||
</Button>
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
to="/videos"
|
||||
data-tooltip-id="sidebar-tooltip"
|
||||
data-tooltip-content={t("sidebar.videos")}
|
||||
className="block"
|
||||
<Link
|
||||
to="/videos"
|
||||
data-tooltip-id="global-tooltip"
|
||||
data-tooltip-content={t("sidebar.videos")}
|
||||
data-tooltip-place="right"
|
||||
className="block px-2"
|
||||
>
|
||||
<Button
|
||||
variant={
|
||||
activeTab.startsWith("/videos") ? "default" : "ghost"
|
||||
}
|
||||
className="w-full xl:justify-start"
|
||||
>
|
||||
<Button
|
||||
variant={
|
||||
activeTab.startsWith("/videos") ? "secondary" : "ghost"
|
||||
}
|
||||
className="w-full xl:justify-start"
|
||||
>
|
||||
<VideoIcon className="xl:mr-2 h-5 w-5" />
|
||||
<span className="hidden xl:block">
|
||||
{t("sidebar.videos")}
|
||||
</span>
|
||||
</Button>
|
||||
</Link>
|
||||
<VideoIcon className="xl:mr-2 h-5 w-5" />
|
||||
<span className="hidden xl:block">{t("sidebar.videos")}</span>
|
||||
</Button>
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
to="/stories"
|
||||
data-tooltip-id="sidebar-tooltip"
|
||||
data-tooltip-content={t("sidebar.stories")}
|
||||
className="block"
|
||||
<Link
|
||||
to="/stories"
|
||||
data-tooltip-id="global-tooltip"
|
||||
data-tooltip-content={t("sidebar.stories")}
|
||||
data-tooltip-place="right"
|
||||
className="block px-2"
|
||||
>
|
||||
<Button
|
||||
variant={
|
||||
activeTab.startsWith("/stories") ? "default" : "ghost"
|
||||
}
|
||||
className="w-full xl:justify-start"
|
||||
>
|
||||
<Button
|
||||
variant={
|
||||
activeTab.startsWith("/stories") ? "secondary" : "ghost"
|
||||
}
|
||||
className="w-full xl:justify-start"
|
||||
>
|
||||
<NewspaperIcon className="xl:mr-2 h-5 w-5" />
|
||||
<span className="hidden xl:block">
|
||||
{t("sidebar.stories")}
|
||||
</span>
|
||||
</Button>
|
||||
</Link>
|
||||
{/* */}
|
||||
{/* <Link */}
|
||||
{/* to="/books" */}
|
||||
{/* data-tooltip-id="sidebar-tooltip" */}
|
||||
{/* data-tooltip-content={t("sidebar.books")} */}
|
||||
{/* className="block" */}
|
||||
{/* > */}
|
||||
{/* <Button */}
|
||||
{/* variant={ */}
|
||||
{/* activeTab.startsWith("books") ? "secondary" : "ghost" */}
|
||||
{/* } */}
|
||||
{/* className="w-full xl:justify-start" */}
|
||||
{/* > */}
|
||||
{/* <BookOpenTextIcon className="xl:mr-2 h-5 w-5" /> */}
|
||||
{/* <span className="hidden xl:block"> */}
|
||||
{/* {t("sidebar.books")} */}
|
||||
{/* </span> */}
|
||||
{/* </Button> */}
|
||||
{/* </Link> */}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="xl:px-3 py-2">
|
||||
<h3 className="hidden xl:block mb-2 px-4 text-lg font-semibold tracking-tight">
|
||||
{t("sidebar.mine")}
|
||||
</h3>
|
||||
<div className="xl:pl-3 space-y-2">
|
||||
<Link
|
||||
to="/vocabulary"
|
||||
data-tooltip-id="sidebar-tooltip"
|
||||
data-tooltip-content={t("sidebar.vocabulary")}
|
||||
className="block"
|
||||
>
|
||||
<Button
|
||||
variant={
|
||||
activeTab.startsWith("vocabulary") ? "secondary" : "ghost"
|
||||
}
|
||||
className="w-full xl:justify-start"
|
||||
>
|
||||
<BookMarkedIcon className="xl:mr-2 h-5 w-5" />
|
||||
<span className="hidden xl:block">
|
||||
{t("sidebar.vocabulary")}
|
||||
</span>
|
||||
</Button>
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
to="/profile"
|
||||
data-tooltip-id="sidebar-tooltip"
|
||||
data-tooltip-content={t("sidebar.profile")}
|
||||
className="block"
|
||||
>
|
||||
<Button
|
||||
variant={
|
||||
activeTab.startsWith("/profile") ? "secondary" : "ghost"
|
||||
}
|
||||
className="w-full xl:justify-start"
|
||||
>
|
||||
<UserIcon className="xl:mr-2 h-5 w-5" />
|
||||
<span className="hidden xl:block">
|
||||
{t("sidebar.profile")}
|
||||
</span>
|
||||
</Button>
|
||||
</Link>
|
||||
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
variant={
|
||||
activeTab.startsWith("/settings")
|
||||
? "secondary"
|
||||
: "ghost"
|
||||
}
|
||||
id="preferences-button"
|
||||
className="w-full xl:justify-start"
|
||||
data-tooltip-id="sidebar-tooltip"
|
||||
data-tooltip-content={t("sidebar.preferences")}
|
||||
>
|
||||
<SettingsIcon className="xl:mr-2 h-5 w-5" />
|
||||
<span className="hidden xl:block">
|
||||
{t("sidebar.preferences")}
|
||||
</span>
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
|
||||
<DialogContent className="max-w-screen-md xl:max-w-screen-lg h-5/6 p-0">
|
||||
<Preferences />
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
<NewspaperIcon className="xl:mr-2 h-5 w-5" />
|
||||
<span className="hidden xl:block">
|
||||
{t("sidebar.stories")}
|
||||
</span>
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Tooltip id="sidebar-tooltip" />
|
||||
<div className="xl:px-3 py-2">
|
||||
<h3 className="hidden xl:block mb-2 px-4 text-lg font-semibold tracking-tight">
|
||||
{t("sidebar.mine")}
|
||||
</h3>
|
||||
<div className="xl:pl-3 space-y-2">
|
||||
<Link
|
||||
to="/vocabulary"
|
||||
data-tooltip-id="global-tooltip"
|
||||
data-tooltip-content={t("sidebar.vocabulary")}
|
||||
data-tooltip-place="right"
|
||||
className="block px-2"
|
||||
>
|
||||
<Button
|
||||
variant={
|
||||
activeTab.startsWith("/vocabulary") ? "default" : "ghost"
|
||||
}
|
||||
className="w-full xl:justify-start"
|
||||
>
|
||||
<BookMarkedIcon className="xl:mr-2 h-5 w-5" />
|
||||
<span className="hidden xl:block">
|
||||
{t("sidebar.vocabulary")}
|
||||
</span>
|
||||
</Button>
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
to="/profile"
|
||||
data-tooltip-id="global-tooltip"
|
||||
data-tooltip-content={t("sidebar.profile")}
|
||||
data-tooltip-place="right"
|
||||
className="block px-2"
|
||||
>
|
||||
<Button
|
||||
variant={
|
||||
activeTab.startsWith("/profile") ? "default" : "ghost"
|
||||
}
|
||||
className="w-full xl:justify-start"
|
||||
>
|
||||
<UserIcon className="xl:mr-2 h-5 w-5" />
|
||||
<span className="hidden xl:block">
|
||||
{t("sidebar.profile")}
|
||||
</span>
|
||||
</Button>
|
||||
</Link>
|
||||
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
variant={
|
||||
activeTab.startsWith("/settings") ? "default" : "ghost"
|
||||
}
|
||||
id="preferences-button"
|
||||
className="w-full xl:justify-start"
|
||||
data-tooltip-id="global-tooltip"
|
||||
data-tooltip-content={t("sidebar.preferences")}
|
||||
data-tooltip-place="right"
|
||||
>
|
||||
<SettingsIcon className="xl:mr-2 h-5 w-5" />
|
||||
<span className="hidden xl:block">
|
||||
{t("sidebar.preferences")}
|
||||
</span>
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
|
||||
<DialogContent className="max-w-screen-md xl:max-w-screen-lg h-5/6 p-0">
|
||||
<Preferences />
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -25,7 +25,7 @@ export const VideoPlayer = (props: { id?: string; md5?: string }) => {
|
||||
if (!layout) return <LoaderSpin />;
|
||||
|
||||
return (
|
||||
<div data-testid="video-player">
|
||||
<div data-testid="video-player" className={layout.wrapper}>
|
||||
<div className={`${layout.upperWrapper} mb-4`}>
|
||||
<div className="grid grid-cols-5 xl:grid-cols-3 gap-6 px-6 h-full">
|
||||
<div className={`col-span-2 xl:col-span-1 rounded-lg border shadow-lg ${layout.upperWrapper}`}>
|
||||
@@ -46,7 +46,7 @@ export const VideoPlayer = (props: { id?: string; md5?: string }) => {
|
||||
<MediaPlayer />
|
||||
</div>
|
||||
|
||||
<div className={`${layout.panelWrapper} w-full bg-background z-10 shadow-xl`}>
|
||||
<div className={`${layout.panelWrapper} bg-background shadow-xl`}>
|
||||
<MediaPlayerControls />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -14,7 +14,17 @@ import { Tooltip } from "react-tooltip";
|
||||
import { debounce } from "lodash";
|
||||
|
||||
type MediaPlayerContextType = {
|
||||
layout: { name: string, width: number, height: number, upperWrapper: string, lowerWrapper: string, playerWrapper: string, panelWrapper: string, playerHeight: number };
|
||||
layout: {
|
||||
name: string;
|
||||
width: number;
|
||||
height: number;
|
||||
wrapper: string;
|
||||
upperWrapper: string;
|
||||
lowerWrapper: string;
|
||||
playerWrapper: string;
|
||||
panelWrapper: string;
|
||||
playerHeight: number;
|
||||
};
|
||||
media: AudioType | VideoType;
|
||||
setMedia: (media: AudioType | VideoType) => void;
|
||||
setMediaProvider: (mediaProvider: HTMLAudioElement | null) => void;
|
||||
@@ -72,22 +82,24 @@ export const MediaPlayerProviderContext =
|
||||
|
||||
const LAYOUT = {
|
||||
sm: {
|
||||
name: 'sm',
|
||||
upperWrapper: "h-[calc(100vh-27.5rem)]",
|
||||
name: "sm",
|
||||
wrapper: "h-[calc(100vh-3.5rem)]",
|
||||
upperWrapper: "h-[calc(100vh-27.5rem)] min-h-64",
|
||||
lowerWrapper: "h-[23rem]",
|
||||
playerWrapper: "h-[9rem] mb-2",
|
||||
panelWrapper: "h-16",
|
||||
panelWrapper: "h-16 w-full z-10 sticky bottom-0",
|
||||
playerHeight: 128,
|
||||
},
|
||||
lg: {
|
||||
name: 'lg',
|
||||
name: "lg",
|
||||
wrapper: "h-[calc(100vh-3.5rem)]",
|
||||
upperWrapper: "h-[calc(100vh-37.5rem)]",
|
||||
lowerWrapper: "h-[33rem]",
|
||||
panelWrapper: "h-20",
|
||||
panelWrapper: "h-20 w-full z-10 sticky bottom-0",
|
||||
playerWrapper: "h-[13rem] mb-4",
|
||||
playerHeight: 192,
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
export const MediaPlayerProvider = ({
|
||||
children,
|
||||
@@ -97,7 +109,17 @@ export const MediaPlayerProvider = ({
|
||||
const minPxPerSec = 150;
|
||||
const { EnjoyApp } = useContext(AppSettingsProviderContext);
|
||||
|
||||
const [layout, setLayout] = useState<{ name: string, width: number, height: number, upperWrapper: string, lowerWrapper: string, playerWrapper: string, panelWrapper: string, playerHeight: number }>();
|
||||
const [layout, setLayout] = useState<{
|
||||
name: string;
|
||||
width: number;
|
||||
height: number;
|
||||
wrapper: string;
|
||||
upperWrapper: string;
|
||||
lowerWrapper: string;
|
||||
playerWrapper: string;
|
||||
panelWrapper: string;
|
||||
playerHeight: number;
|
||||
}>();
|
||||
|
||||
const [media, setMedia] = useState<AudioType | VideoType>(null);
|
||||
const [mediaProvider, setMediaProvider] = useState<HTMLAudioElement | null>(
|
||||
@@ -342,11 +364,19 @@ export const MediaPlayerProvider = ({
|
||||
|
||||
const calculateHeight = () => {
|
||||
if (window.innerHeight <= 1080) {
|
||||
setLayout({ ...LAYOUT.sm, width: window.innerWidth, height: window.innerHeight });
|
||||
setLayout({
|
||||
...LAYOUT.sm,
|
||||
width: window.innerWidth,
|
||||
height: window.innerHeight,
|
||||
});
|
||||
} else {
|
||||
setLayout({ ...LAYOUT.lg, width: window.innerWidth, height: window.innerHeight });
|
||||
setLayout({
|
||||
...LAYOUT.lg,
|
||||
width: window.innerWidth,
|
||||
height: window.innerHeight,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const deboundeCalculateHeight = debounce(calculateHeight, 100);
|
||||
|
||||
@@ -471,7 +501,7 @@ export const MediaPlayerProvider = ({
|
||||
if (wavesurfer) wavesurfer.destroy();
|
||||
setDecoded(false);
|
||||
setDecodeError(null);
|
||||
}
|
||||
};
|
||||
}, [media, ref, mediaProvider, layout?.playerHeight]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -479,12 +509,12 @@ export const MediaPlayerProvider = ({
|
||||
|
||||
EnjoyApp.window.onResize((event, bounds) => {
|
||||
deboundeCalculateHeight();
|
||||
})
|
||||
});
|
||||
|
||||
return () => {
|
||||
EnjoyApp.window.removeListeners();
|
||||
}
|
||||
}, [])
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -10,6 +10,7 @@ type ThemeProviderProps = {
|
||||
|
||||
type ThemeProviderState = {
|
||||
theme: Theme;
|
||||
colorScheme?: Omit<Theme, 'system'>;
|
||||
setTheme: (theme: Theme) => void;
|
||||
};
|
||||
|
||||
@@ -29,6 +30,7 @@ export function ThemeProvider({
|
||||
const [theme, setTheme] = useState<Theme>(
|
||||
() => (localStorage.getItem(storageKey) as Theme) || defaultTheme
|
||||
);
|
||||
const [colorScheme, setColorScheme] = useState<Omit<Theme, 'system'>>();
|
||||
|
||||
useEffect(() => {
|
||||
const root = window.document.documentElement;
|
||||
@@ -42,14 +44,17 @@ export function ThemeProvider({
|
||||
: "light";
|
||||
|
||||
root.classList.add(systemTheme);
|
||||
setColorScheme(systemTheme);
|
||||
return;
|
||||
}
|
||||
|
||||
setColorScheme(theme);
|
||||
root.classList.add(theme);
|
||||
}, [theme]);
|
||||
|
||||
const value = {
|
||||
theme,
|
||||
colorScheme,
|
||||
setTheme: (theme: Theme) => {
|
||||
localStorage.setItem(storageKey, theme);
|
||||
setTheme(theme);
|
||||
|
||||
@@ -14,7 +14,7 @@ export default () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<div className="bg-muted h-full px-4 lg:px-8 py-6">
|
||||
<div className="h-full px-4 lg:px-8 py-6">
|
||||
<div className="max-w-screen-md mx-auto mb-6">
|
||||
<div className="flex space-x-1 items-center mb-4">
|
||||
<Button variant="ghost" size="icon" onClick={() => navigate(-1)}>
|
||||
|
||||
@@ -131,7 +131,7 @@ export default () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-full px-4 py-6 lg:px-8 bg-muted flex flex-col">
|
||||
<div className="h-full px-4 py-6 lg:px-8 flex flex-col">
|
||||
<div className="w-full max-w-screen-md mx-auto flex-1">
|
||||
<div className="flex space-x-1 items-center mb-4">
|
||||
<Button variant="ghost" size="icon" onClick={() => navigate(-1)}>
|
||||
@@ -230,7 +230,7 @@ export default () => {
|
||||
{conversations.map((conversation) => (
|
||||
<Link key={conversation.id} to={`/conversations/${conversation.id}`}>
|
||||
<div
|
||||
className="bg-background text-muted-foreground rounded-full w-full mb-2 p-4 hover:bg-primary hover:text-muted cursor-pointer flex items-center"
|
||||
className="bg-muted text-muted-foreground rounded-full w-full mb-2 p-4 hover:bg-primary hover:text-muted cursor-pointer flex items-center"
|
||||
style={{
|
||||
borderLeftColor: `#${conversation.id
|
||||
.replaceAll("-", "")
|
||||
|
||||
@@ -38,7 +38,7 @@ export default () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-[100vh] bg-muted">
|
||||
<div className="h-[100vh]">
|
||||
<div className="max-w-screen-md mx-auto px-4 py-6">
|
||||
<div className="flex space-x-1 items-center mb-4">
|
||||
<Button variant="ghost" size="icon" onClick={() => navigate(-1)}>
|
||||
@@ -63,7 +63,7 @@ export default () => {
|
||||
>
|
||||
<ChevronLeftIcon className="w-5 h-5" />
|
||||
</Button>
|
||||
<div className="bg-background flex-1 h-5/6 border p-6 rounded-xl shadow-lg">
|
||||
<div className="bg-background flex-1 h-5/6 border p-6 rounded-xl shadow-xl">
|
||||
<MeaningMemorizingCard meaning={meanings[currentIndex]} />
|
||||
</div>
|
||||
<Button
|
||||
|
||||
Reference in New Issue
Block a user