- Introduced foundational UI components (`Badge`, `LockCard`, `SectionHeader`, `AvatarIcon`, etc.) for flexible layouts and consistent design. - Added migration support with the `MigrationModal` component and backend integration for exporting/importing data between Electron and Tauri. - Extended form components with `TextAreaInput`, `OrderInput`, `ToggleField`, and `ToolbarSelect` for improved input handling. - Updated `ScribeShell` with migration popup logic to prompt users for data migration. - Integrated `AlertStack` for better alert handling and notification management. - Enhanced Rust/Tauri services with migration command implementations. - Added translations and styles for new components.
160 lines
8.2 KiB
TypeScript
160 lines
8.2 KiB
TypeScript
import React, {useContext, useState} from "react";
|
|
const logo = "/eritors-favicon-white.png";
|
|
import {ChapterProps} from "@/lib/types/chapter";
|
|
import {chapterVersions} from "@/lib/constants/chapter";
|
|
import {ChapterContext, ChapterContextProps} from "@/context/ChapterContext";
|
|
import {BookContext, BookContextProps} from "@/context/BookContext";
|
|
import {apiGet} from '@/lib/api/client';
|
|
import {isDesktop} from '@/lib/configs';
|
|
import * as tauri from '@/lib/tauri';
|
|
import OfflineContext, {OfflineContextType} from '@/context/OfflineContext';
|
|
import {getCookie, setCookie} from '@/lib/utils/cookies';
|
|
import {useRouter} from "@/lib/navigation";
|
|
import UserMenu from "@/components/layout/UserMenu";
|
|
import {Globe, Home, Settings} from "lucide-react";
|
|
import IconButton from "@/components/ui/IconButton";
|
|
import ToggleGroup from "@/components/ui/ToggleGroup";
|
|
import {SelectBoxProps} from "@/components/form/SelectBox";
|
|
import ToolbarSelect from "@/components/form/ToolbarSelect";
|
|
import {AlertContext, AlertContextProps} from "@/context/AlertContext";
|
|
import {SessionContext, SessionContextProps} from "@/context/SessionContext";
|
|
import {booksToSelectBox} from "@/lib/utils/book";
|
|
import BookSetting from "@/components/book/settings/BookSetting";
|
|
import {useTranslations} from '@/lib/i18n';
|
|
import {isSupportedLocale, LangContext, LangContextProps, SupportedLocale} from "@/context/LangContext";
|
|
import CreditCounter from "@/components/ui/CreditMeters";
|
|
import {getSubLevel, isAnthropicEnabled} from "@/lib/utils/quillsense";
|
|
import {BooksSyncContext, BooksSyncContextProps} from "@/context/BooksSyncContext";
|
|
import OfflineToggle from "@/components/offline/OfflineToggle";
|
|
|
|
export default function ScribeControllerBar() {
|
|
const {chapter, setChapter}: ChapterContextProps = useContext<ChapterContextProps>(ChapterContext);
|
|
const {book}: BookContextProps = useContext<BookContextProps>(BookContext);
|
|
const router = useRouter();
|
|
const {errorMessage}: AlertContextProps = useContext<AlertContextProps>(AlertContext);
|
|
const {session}: SessionContextProps = useContext<SessionContextProps>(SessionContext);
|
|
const t = useTranslations();
|
|
const {lang, setLang}: LangContextProps = useContext<LangContextProps>(LangContext)
|
|
const {serverSyncedBooks, serverOnlyBooks, localOnlyBooks}: BooksSyncContextProps = useContext<BooksSyncContextProps>(BooksSyncContext);
|
|
const {isCurrentlyOffline}: OfflineContextType = useContext<OfflineContextType>(OfflineContext);
|
|
|
|
const anthropicEnabled: boolean = !isCurrentlyOffline() && isAnthropicEnabled(session);
|
|
const isSubTierTwo: boolean = !isCurrentlyOffline() && getSubLevel(session) >= 2;
|
|
const hasAccess: boolean = anthropicEnabled || isSubTierTwo;
|
|
|
|
const [showSettingPanel, setShowSettingPanel] = useState<boolean>(false);
|
|
|
|
async function handleChapterVersionChanged(version: number) {
|
|
try {
|
|
let response: ChapterProps;
|
|
if (isDesktop && (isCurrentlyOffline() || book?.localBook)) {
|
|
response = await tauri.getWholeChapter(chapter?.chapterId ?? '', version, book?.bookId ?? '');
|
|
} else {
|
|
response = await apiGet<ChapterProps>(`chapter/whole`, session.accessToken, lang, {
|
|
bookid: book?.bookId,
|
|
id: chapter?.chapterId,
|
|
version: version,
|
|
});
|
|
}
|
|
if (!response) {
|
|
errorMessage(t("controllerBar.chapterNotFound"));
|
|
return;
|
|
}
|
|
setChapter(response);
|
|
} catch (e: unknown) {
|
|
if (e instanceof Error) {
|
|
errorMessage(e.message);
|
|
} else {
|
|
errorMessage(t("controllerBar.unknownChapterError"));
|
|
}
|
|
}
|
|
}
|
|
|
|
function handleBookNavigation(bookId: string): void {
|
|
router.push(`/book/${bookId}`);
|
|
}
|
|
|
|
function handleLanguageChange(language: SupportedLocale): void {
|
|
setCookie('lang', language, 365);
|
|
const newLang: string | null = getCookie('lang');
|
|
if (newLang && isSupportedLocale(newLang)) {
|
|
setLang(language);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div
|
|
className="relative flex items-center justify-between px-4 py-2 bg-tertiary">
|
|
{/* Gauche : Logo + contrôles */}
|
|
<div className="flex items-center space-x-3">
|
|
<div className="flex items-center space-x-2 pr-3 border-r border-secondary">
|
|
<img src={logo} alt={t("scribeTopBar.logoAlt")} width={24} height={24}/>
|
|
<span className="font-['ADLaM_Display'] text-sm tracking-wide text-text-primary">Scribe</span>
|
|
</div>
|
|
<div className="flex items-center gap-1">
|
|
{book && (
|
|
<IconButton icon={Settings} variant="ghost" shape="square"
|
|
onClick={(): void => setShowSettingPanel(true)}/>
|
|
)}
|
|
{book && (
|
|
<IconButton icon={Home} variant="ghost" shape="square"
|
|
onClick={(): void => router.push('/')}/>
|
|
)}
|
|
</div>
|
|
<ToolbarSelect onChangeCallBack={(e) => handleBookNavigation(e.target.value)}
|
|
data={booksToSelectBox([...(serverSyncedBooks ?? []), ...(serverOnlyBooks ?? []), ...(localOnlyBooks ?? [])])} defaultValue={book?.bookId}
|
|
placeholder={t("controllerBar.selectBook")}/>
|
|
{chapter && (
|
|
<ToolbarSelect onChangeCallBack={(e) => handleChapterVersionChanged(parseInt(e.target.value))}
|
|
data={chapterVersions.filter((version: SelectBoxProps): boolean => {
|
|
return !(version.value === '1' && (!hasAccess || book?.quillsenseEnabled === false));
|
|
}).map((version: SelectBoxProps) => {
|
|
return {
|
|
value: version.value.toString(),
|
|
label: t(version.label)
|
|
}
|
|
})} defaultValue={chapter?.chapterContent.version.toString()}/>
|
|
)}
|
|
</div>
|
|
{/* Centre : Titre du livre */}
|
|
{book && (
|
|
<div
|
|
className="absolute left-1/2 -translate-x-1/2 flex items-center bg-secondary px-4 py-1.5 rounded-lg border border-secondary">
|
|
<div className="h-4 w-0.5 bg-primary rounded-full mr-3"></div>
|
|
<div className="text-center">
|
|
<span className="text-text-primary font-semibold text-sm tracking-wide">
|
|
{book.title}
|
|
</span>
|
|
{book.subTitle && (
|
|
<span className="text-text-secondary text-xs italic ml-2">
|
|
{book.subTitle}
|
|
</span>
|
|
)}
|
|
</div>
|
|
<div className="h-4 w-0.5 bg-primary rounded-full ml-3"></div>
|
|
</div>
|
|
)}
|
|
{/* Droite : Crédits, offline, langue, user */}
|
|
<div className="flex items-center space-x-3">
|
|
{
|
|
hasAccess && book?.quillsenseEnabled !== false &&
|
|
<CreditCounter isCredit={isSubTierTwo}/>
|
|
}
|
|
{isDesktop && <OfflineToggle/>}
|
|
<ToggleGroup
|
|
options={[{value: 'fr', label: 'FR'}, {value: 'en', label: 'EN'}]}
|
|
value={lang}
|
|
onChange={function (value: string): void {
|
|
if (isSupportedLocale(value)) {
|
|
handleLanguageChange(value);
|
|
}
|
|
}}
|
|
icon={Globe}
|
|
size="md"
|
|
/>
|
|
<UserMenu/>
|
|
</div>
|
|
{showSettingPanel && <BookSetting onClose={() => setShowSettingPanel(false)}/>}
|
|
</div>
|
|
)
|
|
} |