Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
NebulaServices
GitHub Repository: NebulaServices/Nebula
Path: blob/main/src/utils/marketplace.ts
976 views
1
import { Elements, log } from "./index";
2
import { StoreManager } from "./storage";
3
import { SettingsVals } from "./values";
4
5
type PluginType = "page" | "serviceWorker";
6
type MarketplacePluginType = "plugin-page" | "plugin-sw";
7
type PackageType = "theme" | MarketplacePluginType;
8
9
interface Plug {
10
name: string;
11
src: string;
12
type: PluginType;
13
remove?: boolean;
14
}
15
interface SWPagePlugin extends Omit<Plug, "name" | "src"> {
16
host: string;
17
html: string;
18
injectTo: "head" | "body";
19
}
20
21
type SWPluginFunction<T extends unknown> = (args: T) => void | unknown;
22
23
type Events =
24
| "abortpayment"
25
| "activate"
26
| "backgroundfetchabort"
27
| "backgroundfetchclick"
28
| "backgroundfetchfail"
29
| "backgroundfetchsuccess"
30
| "canmakepayment"
31
| "contentdelete"
32
| "cookiechange"
33
| "fetch"
34
| "install"
35
| "message"
36
| "messageerror"
37
| "notificationclick"
38
| "notificationclose"
39
| "paymentrequest"
40
| "periodicsync"
41
| "push"
42
| "pushsubscriptionchange"
43
| "sync";
44
45
interface SWPlugin extends Omit<Plug, "src"> {
46
function: string | SWPluginFunction<any>;
47
events: Events[];
48
}
49
50
interface Theme {
51
name: string;
52
payload: string;
53
video?: string;
54
bgImage?: string;
55
};
56
57
/**
58
* A class where for all of the Marketplace handlers.
59
* It creates it's own StoreManager where all of it's values will live.
60
* And it has 2 static items. instances and getInstances.
61
*
62
* @example
63
* //Create a new Marketplace instance
64
* const mp = new Marketplace();
65
* //Use one of the very many methods available.
66
*
67
* //Get all instances of a Marketplace is as easy as:
68
* // const mp = Marketplace.getInstances.next().value;
69
* // Consume and use the class.
70
*/
71
class Marketplace {
72
//create our own subsidized StoreManager with it's own prefix so the marketplace stuff NEVER touches the other data
73
#storage: StoreManager<"nebula||marketplace">;
74
static #instances = new Set();
75
constructor() {
76
this.#storage = new StoreManager("nebula||marketplace");
77
log({ type: 'info', bg: true, prefix: true }, 'Marketplace instance created and ready!');
78
Marketplace.#instances.add(this);
79
}
80
81
/**
82
* A static method to aquire an instance of a marketplace object.
83
*
84
* @example
85
* //Get the first insatnce available.
86
* const mp = Marketplace.getInstances.next().value
87
*
88
* @example
89
* // Iterate over every instance
90
* for (const instance of Marketplace.getInstances()) {
91
* // Do some work
92
* }
93
*/
94
static *getInstances() {
95
//Marketplace.instances.forEach((val) => yield val);
96
for (const item of Marketplace.#instances.keys()) {
97
yield item! as Marketplace;
98
}
99
}
100
101
/**
102
* Detect if our Marketplace is ready or not. If it's not, don't resolve until it IS
103
*/
104
static ready(): Promise<boolean> {
105
return new Promise((resolve) => {
106
const t = setInterval(() => {
107
if (Marketplace.#instances.size !== 0) {
108
clearInterval(t);
109
resolve(true);
110
}
111
}, 100);
112
});
113
}
114
115
async getValueFromStore(val: string): Promise<string> {
116
return this.#storage.getVal(val);
117
}
118
119
120
async getThemes(name?: string): Promise<{themes: any, theme: string, exists: Boolean}> {
121
const themes = JSON.parse(this.#storage.getVal(SettingsVals.marketPlace.themes)) || [];
122
const theme = themes.find((t: any) => t === name);
123
const exists = themes.indexOf(name) !== -1;
124
return { themes, theme, exists };
125
}
126
127
async getPlugins(pname?: string): Promise<{plugins: any, plug: any}> {
128
const plugins = JSON.parse(this.#storage.getVal(SettingsVals.marketPlace.plugins)) || [];
129
const plug = plugins.find(({ name } : { name: string }) => name === pname );
130
return { plugins, plug }
131
}
132
133
/**
134
* Install a theme into both localstorage AND set the theme.
135
*
136
* @example
137
* const mp = new Marketplace() // OR get an instances from getInstances()
138
* mp.installTheme({
139
* name: "testTheme",
140
* payload: "/packages/testTheme/index.css",
141
* // video: if you have a bg video, pass it here.
142
* //bgImage: pass the BG image here if you have one
143
* });
144
*/
145
async installTheme(theme: Omit<Theme, "payload">) {
146
const { themes, exists } = await this.getThemes(theme.name);
147
if (exists) return log({ type: 'error', bg: false, prefix: false, throw: true }, `${theme.name} is already installed!`)
148
themes.push(theme.name);
149
this.#storage.setVal(SettingsVals.marketPlace.themes, JSON.stringify(themes));
150
}
151
152
async installPlugin(plugin: Plug) {
153
let { plugins, plug } = await this.getPlugins(plugin.name);
154
if (plug && plug.remove) { plug.remove = false; console.log(plug); return this.#storage.setVal(SettingsVals.marketPlace.plugins, JSON.stringify(plugins)) };
155
plugins.push({ name: plugin.name, src: plugin.src, type: plugin.type } as unknown as Plug);
156
this.#storage.setVal(SettingsVals.marketPlace.plugins, JSON.stringify(plugins));
157
}
158
159
async uninstallTheme(theme: Omit<Theme, "payload" | "video" | "bgImage">) {
160
const { themes: items, exists } = await this.getThemes(theme.name);
161
if (!exists) return log({ type: 'error', bg: false, prefix: false, throw: true }, `Theme: ${theme.name} is not installed!`);
162
const idx = items.indexOf(theme.name);
163
items.splice(idx, 1);
164
this.#storage.setVal(SettingsVals.marketPlace.themes, JSON.stringify(items));
165
}
166
167
async uninstallPlugin(plug: Omit<Plug, "src">) {
168
let { plugins: items, plug: plugin } = await this.getPlugins(plug.name);
169
170
if (!plugin) return log({ type: 'error', bg: false, prefix: false, throw: true }, `Plugin: ${plug.name} is not installed!`);
171
plugin.remove = true;
172
this.#storage.setVal(SettingsVals.marketPlace.plugins, JSON.stringify(items));
173
}
174
175
async handlePlugins(worker: ServiceWorkerRegistration) {
176
let { plugins } = await this.getPlugins();
177
178
const pagePlugins: SWPagePlugin[] = [];
179
const swPlugins: SWPlugin[] = [];
180
if (plugins.length === 0) return log({ type: 'info', bg: false, prefix: true }, 'No plugins to add! Exiting.');
181
plugins.map(async (plugin: Plug) => {
182
if (plugin.type === "page") {
183
const script = await fetch(`/packages/${plugin.name}/${plugin.src}`);
184
const scriptRes = await script.text();
185
console.log(scriptRes);
186
const evaledScript = eval(scriptRes);
187
console.log(evaledScript);
188
const inject = (await evaledScript()) as unknown as SWPagePlugin;
189
if (!plugin.remove) {
190
pagePlugins.push({
191
host: inject.host,
192
html: inject.html,
193
injectTo: inject.injectTo,
194
type: 'page'
195
});
196
}
197
else {
198
plugins = plugins.filter(({ name }: { name: string }) => name !== plugin.name);
199
pagePlugins.push({
200
remove: true,
201
host: inject.host,
202
html: inject.html,
203
injectTo: inject.injectTo,
204
type: 'page'
205
});
206
}
207
worker.active?.postMessage(pagePlugins);
208
}
209
210
if (plugin.type === "serviceWorker") {
211
const s = await fetch(`/packages/${plugin.name}/${plugin.src}`);
212
const sRes = await s.text();
213
const eScript = eval(sRes);
214
const inject = (await eScript()) as unknown as SWPlugin;
215
if (!plugin.remove) {
216
swPlugins.push({
217
function: inject.function.toString(),
218
name: plugin.name,
219
events: inject.events,
220
type: 'serviceWorker'
221
});
222
}
223
else {
224
plugins = plugins.filter(({ name }: { name: string }) => name !== plugin.name);
225
swPlugins.push({
226
remove: true,
227
function: inject.function.toString(),
228
name: plugin.name,
229
events: inject.events,
230
type: 'serviceWorker'
231
});
232
}
233
worker.active?.postMessage(swPlugins);
234
}
235
this.#storage.setVal(SettingsVals.marketPlace.plugins, JSON.stringify(plugins));
236
});
237
}
238
239
async theme(opts: { type: 'normal', payload: string, sources?: { video?: string, bg?: string }, name: string } | { type: 'remove', payload?: string, sources?: { video?: string, bg?: string }, name?: string }) {
240
const elems = Elements.select([
241
{ type: 'id', val: 'stylesheet' },
242
{ type: 'id', val: 'nebulaVideo' },
243
{ type: 'id', val: 'nebulaImage' }
244
]);
245
const s = Elements.exists<HTMLLinkElement>(await elems.next());
246
const nv = Elements.exists<HTMLVideoElement>(await elems.next());
247
const ni = Elements.exists<HTMLImageElement>(await elems.next());
248
249
const nvl = this.#storage.getVal(SettingsVals.marketPlace.appearance.video);
250
const nil = this.#storage.getVal(SettingsVals.marketPlace.appearance.image);
251
const tsp = this.#storage.getVal(SettingsVals.marketPlace.appearance.theme.payload);
252
const tsn = this.#storage.getVal(SettingsVals.marketPlace.appearance.theme.name);
253
254
const reset = (style: boolean) => {
255
const st = this.#storage;
256
if (style) {
257
st.removeVal(SettingsVals.marketPlace.appearance.theme.name);
258
st.removeVal(SettingsVals.marketPlace.appearance.theme.payload);
259
s.href = "/nebula.css";
260
}
261
st.removeVal(SettingsVals.marketPlace.appearance.video);
262
nv.src = "";
263
st.removeVal(SettingsVals.marketPlace.appearance.image);
264
ni.style.display = "none";
265
ni.src = "";
266
}
267
268
if (opts.type === 'remove') return reset(true);
269
270
if (opts.sources?.video || nvl) {
271
reset(false);
272
if (!nvl) this.#storage.setVal(SettingsVals.marketPlace.appearance.video, opts.sources?.video || nvl);
273
nv.src = `/packages/${opts.name}/${opts.sources?.video ? opts.sources.video : nvl}`
274
}
275
if (opts.sources?.bg || nil) {
276
reset(false);
277
if (!nil) this.#storage.setVal(SettingsVals.marketPlace.appearance.image, opts.sources?.bg || nil);
278
ni.style.display = "block";
279
ni.src = `/packages/${opts.name}/${opts.sources?.bg ? opts.sources.bg : nil}`
280
}
281
282
if (opts.payload) {
283
if (tsp !== opts.payload) {
284
this.#storage.setVal(SettingsVals.marketPlace.appearance.theme.payload, opts.payload);
285
this.#storage.setVal(SettingsVals.marketPlace.appearance.theme.name, opts.name);
286
}
287
s.href = `/packages/${opts.name}/${opts.payload}`;
288
}
289
else {
290
if (tsp) return s.href = `/packages/${tsn}/${tsp}`;
291
}
292
}
293
}
294
295
export {
296
Marketplace,
297
type PluginType,
298
type MarketplacePluginType,
299
type PackageType,
300
type Plug,
301
type SWPagePlugin,
302
type SWPluginFunction,
303
type Events,
304
type SWPlugin,
305
type Theme
306
}
307
308