diff --git a/src/AboutDialog.tsx b/src/AboutDialog.tsx index a346146..b4ef320 100644 --- a/src/AboutDialog.tsx +++ b/src/AboutDialog.tsx @@ -30,6 +30,13 @@ export const AboutDialog: React.FC = (props) => { About +

+ XIVPlan+DB is a minimal mod to the original{' '} + XIVPlan, simply adding a + very basic database behind it to make more accessible share links. This minimal mod is open + source and can be found{' '} + here. +

XIVPlan is a tool for quickly diagramming raid strategies for Final Fantasy XIV, inspired by{' '} RaidPlan.io and{' '} diff --git a/src/App.tsx b/src/App.tsx index f3f05eb..fd4daf9 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -8,6 +8,7 @@ import { FileOpenPage } from './FileOpenPage'; import { HelpProvider } from './HelpProvider'; import { MainPage } from './MainPage'; import { SceneProvider } from './SceneProvider'; +import { SharePage } from './SharePage'; import { SiteHeader } from './SiteHeader'; import { ThemeProvider } from './ThemeProvider'; import { useFileLoaderDropTarget } from './useFileLoader'; @@ -106,6 +107,7 @@ const router = createBrowserRouter( }> } /> } /> + } /> , ), ); diff --git a/src/MainPage.tsx b/src/MainPage.tsx index de674da..8de51f1 100644 --- a/src/MainPage.tsx +++ b/src/MainPage.tsx @@ -55,7 +55,7 @@ const MainPageContent: React.FC = () => { ); }; -const TITLE = 'XIVPlan'; +const TITLE = 'XIVPlan+DB'; function usePageTitle() { const { source } = useScene(); @@ -64,6 +64,7 @@ function usePageTitle() { let title = TITLE; if (source) { title += ': '; + if (source.type === 'db' && source.folder) title += `${source.folder}/`; title += removeFileExtension(source?.name); } if (isDirty) { diff --git a/src/MainToolbar.tsx b/src/MainToolbar.tsx index a98a937..b4e9ff0 100644 --- a/src/MainToolbar.tsx +++ b/src/MainToolbar.tsx @@ -131,7 +131,8 @@ const SaveButton: React.FC = () => { if (!source) { setSaveAsOpen(true); } else if (isDirty) { - await saveFile(canonicalScene, source); + const success = await saveFile(canonicalScene, source); + if (!success) return; setSavedState(canonicalScene); } }; diff --git a/src/SceneProvider.tsx b/src/SceneProvider.tsx index c6641d3..a809e24 100644 --- a/src/SceneProvider.tsx +++ b/src/SceneProvider.tsx @@ -170,7 +170,16 @@ export interface BlobFileSource { file?: File; } -export type FileSource = LocalStorageFileSource | FileSystemFileSource | BlobFileSource; +export interface DBSource { + type: 'db'; + id: string; + name: string; + folder?: string; + userName?: string; + userPin?: string; +} + +export type FileSource = LocalStorageFileSource | FileSystemFileSource | BlobFileSource | DBSource; export interface EditorState { scene: Scene; diff --git a/src/SiteHeader.tsx b/src/SiteHeader.tsx index 79c8d56..80037fb 100644 --- a/src/SiteHeader.tsx +++ b/src/SiteHeader.tsx @@ -75,7 +75,7 @@ export const SiteHeader: React.FC> = ({ className, .

- XIVPlan + XIVPlan+DB {source && }
@@ -117,7 +117,10 @@ const SourceIndicator: React.FC = ({ source }) => { return ( - {removeFileExtension(source.name)} + + {source.type === 'db' && source.folder && `${source.folder}/`} + {removeFileExtension(source.name)} + {isDirty && } diff --git a/src/file.ts b/src/file.ts index 0adbbdc..9f74a6a 100644 --- a/src/file.ts +++ b/src/file.ts @@ -3,12 +3,13 @@ import { deflate, inflate } from 'pako'; import { FileSource } from './SceneProvider'; import { downloadScene, openFileBlob } from './file/blob'; +import { authUser, getPlans, openFromDB, saveNewToDB, saveToDB } from './file/db'; import { openFileFs, saveFileFs } from './file/filesystem'; import { openFileLocalStorage, saveFileLocalStorage } from './file/localStorage'; import { upgradeScene } from './file/upgrade'; import { Scene } from './scene'; -export async function saveFile(scene: Readonly, source: FileSource): Promise { +export async function saveFile(scene: Readonly, source: FileSource): Promise { switch (source.type) { case 'local': await saveFileLocalStorage(scene, source.name); @@ -21,6 +22,41 @@ export async function saveFile(scene: Readonly, source: FileSource): Prom case 'blob': downloadScene(scene, source.name); break; + + case 'db': { + if (!source.userName) { + source.userName = prompt('Please enter your username:') ?? undefined; + } + if (!source.userPin) { + source.userPin = prompt('Please enter your PIN:') ?? undefined; + } + if (!source.userName || !source.userPin) { + alert('Plan not saved, missing username or pin'); + throw new Error('user credentials not provided'); + } + + const tryAuthUser = await authUser({ userName: source.userName, userPin: source.userPin }); + if (!tryAuthUser.success) { + source.userName = ''; + source.userPin = ''; + alert('Plan not saved, Invalid credentials'); + throw new Error('user credentials invalid'); + } + + if (source.id) { + const myPlans = await getPlans(tryAuthUser.userId); + + if (!myPlans.success) { + alert('Plan not saved, Something went wrong'); + throw new Error("couldn't read plans"); + } + + if (myPlans.plans.filter((plan) => plan.id === source.id)) { + return await saveToDB(scene, source); + } + } + return await saveNewToDB(scene, source); + } } } @@ -42,6 +78,9 @@ async function openFileUnvalidated(source: FileSource) { throw new Error('File not set'); } return await openFileBlob(source.file); + + case 'db': + return (await openFromDB(source.id)) as Scene; } } diff --git a/src/file/FileDialog.tsx b/src/file/FileDialog.tsx index 0d52b22..c4c98b7 100644 --- a/src/file/FileDialog.tsx +++ b/src/file/FileDialog.tsx @@ -14,18 +14,20 @@ import React, { useState } from 'react'; import { OutPortal, createHtmlPortalNode } from 'react-reverse-portal'; import { HotkeyBlockingDialogBody } from '../HotkeyBlockingDialogBody'; import { TabActivity } from '../TabActivity'; +import { OpenDB, SaveDB } from './FileDialogDB'; import { FileSystemNotSupportedMessage, OpenFileSystem, SaveFileSystem } from './FileDialogFileSystem'; import { OpenLocalStorage, SaveLocalStorage } from './FileDialogLocalStorage'; import { ImportFromString } from './FileDialogShare'; import { supportsFs } from './filesystem'; -type Tabs = 'file' | 'localStorage' | 'import' | 'fileUnsupported'; +type Tabs = 'db' | 'file' | 'localStorage' | 'import' | 'fileUnsupported'; export type OpenDialogProps = Omit; +// TODO Add DB Tab export const OpenDialog: React.FC = (props) => { const classes = useStyles(); - const [tab, setTab] = useState(supportsFs ? 'file' : 'localStorage'); + const [tab, setTab] = useState('db'); const portalNode = createHtmlPortalNode({ attributes: { class: classes.actionsPortal } }); return ( @@ -40,11 +42,15 @@ export const OpenDialog: React.FC = (props) => { selectedValue={tab} onTabSelect={(ev, data) => setTab(data.value as Tabs)} > + Online DB {supportsFs && Local file} Browser storage Import plan link {!supportsFs && Local file} + + + @@ -71,7 +77,7 @@ export type SaveAsDialogProps = Omit; export const SaveAsDialog: React.FC = (props) => { const classes = useStyles(); - const [tab, setTab] = useState(supportsFs ? 'file' : 'localStorage'); + const [tab, setTab] = useState('db'); const portalNode = createHtmlPortalNode(); return ( @@ -86,10 +92,14 @@ export const SaveAsDialog: React.FC = (props) => { selectedValue={tab} onTabSelect={(ev, data) => setTab(data.value as Tabs)} > + Online DB {supportsFs && Local file} Browser storage {!supportsFs && Local file} + + + diff --git a/src/file/ShareDialogButton.tsx b/src/file/ShareDialogButton.tsx index fdac7a1..853e07c 100644 --- a/src/file/ShareDialogButton.tsx +++ b/src/file/ShareDialogButton.tsx @@ -17,7 +17,7 @@ import { CopyRegular, ShareRegular } from '@fluentui/react-icons'; import React, { ReactNode } from 'react'; import { CollapsableToolbarButton } from '../CollapsableToolbarButton'; import { HotkeyBlockingDialogBody } from '../HotkeyBlockingDialogBody'; -import { useScene } from '../SceneProvider'; +import { FileSource, useScene } from '../SceneProvider'; import { sceneToText } from '../file'; import { Scene } from '../scene'; import { DownloadButton } from './DownloadButton'; @@ -42,9 +42,9 @@ export const ShareDialogButton: React.FC = ({ children } const ShareDialogBody: React.FC = () => { const classes = useStyles(); - const { canonicalScene } = useScene(); + const { canonicalScene, source } = useScene(); const { dispatchToast } = useToastController(); - const url = getSceneUrl(canonicalScene); + const url = getSceneUrl(canonicalScene, source); const copyToClipboard = async () => { await navigator.clipboard.writeText(url); @@ -87,9 +87,13 @@ const CopySuccessToast = () => { ); }; -function getSceneUrl(scene: Scene) { +function getSceneUrl(scene: Scene, source: FileSource | undefined) { const data = sceneToText(scene); - return `${location.protocol}//${location.host}${location.pathname}#/plan/${data}`; + if (source && source.type === 'db') { + return `${location.protocol}//${location.host}${location.pathname}share#${source.id}`; + } else { + return `${location.protocol}//${location.host}${location.pathname}#/plan/${data}`; + } } const useStyles = makeStyles({ diff --git a/vite.config.ts b/vite.config.ts index eb719f3..8263ebe 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -22,6 +22,14 @@ function getEnvOptions(mode: string): UserConfig { export default defineConfig(({ mode }) => ({ ...getEnvOptions(mode), + server: { + proxy: { + '/api': { + target: 'http://localhost:14014', + changeOrigin: true, + }, + }, + }, plugins: [ react(), babel({