mirror of
https://github.com/CHOMPStation2/CHOMPStation2.git
synced 2025-12-09 07:57:00 +00:00
[MIRROR] Port of the iframe storage for settings (#12040)
Co-authored-by: Kashargul <144968721+Kashargul@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
1b99ac2969
commit
ba5019bd9d
33
.github/workflows/generate_client_storage.yml
vendored
Normal file
33
.github/workflows/generate_client_storage.yml
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
name: "Generate Client Storage"
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- tgui/public/*
|
||||
|
||||
jobs:
|
||||
dispatch_repo:
|
||||
if: ( !contains(github.event.head_commit.message, '[ci skip]') )
|
||||
name: Repository Dispatch
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Generate App Token
|
||||
id: app-token-generation
|
||||
uses: actions/create-github-app-token@v2
|
||||
if: env.APP_PRIVATE_KEY != '' && env.APP_ID != ''
|
||||
with:
|
||||
app-id: ${{ secrets.APP_ID }}
|
||||
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||
owner: vorestation
|
||||
env:
|
||||
APP_PRIVATE_KEY: ${{ secrets.APP_PRIVATE_KEY }}
|
||||
APP_ID: ${{ secrets.APP_ID }}
|
||||
|
||||
- name: Send Repository Dispatch
|
||||
if: success()
|
||||
uses: peter-evans/repository-dispatch@v4
|
||||
with:
|
||||
token: ${{ steps.app-token-generation.outputs.token }}
|
||||
repository: vorestation/byond-client-storage
|
||||
event-type: on_master_push
|
||||
@@ -11,3 +11,7 @@
|
||||
/datum/config_entry/flag/smart_cache_assets
|
||||
|
||||
/datum/config_entry/flag/save_spritesheets
|
||||
|
||||
/datum/config_entry/string/storage_cdn_iframe
|
||||
protection = CONFIG_ENTRY_LOCKED
|
||||
default = "https://vorestation.github.io/byond-client-storage/iframe.html"
|
||||
|
||||
@@ -40,6 +40,25 @@ SUBSYSTEM_DEF(tgui)
|
||||
|
||||
basehtml = replacetextEx(basehtml, "<!-- tgui:nt-copyright -->", "Nanotrasen (c) 2284-[text2num(time2text(world.realtime,"YYYY")) + STATION_YEAR_OFFSET]")
|
||||
|
||||
/datum/controller/subsystem/tgui/OnConfigLoad()
|
||||
var/storage_iframe = CONFIG_GET(string/storage_cdn_iframe)
|
||||
|
||||
if(storage_iframe && storage_iframe != /datum/config_entry/string/storage_cdn_iframe::default)
|
||||
basehtml = replacetext(basehtml, "\[tgui:storagecdn\]", storage_iframe)
|
||||
return
|
||||
|
||||
if(CONFIG_GET(string/asset_transport) == "webroot")
|
||||
var/datum/asset_transport/webroot/webroot = SSassets.transport
|
||||
|
||||
var/datum/asset_cache_item/item = webroot.register_asset("iframe.html", file("tgui/public/iframe.html"))
|
||||
basehtml = replacetext(basehtml, "\[tgui:storagecdn\]", webroot.get_asset_url("iframe.html", item))
|
||||
return
|
||||
|
||||
if(!storage_iframe)
|
||||
return
|
||||
|
||||
basehtml = replacetext(basehtml, "\[tgui:storagecdn\]", storage_iframe)
|
||||
|
||||
/datum/controller/subsystem/tgui/Shutdown()
|
||||
close_all_uis()
|
||||
|
||||
|
||||
@@ -269,6 +269,7 @@ GLOBAL_LIST_INIT(blacklisted_builds, list(
|
||||
if (CONFIG_GET(flag/chatlog_database_backend))
|
||||
chatlog_token = vchatlog_generate_token(ckey, GLOB.round_id)
|
||||
|
||||
winset(src, null, list("browser-options" = "find,refresh"))
|
||||
// Instantiate stat panel
|
||||
stat_panel = new(src, "statbrowser")
|
||||
stat_panel.subscribe(src, PROC_REF(on_stat_panel_message))
|
||||
|
||||
@@ -42,3 +42,12 @@ SMART_CACHE_ASSETS
|
||||
## Useful for developers to debug potential spritesheet issues to determine where the issue is cropping up (either in DM-side sprite generation or in the TGUI-side display of said spritesheet).
|
||||
## Will only seek to waste disk space if ran on production.
|
||||
#SAVE_SPRITESHEETS
|
||||
|
||||
# If configured, this allows server operators to define the persistent origin used for clientside
|
||||
# storage. This must host the same file as available in tgui/public/iframe.html. This is also hosted
|
||||
# on the GitHub Pages site for the /tg/station repository, so does not need to be configured.
|
||||
# If multiple servers use the same domain name, clientside features such as message saving
|
||||
# and chat tabs will be persistent across both.
|
||||
# If this setting is not configured, but the webroot CDN is, that will be used instead of GitHub Pages.
|
||||
# If this setting is mpty, and the webroot CDN is disabled, byondstorage will be used.
|
||||
# STORAGE_CDN_IFRAME https://vorestation.github.io/byond-client-storage/iframe.html
|
||||
|
||||
5
tgui/global.d.ts
vendored
5
tgui/global.d.ts
vendored
@@ -63,6 +63,11 @@ type ByondType = {
|
||||
*/
|
||||
strictMode: boolean;
|
||||
|
||||
/**
|
||||
* The external URL for the IndexedDB IFrame to use as the origin
|
||||
*/
|
||||
storageCdn: string;
|
||||
|
||||
/**
|
||||
* Makes a BYOND call.
|
||||
*
|
||||
|
||||
@@ -6,12 +6,13 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
export const IMPL_MEMORY = 0;
|
||||
export const IMPL_HUB_STORAGE = 1;
|
||||
|
||||
type StorageImplementation = typeof IMPL_MEMORY | typeof IMPL_HUB_STORAGE;
|
||||
export const IMPL_IFRAME_INDEXED_DB = 2;
|
||||
|
||||
const KEY_NAME = 'chomp'; // CHOMPEdit - CHOMPStation Localstore
|
||||
type StorageImplementation =
|
||||
| typeof IMPL_HUB_STORAGE
|
||||
| typeof IMPL_IFRAME_INDEXED_DB;
|
||||
|
||||
type StorageBackend = {
|
||||
impl: StorageImplementation;
|
||||
@@ -61,28 +62,160 @@ class HubStorageBackend implements StorageBackend {
|
||||
}
|
||||
}
|
||||
|
||||
class IFrameIndexedDbBackend implements StorageBackend {
|
||||
public impl: StorageImplementation;
|
||||
|
||||
private documentElement: HTMLIFrameElement;
|
||||
private iframeWindow: Window;
|
||||
|
||||
constructor() {
|
||||
this.impl = IMPL_IFRAME_INDEXED_DB;
|
||||
}
|
||||
|
||||
async ready(): Promise<boolean | null> {
|
||||
const iframe = document.createElement('iframe');
|
||||
iframe.style.display = 'none';
|
||||
iframe.src = Byond.storageCdn;
|
||||
|
||||
const completePromise: Promise<boolean> = new Promise((resolve) => {
|
||||
fetch(Byond.storageCdn, { method: 'HEAD' })
|
||||
.then((response) => {
|
||||
if (response.status !== 200) {
|
||||
resolve(false);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
resolve(false);
|
||||
});
|
||||
|
||||
window.addEventListener('message', (message) => {
|
||||
if (message.data === 'ready') {
|
||||
resolve(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
this.documentElement = document.body.appendChild(iframe);
|
||||
if (!this.documentElement.contentWindow) {
|
||||
return new Promise((res) => res(false));
|
||||
}
|
||||
|
||||
this.iframeWindow = this.documentElement.contentWindow;
|
||||
|
||||
return completePromise;
|
||||
}
|
||||
|
||||
async get(key: string): Promise<any> {
|
||||
const promise = new Promise((resolve) => {
|
||||
window.addEventListener('message', (message) => {
|
||||
if (message.data.key && message.data.key === key) {
|
||||
resolve(message.data.value);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
this.iframeWindow.postMessage(
|
||||
{ type: 'get', key: `${KEY_NAME}-${key}` },
|
||||
'*',
|
||||
);
|
||||
return promise;
|
||||
}
|
||||
|
||||
async set(key: string, value: any): Promise<void> {
|
||||
this.iframeWindow.postMessage(
|
||||
{ type: 'set', key: `${KEY_NAME}-${key}`, value: value },
|
||||
'*',
|
||||
);
|
||||
}
|
||||
|
||||
async remove(key: string): Promise<void> {
|
||||
this.iframeWindow.postMessage(
|
||||
{ type: 'remove', key: `${KEY_NAME}-${key}` },
|
||||
'*',
|
||||
);
|
||||
}
|
||||
|
||||
async clear(): Promise<void> {
|
||||
this.iframeWindow.postMessage({ type: 'clear' }, '*');
|
||||
}
|
||||
|
||||
async destroy(): Promise<void> {
|
||||
document.body.removeChild(this.documentElement);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Web Storage Proxy object, which selects the best backend available
|
||||
* depending on the environment.
|
||||
*/
|
||||
class StorageProxy implements StorageBackend {
|
||||
private backendPromise: Promise<StorageBackend>;
|
||||
public impl: StorageImplementation = IMPL_MEMORY;
|
||||
public impl: StorageImplementation = IMPL_IFRAME_INDEXED_DB;
|
||||
|
||||
constructor() {
|
||||
this.backendPromise = (async () => {
|
||||
// If we have not enabled byondstorage yet, we need to check
|
||||
// if we can use the IFrame, or if we need to enable byondstorage
|
||||
if (!testHubStorage()) {
|
||||
// If we have an IFrame URL we can use, and we haven't already enabled
|
||||
// byondstorage, we should use the IFrame backend
|
||||
if (Byond.storageCdn) {
|
||||
const iframe = new IFrameIndexedDbBackend();
|
||||
|
||||
if ((await iframe.ready()) === true) {
|
||||
if (await iframe.get('byondstorage-migrated')) return iframe;
|
||||
|
||||
Byond.winset(null, 'browser-options', '+byondstorage');
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
document.addEventListener('byondstorageupdated', async () => {
|
||||
setTimeout(() => {
|
||||
const hub = new HubStorageBackend();
|
||||
|
||||
// Migrate these existing settings from byondstorage to the IFrame
|
||||
for (const setting of [
|
||||
'panel-settings',
|
||||
'chat-state',
|
||||
'chat-messages',
|
||||
]) {
|
||||
hub
|
||||
.get(setting)
|
||||
.then((settings) => iframe.set(setting, settings));
|
||||
}
|
||||
|
||||
iframe.set('byondstorage-migrated', true);
|
||||
Byond.winset(null, 'browser-options', '-byondstorage');
|
||||
|
||||
resolve();
|
||||
}, 1);
|
||||
});
|
||||
});
|
||||
|
||||
return iframe;
|
||||
}
|
||||
|
||||
iframe.destroy();
|
||||
}
|
||||
|
||||
// IFrame hasn't worked out for us, we'll need to enable byondstorage
|
||||
Byond.winset(null, 'browser-options', '+byondstorage');
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const listener = () => {
|
||||
document.removeEventListener('byondstorageupdated', listener);
|
||||
resolve(new HubStorageBackend());
|
||||
|
||||
// This event is emitted *before* byondstorage is actually created
|
||||
// so we have to wait a little bit before we can use it
|
||||
setTimeout(() => resolve(new HubStorageBackend()), 1);
|
||||
};
|
||||
|
||||
document.addEventListener('byondstorageupdated', listener);
|
||||
});
|
||||
}
|
||||
|
||||
// byondstorage is already enabled, we can use it straight away
|
||||
return new HubStorageBackend();
|
||||
})() as Promise<StorageBackend>;
|
||||
})();
|
||||
}
|
||||
|
||||
async get(key: string): Promise<any> {
|
||||
|
||||
@@ -334,6 +334,7 @@ export const chatMiddleware = (store) => {
|
||||
next(action);
|
||||
const page = selectCurrentChatPage(store.getState());
|
||||
chatRenderer.changePage(page);
|
||||
needsUpdate = true;
|
||||
return;
|
||||
}
|
||||
if (type === rebuildChat.type) {
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
|
||||
// Expose inlined metadata
|
||||
Byond.windowId = parseMetaTag('tgui:windowId');
|
||||
Byond.storageCdn = parseMetaTag('tgui:storagecdn');
|
||||
|
||||
// Backwards compatibility
|
||||
window.__windowId__ = Byond.windowId;
|
||||
@@ -397,32 +398,17 @@
|
||||
],
|
||||
};
|
||||
|
||||
try {
|
||||
window
|
||||
.showSaveFilePicker(opts)
|
||||
.then((fileHandle) => {
|
||||
fileHandle
|
||||
.createWritable()
|
||||
.then((writeableFileHandle) => {
|
||||
writeableFileHandle
|
||||
.write(blob)
|
||||
.then(() => {
|
||||
writeableFileHandle.close();
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
});
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
});
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
window
|
||||
.showSaveFilePicker(opts)
|
||||
.then(function (file) {
|
||||
return file.createWritable();
|
||||
})
|
||||
.then(function (file) {
|
||||
return file.write(blob).then(function () {
|
||||
return file.close();
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
})
|
||||
.catch(function () {});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
2
tgui/public/helpers.min.js
vendored
2
tgui/public/helpers.min.js
vendored
File diff suppressed because one or more lines are too long
102
tgui/public/iframe.html
Normal file
102
tgui/public/iframe.html
Normal file
@@ -0,0 +1,102 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta charset="utf-8" />
|
||||
|
||||
<script type="text/javascript">
|
||||
const INDEXED_DB_VERSION = 1;
|
||||
const INDEXED_DB_NAME = 'tgui';
|
||||
const INDEXED_DB_STORE_NAME = 'storage';
|
||||
|
||||
const READ_ONLY = 'readonly';
|
||||
const READ_WRITE = 'readwrite';
|
||||
|
||||
const MAX_MESSAGES = 1000;
|
||||
|
||||
const dbPromise = new Promise((resolve, reject) => {
|
||||
const indexedDB = window.indexedDB;
|
||||
const req = indexedDB.open(INDEXED_DB_NAME, INDEXED_DB_VERSION);
|
||||
req.onupgradeneeded = (event) => {
|
||||
try {
|
||||
if (event.oldVersion < 1) {
|
||||
req.result.createObjectStore(INDEXED_DB_STORE_NAME);
|
||||
}
|
||||
} catch (err) {
|
||||
reject(new Error('Failed to upgrade IDB: ' + req.error));
|
||||
}
|
||||
};
|
||||
|
||||
req.onsuccess = () => resolve(req.result);
|
||||
|
||||
req.onerror = () => {
|
||||
reject(new Error('Failed to open IDB: ' + req.error));
|
||||
};
|
||||
});
|
||||
|
||||
window.addEventListener('message', (messageEvent) => {
|
||||
switch (messageEvent.data.type) {
|
||||
case 'ping':
|
||||
messageEvent.source.postMessage(true, '*');
|
||||
break;
|
||||
case 'get':
|
||||
get(messageEvent.data.key).then((value) => {
|
||||
messageEvent.source.postMessage(
|
||||
{ key: messageEvent.data.key, value: value },
|
||||
'*',
|
||||
);
|
||||
});
|
||||
break;
|
||||
case 'set':
|
||||
set(messageEvent.data.key, messageEvent.data.value);
|
||||
break;
|
||||
case 'remove':
|
||||
remove(messageEvent.data.key);
|
||||
break;
|
||||
case 'clear':
|
||||
clear();
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
const getStore = async (mode) => {
|
||||
return dbPromise.then((db) =>
|
||||
db
|
||||
.transaction(INDEXED_DB_STORE_NAME, mode)
|
||||
.objectStore(INDEXED_DB_STORE_NAME),
|
||||
);
|
||||
};
|
||||
|
||||
const get = async (key) => {
|
||||
const store = await getStore(READ_ONLY);
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = store.get(key);
|
||||
req.onsuccess = () => resolve(req.result);
|
||||
req.onerror = () => reject(req.error);
|
||||
});
|
||||
};
|
||||
|
||||
const set = async (key, value) => {
|
||||
const store = await getStore(READ_WRITE);
|
||||
store.put(value, key);
|
||||
};
|
||||
|
||||
const remove = async (key) => {
|
||||
const store = await getStore(READ_WRITE);
|
||||
store.delete(key);
|
||||
};
|
||||
|
||||
const clear = async () => {
|
||||
const store = await getStore(READ_WRITE);
|
||||
store.clear();
|
||||
};
|
||||
|
||||
window.onload = () => {
|
||||
window.parent.postMessage('ready', '*');
|
||||
};
|
||||
</script>
|
||||
</head>
|
||||
</html>
|
||||
@@ -6,6 +6,7 @@
|
||||
<!-- Inlined metadata -->
|
||||
<meta id="tgui:windowId" content="[tgui:windowId]" />
|
||||
<meta id="tgui:strictMode" content="[tgui:strictMode]" />
|
||||
<meta id="tgui:storagecdn" content="[tgui:storagecdn]" />
|
||||
|
||||
<!-- Early setup -->
|
||||
<!-- tgui:helpers -->
|
||||
|
||||
Reference in New Issue
Block a user