add db version patch to repo
This commit is contained in:
@@ -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
318
db-version.patch
Normal 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({
|
||||
Reference in New Issue
Block a user