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:
an-lee
2024-04-11 16:03:17 +08:00
committed by GitHub
parent a3ceba97ea
commit ac39ccaf6f
32 changed files with 544 additions and 373 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

View File

@@ -7,7 +7,7 @@
"config": "tailwind.config.js",
"css": "src/index.css",
"baseColor": "zinc",
"cssVariables": false
"cssVariables": true
},
"aliases": {
"components": "src/renderer/components",

View File

@@ -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",

View File

@@ -254,6 +254,10 @@
"playOrPause": "播放/暂停",
"playOrPauseRecording": "播放/暂停录音",
"startOrStopRecording": "开始/结束录音",
"appearance": "外观",
"theme": "主题",
"light": "浅色",
"dark": "深色",
"about": "关于",
"currentVersion": "当前版本",
"checkUpdate": "检查更新",

View File

@@ -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,

View File

@@ -32,7 +32,7 @@ function App() {
});
return (
<ThemeProvider defaultTheme="light" storageKey="vite-ui-theme">
<ThemeProvider defaultTheme="system" storageKey="vite-ui-theme">
<AppSettingsProvider>
<HotKeysSettingsProvider>
<AISettingsProvider>

View File

@@ -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">

View File

@@ -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>

View File

@@ -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>;
},

View File

@@ -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

View File

@@ -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>

View File

@@ -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" && (

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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";

View File

@@ -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>

View File

@@ -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} />

View File

@@ -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>

View File

@@ -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>

View 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 />
</>
);
};

View File

@@ -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";

View File

@@ -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"),

View 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>
);
};

View File

@@ -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" && (

View File

@@ -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,

View File

@@ -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>

View File

@@ -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>

View File

@@ -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 (
<>

View File

@@ -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);

View File

@@ -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)}>

View File

@@ -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("-", "")

View File

@@ -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