From 7bd835c68ad1ee34b9da630a0ccbf752299a748f Mon Sep 17 00:00:00 2001 From: Ean Milligan Date: Thu, 23 Apr 2026 17:01:12 -0400 Subject: [PATCH] add db version patch to repo --- README.md | 6 +- db-version.patch | 318 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 323 insertions(+), 1 deletion(-) create mode 100644 db-version.patch diff --git a/README.md b/README.md index 0335e22..87cd70a 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/db-version.patch b/db-version.patch new file mode 100644 index 0000000..ba028f6 --- /dev/null +++ b/db-version.patch @@ -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 = (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({