diff --git a/packages/UI/package.json b/packages/UI/package.json index 8a9b7635..533d3c2a 100644 --- a/packages/UI/package.json +++ b/packages/UI/package.json @@ -11,6 +11,8 @@ "@radix-ui/react-separator": "^1.1.0", "@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-tooltip": "^1.1.4", + "@radix-ui/react-collapsible": "^1.1.1", + "@radix-ui/react-dropdown-menu": "^2.1.2", "@scribe/theia-utils": "0.0.1", "@svgr/webpack": "^8.1.0", "@tabler/icons-react": "^3.3.0", @@ -30,7 +32,10 @@ "npm-watch": "^0.13.0", "react-resizable-panels": "^2.0.19", "tailwind-merge": "^2.5.5", - "tailwindcss-animate": "^1.0.7" + "tailwindcss-animate": "^1.0.7", + "@types/md5": "^2.3.5", + "md5": "^2.3.0", + "usfm-grammar": "^3.0.0" }, "devDependencies": { "concurrently": "^8.2.2", diff --git a/packages/UI/src/browser/widgets/ChapterWidget.tsx b/packages/UI/src/browser/widgets/ChapterWidget.tsx new file mode 100644 index 00000000..b59fb96a --- /dev/null +++ b/packages/UI/src/browser/widgets/ChapterWidget.tsx @@ -0,0 +1,106 @@ +import * as React from "@theia/core/shared/react"; +import { + inject, + injectable, + postConstruct, +} from "@theia/core/shared/inversify"; +import { ReactWidget } from "@theia/core/lib/browser/widgets/react-widget"; +import { + AbstractViewContribution, + FrontendApplicationContribution, + FrontendApplication, +} from "@theia/core/lib/browser"; +import { FrontendApplicationStateService } from "@theia/core/lib/browser/frontend-application-state"; +import { WorkspaceService } from "@theia/workspace/lib/browser"; +import DraftbodySection from "../../components/DraftbodySection"; +import Button from "../../components/Button"; +import { IconPlus, IconX } from "@tabler/icons-react"; + +@injectable() +export class ChapterWidget extends ReactWidget { + static readonly ID = "Chapter-page-widget"; + static readonly LABER = "main-chapter"; + + @postConstruct() + protected init(): void { + this.doInit(); + } + + protected async doInit(): Promise { + this.id = ChapterWidget.ID; + this.title.label = ChapterWidget.LABER; + this.title.caption = ChapterWidget.LABER; + this.title.closable = true; + this.update(); + } + + render(): React.ReactNode { + return ( +
+
+
+
+
+ +
+ ); + } +} + +@injectable() +export class ChapterContribution + extends AbstractViewContribution + implements FrontendApplicationContribution +{ + @inject(FrontendApplicationStateService) + protected readonly stateService: FrontendApplicationStateService; + + @inject(WorkspaceService) + protected readonly workspaceService: WorkspaceService; + + constructor() { + super({ + widgetId: ChapterWidget.ID, + widgetName: ChapterWidget.LABER, + defaultWidgetOptions: { + area: "main", + }, + }); + } + + async onStart(app: FrontendApplication): Promise { + this.stateService.reachedState("ready").then(() => { + this.openView({ + activate: true, + reveal: true, + }); + }); + } +} diff --git a/packages/UI/src/browser/widgets/CreateNewProjectWidget.tsx b/packages/UI/src/browser/widgets/CreateNewProjectWidget.tsx new file mode 100644 index 00000000..102cc838 --- /dev/null +++ b/packages/UI/src/browser/widgets/CreateNewProjectWidget.tsx @@ -0,0 +1,375 @@ +import * as React from '@theia/core/shared/react'; +import { + inject, + injectable, + postConstruct, +} from '@theia/core/shared/inversify'; +import { ReactWidget } from '@theia/core/lib/browser/widgets/react-widget'; +import { + AbstractViewContribution, + FrontendApplicationContribution, + FrontendApplication, +} from '@theia/core/lib/browser'; +import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state'; +import { WorkspaceService } from '@theia/workspace/lib/browser'; +import CreateProjectComponents from '../../components/CreateProjectComponents'; +import { ProjectInitializer } from '../../functions/initializeNewProject'; +import { Dialog, DialogContent } from '../../components/ui/dialog'; +import { FolderOpen, Plus, PlusCircle, Search } from 'lucide-react'; +import { MessageService } from '@theia/core'; +import { FileService } from '@theia/filesystem/lib/browser/file-service'; +import { URI } from '@theia/core/lib/common/uri'; +import { + FileDialogService, + OpenFileDialogProps, +} from '@theia/filesystem/lib/browser'; + +interface ProjectDataType { + ProjectName: string; + Abbreviation: string; + Description: string; + Language: string; + ProjectFilePath: any; +} + +interface SettingsType { + scope: string; + versification: string; + license: string; +} + +interface WidgetState { + activeDropdown: boolean; + activeBooks: string; + settings: SettingsType; + projectData: ProjectDataType; + activePopUp: boolean; +} + +@injectable() +export class CreateNewProjectWidget extends ReactWidget { + static readonly ID = 'Create-NewProject-Widget'; + static readonly LABER = 'NewProject'; + + private state: WidgetState; + + @inject(ProjectInitializer) + private readonly projectInitializer: ProjectInitializer; + + @inject(WorkspaceService) + private readonly workspaceService: WorkspaceService; + + @inject(MessageService) + private readonly messageService: MessageService; + + @inject(FileService) + private readonly fileService: FileService; + + @inject(FileDialogService) + private readonly fileDialog: FileDialogService; + + constructor() { + super(); + this.state = { + activePopUp: false, + activeDropdown: false, + activeBooks: 'Bible translation', + settings: { + scope: 'All books', + versification: 'eng', + license: 'cc by-sa', + }, + projectData: { + ProjectName: '', + Abbreviation: '', + Description: '', + Language: 'english', + ProjectFilePath: '', + }, + }; + } + + @postConstruct() + protected init(): void { + this.doInit(); + } + + protected async doInit(): Promise { + this.id = CreateNewProjectWidget.ID; + this.title.label = CreateNewProjectWidget.LABER; + this.title.caption = CreateNewProjectWidget.LABER; + this.title.closable = true; + this.update(); + } + + setState(state: Partial): void { + this.state = { + ...this.state, + ...state, + }; + this.update(); + } + + handleSettingsChange = (key: keyof SettingsType, value: string) => { + this.setState({ + settings: { + ...this.state.settings, + [key]: value, + }, + }); + }; + + handleInputChange = (key: keyof ProjectDataType, value: any) => { + this.setState({ + projectData: { + ...this.state.projectData, + [key]: value, + }, + }); + }; + + validateForm = (): boolean => { + const requiredFields: (keyof ProjectDataType)[] = [ + 'ProjectName', + 'Abbreviation', + 'Description', + 'Language', + ]; + for (const field of requiredFields) { + if (!this.state.projectData[field]) { + alert(`${field} is required.`); + return false; + } + } + return true; + }; + + handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (!this.validateForm()) return; + + const ProjectData = { + projectName: this.state.projectData.ProjectName, + projectCategory: this.state.activeBooks, + userName: '', + abbreviation: this.state.projectData.Abbreviation, + sourceLanguage: this.state.projectData.Language, + targetLanguage: this.state.projectData.Language, + ProjectFilePath: this.state.projectData.ProjectFilePath, + }; + + const dataCreated = await this.projectInitializer.initializeNewProject( + ProjectData + ); + + if (dataCreated?.status === 200) { + // Open the project folder path, not just the parent folder + const projectPath = new URI( + this.state.projectData.ProjectFilePath + ).resolve(this.state.projectData.ProjectName); + await this.workspaceService.open(projectPath, { + preserveWindow: true, + }); + this.setState({ activePopUp: false }); + this.messageService.info('Project created successfully!'); + } + }; + + handleFileSelect = (file: File) => { + this.handleInputChange('ProjectFilePath', file); + }; + + private async selectFolder(): Promise { + try { + const props: OpenFileDialogProps = { + title: 'Select Project Folder', + canSelectFolders: true, + canSelectFiles: false, + }; + + const uri = await this.fileDialog.showOpenDialog(props); + + if (uri) { + const selectedFolderUri = new URI(uri.toString()); + // Just return the path without opening it + return selectedFolderUri.toString(); + } + return undefined; + } catch (error) { + this.messageService.error(`Failed to open folder dialog: ${error}`); + return undefined; + } + } + + handleFileClick = async () => { + // Always show folder selection first + const selectedFolder = await this.selectFolder(); + if (!selectedFolder) { + return; + } + + // Check if selected folder has a project + const folderUri = new URI(selectedFolder); + const projectFilePath = folderUri.resolve('metadata.json'); + const fileExists = await this.fileService.exists(projectFilePath); + + if (fileExists) { + const fileData = await this.fileService.readFile(projectFilePath); + const fileContent = fileData.value.toString(); + const metadata = JSON.parse(fileContent); + const projectName = metadata.projectName; + + const userChoice = await this.messageService.info( + `A project named "${projectName}" already exists in this folder. Do you want to continue with this project or select a different folder?`, + 'Continue', + 'Select Different' + ); + + if (userChoice === 'Select Different') { + // If user wants to select different folder, restart the process + this.handleFileClick(); + return; + } + // If user wants to continue with existing project, just open it + await this.workspaceService.open(folderUri, { + preserveWindow: true, + }); + return; + } + + // No project exists in selected folder, store the path and show create project dialog + this.handleInputChange('ProjectFilePath', selectedFolder); + this.setState({ activePopUp: true }); + }; + + render(): React.ReactNode { + return ( +
+
+ {/* Header */} +
+
+ + SCRIBE 2.0 +
+ +
+ + + +
+
+ + {/* Main Content */} +
+

+ Welcome to Scribe 3.0 +

+

Scripture editing made simple

+ +
+

+ What would you like to work on today? +

+
+ + +
+
+ +
+ + + +
+
+
+ this.setState({ activePopUp: open })} + > + {/* + + */} + + + this.setState({ activeDropdown: value }) + } + setActiveBooks={(value) => this.setState({ activeBooks: value })} + handleSettingsChange={this.handleSettingsChange} + handleInputChange={this.handleInputChange} + handleSubmit={this.handleSubmit} + onFileSelect={this.handleFileSelect} + /> + + +
+ ); + } +} + +@injectable() +export class CreateNewProjectContribution + extends AbstractViewContribution + implements FrontendApplicationContribution +{ + @inject(FrontendApplicationStateService) + protected readonly stateService: FrontendApplicationStateService; + + @inject(WorkspaceService) + protected readonly workspaceService: WorkspaceService; + + constructor() { + super({ + widgetId: CreateNewProjectWidget.ID, + widgetName: CreateNewProjectWidget.LABER, + defaultWidgetOptions: { + area: 'main', + }, + }); + } + + async onStart(app: FrontendApplication): Promise { + this.stateService.reachedState('ready').then(() => { + this.openView({ + activate: true, + reveal: true, + }); + }); + } +} diff --git a/packages/UI/src/browser/widgets/index.ts b/packages/UI/src/browser/widgets/index.ts index 7aa22c4c..37d9426e 100644 --- a/packages/UI/src/browser/widgets/index.ts +++ b/packages/UI/src/browser/widgets/index.ts @@ -2,36 +2,43 @@ import { FrontendApplicationContribution, WidgetFactory, bindViewContribution, -} from "@theia/core/lib/browser"; -import { interfaces } from "@theia/core/shared/inversify"; -import { AiSidebar, AiSidebarContribution } from "./AiSidebar"; +} from '@theia/core/lib/browser'; +import { interfaces } from '@theia/core/shared/inversify'; +import { AiSidebar, AiSidebarContribution } from './AiSidebar'; import { MainEditorLeftContribution, MainEditorLeftWidget, -} from "./MainEditorLeft"; -import { AudioWidget, AudioContribution } from "./AudioWidget"; +} from './MainEditorLeft'; +import { AudioWidget, AudioContribution } from './AudioWidget'; import { MainEditorRightContribution, MainEditorRightWidget, -} from "./MainEditorRight"; +} from './MainEditorRight'; import { BottomEditorLeft, BottomEditorLeftContribution, -} from "./BottomEditorLeft"; +} from './BottomEditorLeft'; import { BottomEditorRightContribution, BottomEditorRightWidget, -} from "./BottomEditorRight"; -import { ChatContribution, ChatWidget } from "./ChatWidget"; -import { VideoContribution, VideoWidget } from "./Videowidget"; -import { AudioPlayContribution, AudioPlayWidget } from "./AudioplayWidget"; +} from './BottomEditorRight'; +import { ChatContribution, ChatWidget } from './ChatWidget'; +import { VideoContribution, VideoWidget } from './Videowidget'; +import { AudioPlayContribution, AudioPlayWidget } from './AudioplayWidget'; import { CloudSyncCommandContribution, CloudSyncWidget, CloudSyncWidgetDialogProps, -} from "./cloud-sync-widget"; -import { CommandContribution } from "@theia/core"; -import { CloudSyncUtils } from "../../utils/CloudSyncUtils"; +} from './cloud-sync-widget'; +import { CommandContribution } from '@theia/core'; +import { CloudSyncUtils } from '../../utils/CloudSyncUtils'; +import { ChapterContribution, ChapterWidget } from './ChapterWidget'; +import { + CreateNewProjectContribution, + CreateNewProjectWidget, +} from './CreateNewProjectWidget'; +import { ProjectInitializer } from '../../functions/initializeNewProject'; +import { createVersificationUSFMClass } from '../../functions/createVersificationUSFM'; export const bindAllWidgetsContributions = (bind: interfaces.Bind) => { // sidebar widget binds @@ -94,6 +101,7 @@ export const bindAllWidgetsContributions = (bind: interfaces.Bind) => { })) .inSingletonScope(); + //Todo bind Project Initializer // Audio widget binds bindViewContribution(bind, AudioContribution); bind(FrontendApplicationContribution).toService(AudioContribution); @@ -140,11 +148,36 @@ export const bindAllWidgetsContributions = (bind: interfaces.Bind) => { .inSingletonScope(); bind(CloudSyncWidgetDialogProps).toConstantValue({ - title: "Cloud Sync", + title: 'Cloud Sync', }); bind(CloudSyncWidget).toSelf().inSingletonScope(); bind(CloudSyncCommandContribution).toSelf().inSingletonScope(); bind(CommandContribution).toService(CloudSyncCommandContribution); bind(CloudSyncUtils).toSelf().inSingletonScope(); + // CHAPTER WIDGET BINDS + bindViewContribution(bind, ChapterContribution); + bind(FrontendApplicationContribution).toService(ChapterContribution); + bind(ChapterWidget).toSelf(); + bind(WidgetFactory) + .toDynamicValue((context) => ({ + id: ChapterWidget.ID, + createWidget: () => context.container.get(ChapterWidget), + })) + .inSingletonScope(); + + // CHAPTER NEW PROJECT WIDGET + bindViewContribution(bind, CreateNewProjectContribution); + bind(FrontendApplicationContribution).toService(CreateNewProjectContribution); + bind(CreateNewProjectWidget).toSelf(); + bind(WidgetFactory) + .toDynamicValue((context) => ({ + id: CreateNewProjectWidget.ID, + createWidget: () => + context.container.get(CreateNewProjectWidget), + })) + .inSingletonScope(); + + bind(ProjectInitializer).toSelf().inSingletonScope(); + bind(createVersificationUSFMClass).toSelf().inSingletonScope(); }; diff --git a/packages/UI/src/components/AdvancedSettings.tsx b/packages/UI/src/components/AdvancedSettings.tsx new file mode 100644 index 00000000..336c43f1 --- /dev/null +++ b/packages/UI/src/components/AdvancedSettings.tsx @@ -0,0 +1,107 @@ +import React from "@theia/core/shared/react"; +import Button from "./Button"; +import { FileEditIcon } from "lucide-react"; + +export type SettingsType = { + scope: string; + versification: string; + license: string; +}; + +interface AdvancedSettingsProps { + settings: SettingsType; + onChange: (key: keyof SettingsType, value: string) => void; +} + +const AdvancedSettings: React.FC = ({ + settings, + onChange, +}) => { + const { scope, versification, license } = settings; + + const scopes = [ + "All books", + "old testment (ot)", + "new testment (nt)", + "custom", + ]; + + return ( +
+
+ {/* Scope Section */} +
+

scope *

+
+ 66 books +
+
+ +
+ {scopes.map((item) => ( +
+ + {/* Versification Section */} +
+

+ versification scheme* +

+ +
+ + {/* License Section */} +
+

License*

+
+ + console.log(e.target.files[0])} + /> + + +
+
+
+
+ ); +}; + +export default AdvancedSettings; diff --git a/packages/UI/src/components/Button.tsx b/packages/UI/src/components/Button.tsx index 6444bb27..6751e514 100644 --- a/packages/UI/src/components/Button.tsx +++ b/packages/UI/src/components/Button.tsx @@ -10,7 +10,7 @@ export default function Button({ label?: string; icon?: React.ReactNode; className?: string; - onClick?: () => void; + onClick?: (e?: any) => void; size?: "default" | "sm" | "lg" | "icon"; }) { return ( diff --git a/packages/UI/src/components/CreateProjectComponents.tsx b/packages/UI/src/components/CreateProjectComponents.tsx new file mode 100644 index 00000000..4071cdb3 --- /dev/null +++ b/packages/UI/src/components/CreateProjectComponents.tsx @@ -0,0 +1,253 @@ +import React from "@theia/core/shared/react"; +import AdvancedSettings from "./AdvancedSettings"; +import Button from "./Button"; +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from "../components/ui/Collapsible"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuTrigger, +} from "../components/ui/DropdownMenu"; +import { + ChevronRight, + PlusIcon, + Speaker, + BookOpen, + MenuSquare, + ImageIcon, + CircleAlert, + ChevronsUpDown, +} from "lucide-react"; +interface initialProjectData { + ProjectName: string; + Abbreviation: string; + Description: string; + Language: string; + ProjectFilePath: any; +} +const initialProjectData: initialProjectData = { + ProjectName: "", + Abbreviation: "", + Description: "", + Language: "english", + ProjectFilePath: "", +}; +interface ProjectSettings { + scope: string; + versification: string; + license: string; +} + +interface ProjectData { + ProjectName: string; + Abbreviation: string; + Description: string; + Language: string; + ProjectFilePath: any; +} + +interface CreateProjectProps { + activeDropdown: boolean; + activeBooks: string; + settings: ProjectSettings; + projectData: ProjectData; + + setActiveDropdown: (value: boolean) => void; + setActiveBooks: (value: string) => void; + handleSettingsChange: (key: keyof ProjectSettings, value: string) => void; + handleInputChange: (key: keyof ProjectData, value: any) => void; + handleSubmit: (e: React.FormEvent) => void; + onFileSelect: (file: File) => void; + + supportedLanguages?: Array<{ code: string; name: string }>; + projectTypes?: Array<{ label: string; icon: React.ComponentType }>; +} +const CreateProjectComponents = ({ + activeDropdown, + activeBooks, + settings, + projectData, + + setActiveDropdown, + setActiveBooks, + handleSettingsChange, + handleInputChange, + handleSubmit, + onFileSelect, + + supportedLanguages = [ + { code: "eng", name: "English" }, + { code: "fr", name: "French" }, + { code: "swh", name: "Swahili" }, + ], + projectTypes = [ + { label: "Bible translation", icon: MenuSquare }, + { label: "Audio", icon: Speaker }, + { label: "OBS", icon: ImageIcon }, + { label: "Juta", icon: BookOpen }, + ], +}: CreateProjectProps) => { + return ( +
+
+
+

+ Project Type :{" "} + {activeBooks} +

+ + +
+

{activeBooks}

+ +
+
+ +
+ {projectTypes.map(({ label, icon: Icon }) => ( +
setActiveBooks(label)} + className={`flex flex-col gap-2 items-center justify-center p-4 border border-zinc-900 hover:bg-cyan-900 hover:text-cyan-500 ${ + activeBooks === label ? "bg-cyan-950" : "bg-zinc-950" + }`} + > + +

{label}

+
+ ))} +
+
+
+
+ + {/* Form */} +
+
+ {[ + { + label: "Project Name*", + name: "ProjectName", + placeholder: "Enter Project Name", + }, + { label: "Abbreviation*", name: "Abbreviation", placeholder: "" }, + ].map(({ label, name, placeholder }) => ( +
+

{label}

+ + handleInputChange( + name as keyof typeof initialProjectData, + e.target.value + ) + } + type="text" + placeholder={placeholder} + className="w-full rounded-sm outline-none p-2 bg-zinc-900 text-[12px] text-gray-400" + /> +
+ ))} +
+ +
+
+

Description*

+