add db version patch to repo

This commit is contained in:
Ean Milligan
2026-04-23 17:01:12 -04:00
parent 8126e6dc95
commit 7bd835c68a
2 changed files with 323 additions and 1 deletions

View File

@@ -1,6 +1,10 @@
# XIVPlan+DB Mod Edition
This is just XIVPlan with a very basic DB implementation to make sharing links to a plan much more accessible. This repo contains the modification to XIVPlan as a git `.patch` file, and the rest of the source in this repo is the server that acts as an interface between XIVPlan and the DB.
This is just XIVPlan with a very basic DB implementation to make sharing links to a plan much more accessible. This repo contains the modification to XIVPlan as a git `db-version.patch` file, and the rest of the source in this repo is the server that acts as an interface between XIVPlan and the DB.
## Applying the patch
`git apply db-version.patch`
## Ideas around the mod

318
db-version.patch Normal file
View File

@@ -0,0 +1,318 @@
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<AboutDialogProps> = (props) => {
<HotkeyBlockingDialogBody>
<DialogTitle>About</DialogTitle>
<DialogContent className={classes.content}>
+ <p>
+ XIVPlan+DB is a minimal mod to the original{' '}
+ <ExternalLink href="https://xivplan.netlify.app/">XIVPlan</ExternalLink>, simply adding a
+ very basic database behind it to make more accessible share links. This minimal mod is open
+ source and can be found{' '}
+ <ExternalLink href="https://git.milligan.dev/xivdev/XIVPlan-DB">here</ExternalLink>.
+ </p>
<p>
XIVPlan is a tool for quickly diagramming raid strategies for Final Fantasy XIV, inspired by{' '}
<ExternalLink href="https://raidplan.io">RaidPlan.io</ExternalLink> 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(
<Route path="/" element={<Layout />}>
<Route index element={<MainPage />} />
<Route path="open" element={<FileOpenPage />} />
+ <Route path="share" element={<SharePage />} />
</Route>,
),
);
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<HTMLAttributes<HTMLElement>> = ({ className, .
<header className={mergeClasses(classes.root, className)} {...props}>
<div className={classes.title}>
<Text size={titleSize} weight="semibold">
- XIVPlan
+ XIVPlan+DB
</Text>
{source && <SourceIndicator source={source} />}
</div>
@@ -117,7 +117,10 @@ const SourceIndicator: React.FC<SourceIndicatorProps> = ({ source }) => {
return (
<Tooltip content={tooltip} relationship="description">
<span className={classes.source}>
- <Text className={classes.filename}>{removeFileExtension(source.name)}</Text>
+ <Text className={classes.filename}>
+ {source.type === 'db' && source.folder && `${source.folder}/`}
+ {removeFileExtension(source.name)}
+ </Text>
{isDirty && <Text className={classes.dirty}>●</Text>}
</span>
</Tooltip>
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<Scene>, source: FileSource): Promise<void> {
+export async function saveFile(scene: Readonly<Scene>, source: FileSource): Promise<void | boolean> {
switch (source.type) {
case 'local':
await saveFileLocalStorage(scene, source.name);
@@ -21,6 +22,41 @@ export async function saveFile(scene: Readonly<Scene>, 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<DialogProps, 'children'>;
+// TODO Add DB Tab
export const OpenDialog: React.FC<OpenDialogProps> = (props) => {
const classes = useStyles();
- const [tab, setTab] = useState<Tabs>(supportsFs ? 'file' : 'localStorage');
+ const [tab, setTab] = useState<Tabs>('db');
const portalNode = createHtmlPortalNode({ attributes: { class: classes.actionsPortal } });
return (
@@ -40,11 +42,15 @@ export const OpenDialog: React.FC<OpenDialogProps> = (props) => {
selectedValue={tab}
onTabSelect={(ev, data) => setTab(data.value as Tabs)}
>
+ <Tab value="db">Online DB</Tab>
{supportsFs && <Tab value="file">Local file</Tab>}
<Tab value="localStorage">Browser storage</Tab>
<Tab value="import">Import plan link</Tab>
{!supportsFs && <Tab value="fileUnsupported">Local file</Tab>}
</TabList>
+ <TabActivity value="db" activeTab={tab}>
+ <OpenDB actions={portalNode} />
+ </TabActivity>
<TabActivity value="file" activeTab={tab}>
<OpenFileSystem actions={portalNode} />
</TabActivity>
@@ -71,7 +77,7 @@ export type SaveAsDialogProps = Omit<DialogProps, 'children'>;
export const SaveAsDialog: React.FC<SaveAsDialogProps> = (props) => {
const classes = useStyles();
- const [tab, setTab] = useState<Tabs>(supportsFs ? 'file' : 'localStorage');
+ const [tab, setTab] = useState<Tabs>('db');
const portalNode = createHtmlPortalNode();
return (
@@ -86,10 +92,14 @@ export const SaveAsDialog: React.FC<SaveAsDialogProps> = (props) => {
selectedValue={tab}
onTabSelect={(ev, data) => setTab(data.value as Tabs)}
>
+ <Tab value="db">Online DB</Tab>
{supportsFs && <Tab value="file">Local file</Tab>}
<Tab value="localStorage">Browser storage</Tab>
{!supportsFs && <Tab value="fileUnsupported">Local file</Tab>}
</TabList>
+ <TabActivity value="db" activeTab={tab}>
+ <SaveDB actions={portalNode} />
+ </TabActivity>
<TabActivity value="file" activeTab={tab}>
<SaveFileSystem actions={portalNode} />
</TabActivity>
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<ShareDialogButtonProps> = ({ 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({