diff --git a/README.md b/README.md index a0f7b70..405058c 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,22 @@ Features: 2. Group the character to accounts by clicking on the character name and setting the account name 3. Make sure your extractors are running! +## Backing up or moving character list to another device + +Because everything is stored in the browser there is no way to populate the list. To help with this EVE PI provides Backup and Restore functionality using basic JSON file. + +To dowload your list: + +1. Click Dowload button in the top button bar +2. Find your backup json file in your Downloads folder + +To restore your list: + +**Take note that restoring the list will overwrite any local list that you have!** + +1. Click Restore button in the top button bar +2. Use the dialog to select the file you previously dowloaded to restore the list. + ## Security All eve sso information is stored in your browser and refresh token is encrypted with apps EVE SSO secret. Backend processes only the token exchange, refresh and revoke that need the EVE_SSO_SECRET. Everything else is handled in frontend. diff --git a/src/app/components/Backup/DowloadButton.tsx b/src/app/components/Backup/DowloadButton.tsx index 5005750..3025230 100644 --- a/src/app/components/Backup/DowloadButton.tsx +++ b/src/app/components/Backup/DowloadButton.tsx @@ -1,4 +1,4 @@ -import { CharacterContext } from "@/app/context/Context"; +import { CharacterContext, SessionContext } from "@/app/context/Context"; import { Button } from "@mui/material"; import { useContext } from "react"; @@ -9,7 +9,7 @@ export const DowloadButton = () => { href={`data:text/json;charset=utf-8,${encodeURIComponent( JSON.stringify(characters) )}`} - download="pi-avanto-tk-characters.json" + download={`eve-pi-characters.json`} > Backup diff --git a/src/app/components/Backup/UploadButton.tsx b/src/app/components/Backup/UploadButton.tsx new file mode 100644 index 0000000..3255470 --- /dev/null +++ b/src/app/components/Backup/UploadButton.tsx @@ -0,0 +1,21 @@ +import { Box, Button } from "@mui/material"; +import { useState } from "react"; +import { UploadDialog } from "./UploadDialog"; + +export const UploadButton = () => { + const [uploadDialogOpen, setUploadDialogOpen] = useState(false); + return ( + + + setUploadDialogOpen(false)} + /> + + ); +}; diff --git a/src/app/components/Backup/UploadDialog.tsx b/src/app/components/Backup/UploadDialog.tsx new file mode 100644 index 0000000..6b5b55c --- /dev/null +++ b/src/app/components/Backup/UploadDialog.tsx @@ -0,0 +1,72 @@ +import DialogTitle from "@mui/material/DialogTitle"; +import Dialog from "@mui/material/Dialog"; +import Button from "@mui/material/Button"; +import { DialogActions, DialogContent, Typography } from "@mui/material"; +import { useContext, useEffect, useState } from "react"; +import { CharacterContext } from "@/app/context/Context"; +import { AccessToken } from "@/types"; + +export const UploadDialog = ({ + open, + closeDialog, +}: { + open: boolean; + closeDialog: () => void; +}) => { + const [file, setFile] = useState(); + + const fileReader = new FileReader(); + const { restoreCharacters } = useContext(CharacterContext); + + const error = new Error("Invalid input"); + + const validate = (characters: AccessToken[]) => { + characters.forEach((c) => { + if (!c.access_token) throw error; + if (!c.expires_at) throw error; + if (!c.refresh_token) throw error; + if (!c.character) throw error; + if (!c.character.characterId) throw error; + if (!c.character.name) throw error; + }); + + return characters; + }; + useEffect(() => { + if (file) { + fileReader.onload = (event) => { + if (!event || event.target === null) return; + const jsonOutput = event.target.result?.toString(); + if (jsonOutput) restoreCharacters(validate(JSON.parse(jsonOutput))); + closeDialog(); + }; + + fileReader.readAsText(file); + } + }, [file]); + + const changeHandler = (event: any) => { + setFile(event.target.files[0]); + }; + return ( + + Restore your character list + + + The list must be exported from the same EVE PI instance to work! + + + + + + + + + ); +}; diff --git a/src/app/components/MainGrid.tsx b/src/app/components/MainGrid.tsx index d4b785f..61c5336 100644 --- a/src/app/components/MainGrid.tsx +++ b/src/app/components/MainGrid.tsx @@ -7,6 +7,7 @@ import { CharacterContext } from "../context/Context"; import { DowloadButton } from "./Backup/DowloadButton"; import { DiscordButton } from "./Discord/DiscordButton"; import { GitHubButton } from "./Github/GitHubButton"; +import { UploadButton } from "./Backup/UploadButton"; interface Grouped { [key: string]: AccessToken[]; @@ -28,6 +29,7 @@ export const MainGrid = ({ sessionReady }: { sessionReady: boolean }) => { + diff --git a/src/app/context/Context.tsx b/src/app/context/Context.tsx index 0f00ac4..51af92a 100644 --- a/src/app/context/Context.tsx +++ b/src/app/context/Context.tsx @@ -5,10 +5,12 @@ export const CharacterContext = createContext<{ characters: AccessToken[]; deleteCharacter: (character: AccessToken) => void; updateCharacter: (character: AccessToken, update: CharacterUpdate) => void; + restoreCharacters: (characters: AccessToken[]) => void; }>({ characters: [], deleteCharacter: () => {}, updateCharacter: () => {}, + restoreCharacters: () => {}, }); export const SessionContext = createContext<{ diff --git a/src/app/page.tsx b/src/app/page.tsx index fc17d1b..38d5f1d 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -73,6 +73,10 @@ const Home = () => { return characters; }; + const restoreCharacters = (characters: AccessToken[]) => { + refreshSession(characters).then(saveCharacters).then(setCharacters); + }; + // Initialize EVE PI useEffect(() => { fetch("api/env") @@ -106,6 +110,7 @@ const Home = () => { characters, deleteCharacter, updateCharacter, + restoreCharacters, }} >