forked from Cookies_Github_mirror/AquaDX
AquaTrans and stuff (#131)
This commit is contained in:
@@ -11,6 +11,7 @@
|
||||
import { pfp, tooltip } from "./libs/ui"
|
||||
import { ANNOUNCEMENT } from "./libs/config";
|
||||
import { t } from "./libs/i18n";
|
||||
import Transfer from "./pages/Transfer/Transfer.svelte";
|
||||
|
||||
console.log(`%c
|
||||
┏━┓ ┳━┓━┓┏━
|
||||
@@ -76,6 +77,7 @@
|
||||
<Route path="/u/:username/:game" component={UserHome} />
|
||||
<Route path="/settings" component={Settings} />
|
||||
<Route path="/pictures" component={MaiPhoto} />
|
||||
<Route path="/transfer" component={Transfer} />
|
||||
</Router>
|
||||
|
||||
<style lang="sass">
|
||||
|
||||
@@ -12,6 +12,11 @@
|
||||
export let confirm: ConfirmProps | null = null
|
||||
export let error: string | null
|
||||
export let loading: boolean = false
|
||||
|
||||
function doConfirm(fn?: () => void) {
|
||||
confirm = null
|
||||
fn && fn()
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if confirm}
|
||||
@@ -22,15 +27,9 @@
|
||||
|
||||
<div class="actions">
|
||||
{#if confirm.cancel}
|
||||
<!-- Svelte LSP is very annoying here -->
|
||||
<button on:click={() => {
|
||||
confirm && confirm.cancel && confirm.cancel()
|
||||
|
||||
// Set to null
|
||||
confirm = null
|
||||
}}>{t('action.cancel')}</button>
|
||||
<button on:click={() => doConfirm(confirm?.cancel)}>{t('action.cancel')}</button>
|
||||
{/if}
|
||||
<button on:click={() => confirm && confirm.confirm()} class:error={confirm.dangerous}>{t('action.confirm')}</button>
|
||||
<button on:click={() => doConfirm(confirm?.confirm)} class:error={confirm.dangerous}>{t('action.confirm')}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import StatusOverlays from "../StatusOverlays.svelte";
|
||||
import { GAME } from "../../libs/sdk";
|
||||
import GameSettingFields from "./GameSettingFields.svelte";
|
||||
import { download } from "../../libs/ui";
|
||||
|
||||
const profileFields = [
|
||||
['name', t('settings.mai2.name')],
|
||||
@@ -42,15 +43,6 @@
|
||||
.catch(e => error = e.message)
|
||||
.finally(() => submitting = "")
|
||||
}
|
||||
|
||||
function download(data: string, filename: string) {
|
||||
const blob = new Blob([data]);
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = filename;
|
||||
link.click();
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="fields" out:fade={FADE_OUT} in:fade={FADE_IN}>
|
||||
|
||||
@@ -4,13 +4,16 @@
|
||||
import { DISCORD_INVITE } from "../../libs/config";
|
||||
|
||||
export let error: string;
|
||||
export let expected: boolean = false;
|
||||
</script>
|
||||
|
||||
<div class="overlay" transition:fade>
|
||||
<div>
|
||||
<h2 class="error">{t('status.error')}</h2>
|
||||
<span>{t('status.error.hint')}<a href={DISCORD_INVITE}>{t('status.error.hint.link')}</a></span>
|
||||
<span>{t('status.detail', { detail: error })}</span>
|
||||
{#if !expected}
|
||||
<span>{t('status.error.hint')}<a href={DISCORD_INVITE}>{t('status.error.hint.link')}</a></span>
|
||||
{/if}
|
||||
<span class="detail">{error}</span>
|
||||
|
||||
<div class="actions">
|
||||
<button on:click={() => location.reload()} class="error">
|
||||
@@ -27,4 +30,10 @@
|
||||
|
||||
button
|
||||
width: 100%
|
||||
|
||||
.detail
|
||||
white-space: pre-line
|
||||
font-size: 0.9em
|
||||
line-height: 1.2
|
||||
opacity: 0.8
|
||||
</style>
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
export type Dict = Record<string, any>
|
||||
|
||||
export interface TrendEntry {
|
||||
date: string
|
||||
rating: number
|
||||
@@ -48,7 +50,7 @@ export interface CardSummary {
|
||||
export interface ConfirmProps {
|
||||
title: string
|
||||
message: string
|
||||
confirm: () => void
|
||||
confirm?: () => void
|
||||
cancel?: () => void
|
||||
dangerous?: boolean
|
||||
}
|
||||
|
||||
@@ -8,13 +8,14 @@ import type {
|
||||
TrendEntry,
|
||||
AquaNetUser, GameOption,
|
||||
UserBox,
|
||||
UserItem
|
||||
UserItem,
|
||||
Dict
|
||||
} from './generalTypes'
|
||||
import type { GameName } from './scoring'
|
||||
|
||||
interface RequestInitWithParams extends RequestInit {
|
||||
interface ExtReqInit extends RequestInit {
|
||||
params?: { [index: string]: string }
|
||||
localCache?: boolean
|
||||
json?: any
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -37,188 +38,113 @@ export function reconstructUrl(input: URL | RequestInfo, callback: (url: URL) =>
|
||||
/**
|
||||
* Fetch with url parameters
|
||||
*/
|
||||
export function fetchWithParams(input: URL | RequestInfo, init?: RequestInitWithParams): Promise<Response> {
|
||||
export function fetchWithParams(input: URL | RequestInfo, init?: ExtReqInit): Promise<Response> {
|
||||
return fetch(reconstructUrl(input, u => {
|
||||
u.search = new URLSearchParams(init?.params ?? {}).toString()
|
||||
}), init)
|
||||
}
|
||||
|
||||
const cache: { [index: string]: any } = {}
|
||||
/**
|
||||
* Do something with the response when it's not ok
|
||||
*
|
||||
* @param res Response object
|
||||
*/
|
||||
async function ensureOk(res: Response) {
|
||||
if (!res.ok) {
|
||||
const text = await res.text()
|
||||
console.error(`${res.status}: ${text}`)
|
||||
|
||||
export async function post(endpoint: string, params: Record<string, any> = {}, init?: RequestInitWithParams): Promise<any> {
|
||||
// If 400 invalid token is caught, should invalidate the token and redirect to signin
|
||||
if (text === 'Invalid token') {
|
||||
localStorage.removeItem('token')
|
||||
window.location.href = '/'
|
||||
}
|
||||
|
||||
// Try to parse as json
|
||||
let json
|
||||
try {
|
||||
json = JSON.parse(text)
|
||||
} catch (e) {
|
||||
throw new Error(text)
|
||||
}
|
||||
if (json.error) throw new Error(json.error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Post to an endpoint and return the response in JSON while doing error checks
|
||||
* and handling token (and token expiry) automatically.
|
||||
*
|
||||
* @param endpoint The endpoint to post to (e.g., '/pull')
|
||||
* @param params An object containing the request body or any necessary parameters
|
||||
* @param init Additional fetch/init configuration
|
||||
* @returns The JSON response from the server
|
||||
*/
|
||||
export async function post(endpoint: string, params: Dict = {}, init?: ExtReqInit): Promise<any> {
|
||||
return postHelper(endpoint, params, init).then(it => it.json())
|
||||
}
|
||||
|
||||
/**
|
||||
* Actual impl of post(). This does not return JSON but returns response object.
|
||||
*/
|
||||
async function postHelper(endpoint: string, params: Dict = {}, init?: ExtReqInit): Promise<any> {
|
||||
// Add token if exists
|
||||
const token = localStorage.getItem('token')
|
||||
if (token && !('token' in params)) params = { ...(params ?? {}), token }
|
||||
|
||||
if (init?.localCache) {
|
||||
const cached = cache[endpoint + JSON.stringify(params) + JSON.stringify(init)]
|
||||
if (cached) return cached
|
||||
if (init?.json) {
|
||||
init.body = JSON.stringify(init.json)
|
||||
init.headers = { 'Content-Type': 'application/json', ...init.headers }
|
||||
init.json = undefined
|
||||
}
|
||||
|
||||
const res = await fetchWithParams(AQUA_HOST + endpoint, {
|
||||
method: 'POST',
|
||||
params,
|
||||
...init
|
||||
}).catch(e => {
|
||||
console.error(e)
|
||||
throw new Error('Network error')
|
||||
})
|
||||
const res = await fetchWithParams(AQUA_HOST + endpoint, { method: 'POST', params, ...init })
|
||||
.catch(e => { console.error(e); throw new Error("Network error") })
|
||||
await ensureOk(res)
|
||||
|
||||
if (!res.ok) {
|
||||
const text = await res.text()
|
||||
console.error(`${res.status}: ${text}`)
|
||||
|
||||
// If 400 invalid token is caught, should invalidate the token and redirect to signin
|
||||
if (text === 'Invalid token') {
|
||||
localStorage.removeItem('token')
|
||||
window.location.href = '/'
|
||||
}
|
||||
|
||||
// Try to parse as json
|
||||
let json
|
||||
try {
|
||||
json = JSON.parse(text)
|
||||
} catch (e) {
|
||||
throw new Error(text)
|
||||
}
|
||||
if (json.error) throw new Error(json.error)
|
||||
}
|
||||
|
||||
const ret = res.json()
|
||||
cache[endpoint + JSON.stringify(params) + JSON.stringify(init)] = ret
|
||||
|
||||
return ret
|
||||
return res
|
||||
}
|
||||
|
||||
export async function get(endpoint: string, params:any,init?: RequestInitWithParams): Promise<any> {
|
||||
// Add token if exists
|
||||
const token = localStorage.getItem('token')
|
||||
if (token && !('token' in params)) params = { ...(params ?? {}), token }
|
||||
const decoder = new TextDecoder()
|
||||
|
||||
if (init?.localCache) {
|
||||
const cached = cache[endpoint + JSON.stringify(init)]
|
||||
if (cached) return cached
|
||||
/**
|
||||
* Post with a stream response. Similar to post(), but the response will stream messages to onChunk.
|
||||
*/
|
||||
export async function postStream(endpoint: string, params: Dict = {}, onChunk: (data: any) => void, init?: ExtReqInit): Promise<void> {
|
||||
const res = await postHelper(endpoint, params, init)
|
||||
if (!res.body) {
|
||||
console.error('Response body is not a stream')
|
||||
return
|
||||
}
|
||||
|
||||
const res = await fetchWithParams(AQUA_HOST + endpoint, {
|
||||
method: 'GET',
|
||||
params,
|
||||
...init
|
||||
}).catch(e => {
|
||||
console.error(e)
|
||||
throw new Error('Network error')
|
||||
})
|
||||
// The response body is a ReadableStream. We'll read chunks as they arrive.
|
||||
const reader = res.body?.getReader()
|
||||
if (!reader) return
|
||||
let buffer = ''
|
||||
|
||||
if (!res.ok) {
|
||||
const text = await res.text()
|
||||
console.error(`${res.status}: ${text}`)
|
||||
try {
|
||||
while (true) {
|
||||
const { done, value } = await reader.read()
|
||||
if (done) break
|
||||
|
||||
// If 400 invalid token is caught, should invalidate the token and redirect to signin
|
||||
if (text === 'Invalid token') {
|
||||
localStorage.removeItem('token')
|
||||
window.location.href = '/'
|
||||
// Decode any new data, parse full lines, keep the rest in buffer
|
||||
buffer += decoder.decode(value, { stream: true })
|
||||
let fullLines = buffer.split('\n')
|
||||
buffer = fullLines.pop() ?? ''
|
||||
|
||||
for (const line of fullLines) {
|
||||
if (!line.trim()) continue // skip empty lines
|
||||
onChunk(JSON.parse(line))
|
||||
}
|
||||
}
|
||||
|
||||
// Try to parse as json
|
||||
let json
|
||||
try {
|
||||
json = JSON.parse(text)
|
||||
} catch (e) {
|
||||
throw new Error(text)
|
||||
}
|
||||
if (json.error) throw new Error(json.error)
|
||||
// If there's leftover data in 'buffer' after stream ends, parse
|
||||
if (buffer.trim())
|
||||
onChunk(JSON.parse(buffer.trim()))
|
||||
|
||||
} finally {
|
||||
reader.releaseLock()
|
||||
}
|
||||
|
||||
const ret = res.json()
|
||||
cache[endpoint + JSON.stringify(init)] = ret
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
export async function put(endpoint: string, params: any, init?: RequestInitWithParams): Promise<any> {
|
||||
// Add token if exists
|
||||
const token = localStorage.getItem('token')
|
||||
if (token && !('token' in params)) params = { ...(params ?? {}), token }
|
||||
|
||||
if (init?.localCache) {
|
||||
const cached = cache[endpoint + JSON.stringify(params) + JSON.stringify(init)]
|
||||
if (cached) return cached
|
||||
}
|
||||
|
||||
const res = await fetchWithParams(AQUA_HOST + endpoint, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(params),
|
||||
headers:{
|
||||
'Content-Type':'application/json',
|
||||
...init?.headers
|
||||
},
|
||||
...init
|
||||
}).catch(e => {
|
||||
console.error(e)
|
||||
throw new Error('Network error')
|
||||
})
|
||||
|
||||
if (!res.ok) {
|
||||
const text = await res.text()
|
||||
console.error(`${res.status}: ${text}`)
|
||||
|
||||
// If 400 invalid token is caught, should invalidate the token and redirect to signin
|
||||
if (text === 'Invalid token') {
|
||||
localStorage.removeItem('token')
|
||||
window.location.href = '/'
|
||||
}
|
||||
|
||||
// Try to parse as json
|
||||
let json
|
||||
try {
|
||||
json = JSON.parse(text)
|
||||
} catch (e) {
|
||||
throw new Error(text)
|
||||
}
|
||||
if (json.error) throw new Error(json.error)
|
||||
}
|
||||
|
||||
const ret = res.json()
|
||||
cache[endpoint + JSON.stringify(params) + JSON.stringify(init)] = ret
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
export async function realPost(endpoint: string, params: any, init?: RequestInitWithParams): Promise<any> {
|
||||
const res = await fetchWithParams(AQUA_HOST + endpoint, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(params),
|
||||
headers:{
|
||||
'Content-Type':'application/json',
|
||||
...init?.headers
|
||||
},
|
||||
...init
|
||||
}).catch(e => {
|
||||
console.error(e)
|
||||
throw new Error('Network error')
|
||||
})
|
||||
|
||||
if (!res.ok) {
|
||||
const text = await res.text()
|
||||
console.error(`${res.status}: ${text}`)
|
||||
|
||||
// If 400 invalid token is caught, should invalidate the token and redirect to signin
|
||||
if (text === 'Invalid token') {
|
||||
localStorage.removeItem('token')
|
||||
window.location.href = '/'
|
||||
}
|
||||
|
||||
// Try to parse as json
|
||||
let json
|
||||
try {
|
||||
json = JSON.parse(text)
|
||||
} catch (e) {
|
||||
throw new Error(text)
|
||||
}
|
||||
if (json.error) throw new Error(json.error)
|
||||
}
|
||||
|
||||
return res.json()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -229,6 +155,7 @@ export async function realPost(endpoint: string, params: any, init?: RequestInit
|
||||
async function register(user: { username: string, email: string, password: string, turnstile: string }) {
|
||||
return await post('/api/v2/user/register', user)
|
||||
}
|
||||
|
||||
async function login(user: { email: string, password: string, turnstile: string }) {
|
||||
const data = await post('/api/v2/user/login', user)
|
||||
|
||||
@@ -263,11 +190,11 @@ export const USER = {
|
||||
|
||||
export const USERBOX = {
|
||||
getProfile: (): Promise<{ user: UserBox, items: UserItem[] }> =>
|
||||
get('/api/v2/game/chu3/user-box', {}),
|
||||
post('/api/v2/game/chu3/user-box', {}),
|
||||
setUserBox: (d: { field: string, value: number | string }) =>
|
||||
post(`/api/v2/game/chu3/user-detail-set`, d),
|
||||
getUserProfile: (username: string): Promise<UserBox> =>
|
||||
get(`/api/v2/game/chu3/user-detail`, {username})
|
||||
post(`/api/v2/game/chu3/user-detail`, {username})
|
||||
}
|
||||
|
||||
export const CARD = {
|
||||
@@ -295,9 +222,9 @@ export const GAME = {
|
||||
export: (game: GameName): Promise<Record<string, any>> =>
|
||||
post(`/api/v2/game/${game}/export`),
|
||||
import: (game: GameName, data: any): Promise<Record<string, any>> =>
|
||||
post(`/api/v2/game/${game}/import`, {}, { body: JSON.stringify(data) }),
|
||||
post(`/api/v2/game/${game}/import`, {}, { json: data }),
|
||||
importMusicDetail: (game: GameName, data: any): Promise<Record<string, any>> =>
|
||||
post(`/api/v2/game/${game}/import-music-detail`, {}, {body: JSON.stringify(data), headers: {'Content-Type': 'application/json'}}),
|
||||
post(`/api/v2/game/${game}/import-music-detail`, {}, { json: data }),
|
||||
setRival: (game: GameName, rivalUserName: string, isAdd: boolean) =>
|
||||
post(`/api/v2/game/${game}/set-rival`, { rivalUserName, isAdd }),
|
||||
}
|
||||
@@ -317,3 +244,12 @@ export const SETTING = {
|
||||
detailSet: (game: string, field: string, value: any) =>
|
||||
post(`/api/v2/game/${game}/user-detail-set`, { field, value }),
|
||||
}
|
||||
|
||||
export const TRANSFER = {
|
||||
check: (d: AllNetClient): Promise<TrCheckGood> =>
|
||||
post('/api/v2/transfer/check', {}, { json: d }),
|
||||
pull: (d: AllNetClient, callback: (data: TrStreamMessage) => void) =>
|
||||
postStream('/api/v2/transfer/pull', {}, callback, { json: d }),
|
||||
push: (d: AllNetClient, data: string) =>
|
||||
post('/api/v2/transfer/push', {}, { json: { client: d, data } }),
|
||||
}
|
||||
|
||||
@@ -212,3 +212,53 @@ export function pfp(node: HTMLImageElement, me?: AquaNetUser) {
|
||||
node.src = me?.profilePicture ? `${AQUA_HOST}/uploads/net/portrait/${me.profilePicture}` : DEFAULT_PFP
|
||||
node.onerror = e => pfpNotFound(e as Event)
|
||||
}
|
||||
|
||||
export function download(data: string, filename: string) {
|
||||
const blob = new Blob([data]);
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = filename;
|
||||
link.click();
|
||||
}
|
||||
|
||||
export async function selectJsonFile(): Promise<any> {
|
||||
return new Promise((resolve, reject) => {
|
||||
// Create a hidden file input element
|
||||
const input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.accept = '.json,application/json';
|
||||
input.style.display = 'none';
|
||||
|
||||
// Listen for when the user selects a file
|
||||
input.addEventListener('change', (event: Event) => {
|
||||
const target = event.target as HTMLInputElement;
|
||||
if (!target.files || target.files.length === 0) {
|
||||
return reject(new Error("No file selected"));
|
||||
}
|
||||
const file = target.files[0];
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onload = () => {
|
||||
try {
|
||||
const jsonData = JSON.parse(reader.result as string);
|
||||
resolve(jsonData);
|
||||
} catch (error) {
|
||||
reject(new Error("Error parsing JSON: " + error));
|
||||
}
|
||||
};
|
||||
|
||||
reader.onerror = () => {
|
||||
reject(new Error("Error reading file"));
|
||||
};
|
||||
|
||||
reader.readAsText(file);
|
||||
});
|
||||
|
||||
// Append the input to the DOM, trigger click, and then remove it
|
||||
document.body.appendChild(input);
|
||||
input.click();
|
||||
document.body.removeChild(input);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
26
AquaNet/src/pages/Transfer/InputTextShort.svelte
Normal file
26
AquaNet/src/pages/Transfer/InputTextShort.svelte
Normal file
@@ -0,0 +1,26 @@
|
||||
<script lang="ts">
|
||||
export let desc: string
|
||||
export let value: string
|
||||
export let placeholder: string
|
||||
export let flex: number = 60
|
||||
|
||||
export let disabled: boolean = false
|
||||
|
||||
export let validate: (value: string) => boolean = () => true
|
||||
</script>
|
||||
|
||||
<div class="field" style="flex: {flex}">
|
||||
<label for={desc}>{desc}</label>
|
||||
<input type="text" placeholder={placeholder} bind:value={value} id="{desc}" on:change
|
||||
class:error={value && !validate(value)} {disabled}/>
|
||||
</div>
|
||||
|
||||
<style lang="sass">
|
||||
.field
|
||||
display: inline-flex
|
||||
flex-direction: column
|
||||
gap: 0.5rem
|
||||
|
||||
label
|
||||
font-weight: bold
|
||||
</style>
|
||||
129
AquaNet/src/pages/Transfer/Transfer.svelte
Normal file
129
AquaNet/src/pages/Transfer/Transfer.svelte
Normal file
@@ -0,0 +1,129 @@
|
||||
<script lang="ts">
|
||||
import { slide } from "svelte/transition";
|
||||
import { t, ts } from "../../libs/i18n";
|
||||
import TransferServer from "./TransferServer.svelte";
|
||||
import { DATA_HOST } from "../../libs/config";
|
||||
import type { ConfirmProps } from "../../libs/generalTypes";
|
||||
import StatusOverlays from "../../components/StatusOverlays.svelte";
|
||||
|
||||
|
||||
let tabs = ['chu3', 'mai2', 'ongeki']
|
||||
let game: Record<string, { game: string, version: string }> = {
|
||||
'chu3': { game: "SDHD", version: "2.30" },
|
||||
'mai2': { game: "SDGA", version: "1.50" },
|
||||
'ongeki': { game: "SDDT", version: "1.45" }
|
||||
}
|
||||
let tab = 0
|
||||
|
||||
let src = JSON.parse(localStorage.getItem('src') ?? `{"dns": "", "card": "", "keychip": ""}`)
|
||||
let dst = JSON.parse(localStorage.getItem('dst') ?? `{"dns": "", "card": "", "keychip": ""}`)
|
||||
let [srcTested, dstTested] = [false, false]
|
||||
let gameInfo = JSON.parse(localStorage.getItem('gameInfo') ?? `{"game": "", "version": ""}`)
|
||||
|
||||
let srcEl: TransferServer, dstEl: TransferServer
|
||||
let srcExportedData: string
|
||||
let [error, loading] = ["", false]
|
||||
let confirm: ConfirmProps | null = null
|
||||
|
||||
function defaultGame() {
|
||||
gameInfo.game = game[tabs[tab]].game
|
||||
gameInfo.version = game[tabs[tab]].version
|
||||
}
|
||||
|
||||
function onChange() {
|
||||
localStorage.setItem('src', JSON.stringify(src))
|
||||
localStorage.setItem('dst', JSON.stringify(dst))
|
||||
localStorage.setItem('gameInfo', JSON.stringify(gameInfo))
|
||||
}
|
||||
|
||||
function actuallyStartTransfer() {
|
||||
srcEl.pull()
|
||||
.then(() => dstEl.push(srcExportedData))
|
||||
.then(() => confirm = {
|
||||
title: "Done!",
|
||||
message: `Transfer completed successfully! Your data on ${dst.dns} is overwritten with your data from ${src.dns}.`
|
||||
})
|
||||
.catch(e => error = e)
|
||||
.finally(() => loading = false)
|
||||
}
|
||||
|
||||
function startTransfer() {
|
||||
if (!(srcTested && dstTested)) return alert("Please test both servers first!")
|
||||
if (loading) return alert("Transfer already in progress!")
|
||||
console.log("Starting transfer...")
|
||||
loading = true
|
||||
|
||||
if (dstEl.exportedData) return actuallyStartTransfer()
|
||||
|
||||
// Ask user to make sure to backup their data
|
||||
confirm = {
|
||||
title: "Confirm transfer",
|
||||
message: "It seems like you haven't backed up your destination data. Are you sure you want to proceed? (This will overwrite your destination server's data)",
|
||||
dangerous: true,
|
||||
confirm: actuallyStartTransfer,
|
||||
cancel: () => { loading = false }
|
||||
}
|
||||
}
|
||||
|
||||
defaultGame()
|
||||
</script>
|
||||
|
||||
<StatusOverlays bind:confirm={confirm} {error} />
|
||||
|
||||
<main class="content">
|
||||
<div class="outer-title-options">
|
||||
<h2>🏳️⚧️ AquaTrans™ Data Transfer?</h2>
|
||||
<nav>
|
||||
{#each tabs as tabName, i}
|
||||
<div transition:slide={{axis: 'x'}} class:active={tab === i}
|
||||
on:click={() => tab = i} on:keydown={e => e.key === 'Enter' && (tab = i)}
|
||||
role="button" tabindex="0">
|
||||
{ts(`settings.tabs.${tabName}`)}
|
||||
</div>
|
||||
{/each}
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div class="prompt">
|
||||
<p>👋 Welcome to the AquaTrans™ server data transfer tool!</p>
|
||||
<p>You can use this to export data from any server, and input data into any server using the connection credentials (card number, server address, and keychip id).</p>
|
||||
<p>This tool will simulate a game client and pull your data from the source server, and push your data to the destination server.</p>
|
||||
<p>Please fill out the info below to get started!</p>
|
||||
</div>
|
||||
|
||||
<TransferServer bind:src={src} bind:gameInfo={gameInfo} on:change={onChange}
|
||||
bind:tested={srcTested} bind:this={srcEl} bind:exportedData={srcExportedData} />
|
||||
|
||||
<div class="arrow" class:disabled={!(srcTested && dstTested)}>
|
||||
<img src="{DATA_HOST}/d/DownArrow.png" alt="arrow" on:click={startTransfer}>
|
||||
</div>
|
||||
|
||||
<TransferServer bind:src={dst} bind:gameInfo={gameInfo} on:change={onChange}
|
||||
bind:tested={dstTested} bind:this={dstEl} isSrc={false} />
|
||||
</main>
|
||||
|
||||
|
||||
<style lang="sass">
|
||||
.arrow
|
||||
width: 100%
|
||||
display: flex
|
||||
justify-content: center
|
||||
margin-top: -40px
|
||||
margin-bottom: -40px
|
||||
z-index: 1
|
||||
|
||||
&.disabled
|
||||
filter: grayscale(1)
|
||||
|
||||
// CSS animation to let the image opacity breathe
|
||||
img
|
||||
animation: breathe 1s infinite alternate
|
||||
|
||||
@keyframes breathe
|
||||
0%
|
||||
opacity: 0.5
|
||||
100%
|
||||
opacity: 1
|
||||
</style>
|
||||
|
||||
|
||||
21
AquaNet/src/pages/Transfer/TransferLib.ts
Normal file
21
AquaNet/src/pages/Transfer/TransferLib.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
|
||||
interface AllNetSrc {
|
||||
card: string
|
||||
dns: string
|
||||
keychip: string
|
||||
}
|
||||
|
||||
interface AllNetGame {
|
||||
game: string
|
||||
version: string
|
||||
}
|
||||
|
||||
interface AllNetClient extends AllNetSrc, AllNetGame {}
|
||||
|
||||
interface TrCheckGood {
|
||||
gameUrl: string
|
||||
userId: number
|
||||
}
|
||||
|
||||
type TrStreamMessage = { message: string } | { error: string } | { data: string }
|
||||
|
||||
198
AquaNet/src/pages/Transfer/TransferServer.svelte
Normal file
198
AquaNet/src/pages/Transfer/TransferServer.svelte
Normal file
@@ -0,0 +1,198 @@
|
||||
<script lang="ts">
|
||||
import StatusOverlays from "../../components/StatusOverlays.svelte";
|
||||
import { TRANSFER } from "../../libs/sdk";
|
||||
import { download, selectJsonFile } from "../../libs/ui";
|
||||
import InputTextShort from "./InputTextShort.svelte";
|
||||
|
||||
export let src: AllNetSrc
|
||||
export let gameInfo: AllNetGame
|
||||
export let isSrc: boolean = true
|
||||
|
||||
export let tested: boolean = false
|
||||
let [loading, error, expectedError] = [false, "", ""]
|
||||
|
||||
function testConnection() {
|
||||
if (loading) return
|
||||
|
||||
// Preliminiary checks
|
||||
if (!src.dns || !src.keychip || !src.card || !gameInfo.game || !gameInfo.version) {
|
||||
error = "Please fill out all fields"
|
||||
return
|
||||
}
|
||||
|
||||
loading = true
|
||||
console.log("Testing connection...")
|
||||
TRANSFER.check({...src, ...gameInfo}).then(res => {
|
||||
console.log("Connection test result:", res)
|
||||
tested = true
|
||||
}).catch(err => expectedError = err.message).finally(() => loading = false)
|
||||
}
|
||||
|
||||
let messages: string[] = []
|
||||
export let exportedData: string = ""
|
||||
|
||||
export function pull(): Promise<string> {
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
if (loading || !tested) return reject("Please test connection first")
|
||||
if (exportedData) return resolve(exportedData)
|
||||
console.log("Exporting data...")
|
||||
|
||||
TRANSFER.pull({...src, ...gameInfo}, (msg: TrStreamMessage) => {
|
||||
console.log("Export progress: ", JSON.stringify(msg))
|
||||
|
||||
if ('message' in msg) messages = [...messages, msg.message]
|
||||
|
||||
if ('error' in msg) {
|
||||
expectedError = msg.error
|
||||
reject(msg.error)
|
||||
}
|
||||
|
||||
if ('data' in msg) {
|
||||
// file name: Export YYYY-MM-DD {server host} {game} {card last 6}.json
|
||||
let date = new Date().toISOString().split('T')[0]
|
||||
let host = new URL(src.dns).hostname
|
||||
download(msg.data, `Export ${date} ${host} ${gameInfo.game} ${src.card.slice(-6)}.json`)
|
||||
exportedData = msg.data
|
||||
resolve(msg.data)
|
||||
}
|
||||
}).catch(err => { expectedError = err; reject(err) })
|
||||
})
|
||||
}
|
||||
|
||||
function pushBtn() {
|
||||
if (loading || !tested) return
|
||||
selectJsonFile().then(obj => push(JSON.stringify(obj)))
|
||||
}
|
||||
|
||||
export function push(data: string) {
|
||||
if (loading || !tested) return
|
||||
console.log("Import data...")
|
||||
loading = true
|
||||
|
||||
return TRANSFER.push({...src, ...gameInfo}, data).then(() => {
|
||||
console.log("Data imported successfully")
|
||||
messages = ["Data imported successfully"]
|
||||
}).catch(err => expectedError = err.message).finally(() => loading = false)
|
||||
}
|
||||
</script>
|
||||
|
||||
<StatusOverlays {loading} {error} />
|
||||
|
||||
<div class="server source" class:src={isSrc} class:hasError={expectedError} class:tested={tested}>
|
||||
<h3>{isSrc ? "Source" : "Target"} Server</h3>
|
||||
|
||||
{#if expectedError}
|
||||
<blockquote class="error-msg">{expectedError}</blockquote>
|
||||
{/if}
|
||||
|
||||
<!-- First input line -->
|
||||
<div class="inputs">
|
||||
<InputTextShort desc="Server Address" placeholder="e.g. http://aquadx.hydev.org"
|
||||
bind:value={src.dns} on:change validate={v => /^https?:\/\/[a-z0-9.-]+(:\d+)?$/i.test(v)} disabled={tested} />
|
||||
<InputTextShort desc="Keychip ID" placeholder="e.g. A0299792458"
|
||||
bind:value={src.keychip} on:change validate={v => /^([A-Z0-9]{11}|[A-Z0-9]{4}-[A-Z0-9]{11})$/.test(v)} disabled={tested} />
|
||||
</div>
|
||||
|
||||
<!-- Second input line -->
|
||||
<div class="inputs">
|
||||
<div class="game-version">
|
||||
<InputTextShort desc="Game" placeholder="e.g. SDHD"
|
||||
bind:value={gameInfo.game} on:change disabled={tested} />
|
||||
<InputTextShort desc="Version" placeholder="e.g. 2.30"
|
||||
bind:value={gameInfo.version} on:change disabled={tested} />
|
||||
</div>
|
||||
<InputTextShort desc="Card Number" placeholder="e.g. 27182818284590452353"
|
||||
bind:value={src.card} on:change disabled={tested} />
|
||||
</div>
|
||||
|
||||
<!-- Streaming messages -->
|
||||
{#if messages.length > 0}
|
||||
<div class="stream-messages">
|
||||
{#each messages.slice(Math.max(messages.length - 5, 0), undefined) as msg}
|
||||
<p>{msg}</p>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Buttons -->
|
||||
<div class="inputs buttons">
|
||||
{#if !tested}
|
||||
<button class="flex-1" on:click={testConnection} disabled={loading}>Test Connection</button>
|
||||
{:else}
|
||||
<button class="flex-1" on:click={pull}>Export Data</button>
|
||||
<button class="flex-1" on:click={pushBtn}>Import Data</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="sass">
|
||||
@use "../../vars"
|
||||
@use "sass:color"
|
||||
|
||||
.error-msg
|
||||
white-space: pre-wrap
|
||||
margin: 0
|
||||
|
||||
.server
|
||||
display: flex
|
||||
flex-direction: column
|
||||
gap: 1rem
|
||||
|
||||
// --c-src: 202, 168, 252
|
||||
--c-src: 179, 198, 255
|
||||
// animation: hue-rotate 10s infinite linear
|
||||
// &.src
|
||||
// --c-src: 173, 192, 247
|
||||
// animation: hue-rotate 10s infinite linear reverse
|
||||
|
||||
&.tested
|
||||
--c-src: 169, 255, 186
|
||||
|
||||
&.hasError
|
||||
--c-src: 255, 174, 174
|
||||
animation: none
|
||||
|
||||
padding: 1rem
|
||||
border-radius: vars.$border-radius
|
||||
// background-color: vars.$ov-light
|
||||
background: #252525
|
||||
|
||||
// Pink outline
|
||||
border: 1px solid rgba(var(--c-src), 0.5)
|
||||
box-shadow: 0 0 1rem 0 rgba(var(--c-src), 0.25)
|
||||
|
||||
h3
|
||||
margin: 0
|
||||
font-size: 1.5rem
|
||||
text-align: center
|
||||
|
||||
|
||||
// @keyframes hue-rotate
|
||||
// 0%
|
||||
// filter: hue-rotate(0deg)
|
||||
// 100%
|
||||
// filter: hue-rotate(360deg)
|
||||
|
||||
.inputs
|
||||
display: flex
|
||||
flex-wrap: wrap
|
||||
gap: 1rem
|
||||
|
||||
.game-version
|
||||
flex: 60
|
||||
display: flex
|
||||
gap: 1rem
|
||||
|
||||
:global(> *)
|
||||
width: 100px
|
||||
|
||||
&.buttons
|
||||
margin-top: 0.5rem
|
||||
|
||||
.stream-messages
|
||||
font-size: 0.8rem
|
||||
opacity: 0.8
|
||||
|
||||
margin-top: 0.5rem
|
||||
padding: 0 0.5rem
|
||||
</style>
|
||||
Reference in New Issue
Block a user