init
This commit is contained in:
commit
451223816b
338 changed files with 9938 additions and 0 deletions
184
src/lib/core/adb/adb.ts
Normal file
184
src/lib/core/adb/adb.ts
Normal file
|
|
@ -0,0 +1,184 @@
|
|||
import { Adb, AdbDaemonTransport, encodeUtf8 } from '@yume-chan/adb';
|
||||
import AdbWebCredentialStore from '@yume-chan/adb-credential-web';
|
||||
import {
|
||||
AdbDaemonWebUsbDeviceManager,
|
||||
AdbDaemonWebUsbDeviceObserver,
|
||||
type AdbDaemonWebUsbDevice
|
||||
} from '@yume-chan/adb-daemon-webusb';
|
||||
import { AdbInstance } from '../../../routes/state.svelte';
|
||||
import { deviceCredentialManager } from './deviceCredManager';
|
||||
import { Consumable, MaybeConsumable, ReadableStream } from '@yume-chan/stream-extra';
|
||||
import { AdbScrcpyClient } from '@yume-chan/adb-scrcpy';
|
||||
|
||||
export async function connnectViaWebUSB() {
|
||||
const device = await AdbDaemonWebUsbDeviceManager.BROWSER?.requestDevice();
|
||||
console.log('usb ok', globalThis.navigator.usb);
|
||||
if (device) {
|
||||
console.log('connect ', device.name);
|
||||
|
||||
try {
|
||||
const credentialStore = new AdbWebCredentialStore();
|
||||
const connection = await device.connect();
|
||||
|
||||
const transport = await AdbDaemonTransport.authenticate({
|
||||
connection: connection,
|
||||
serial: device.serial,
|
||||
credentialStore: credentialStore
|
||||
});
|
||||
|
||||
const adb = new Adb(transport);
|
||||
saveAdbInstance(adb);
|
||||
|
||||
// save device info
|
||||
await deviceCredentialManager.saveDeviceInfo(device);
|
||||
} catch (e: any) {
|
||||
console.error('error on connect', e);
|
||||
throw new Error(e.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function connectDeviceByCred(
|
||||
device: AdbDaemonWebUsbDevice,
|
||||
credStore: AdbWebCredentialStore
|
||||
) {
|
||||
try {
|
||||
const connection = await device.connect();
|
||||
const transport = await AdbDaemonTransport.authenticate({
|
||||
connection: connection,
|
||||
serial: device.serial,
|
||||
credentialStore: credStore
|
||||
});
|
||||
|
||||
const adb = new Adb(transport);
|
||||
saveAdbInstance(adb);
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export function saveAdbInstance(adb: Adb | undefined) {
|
||||
AdbInstance.instance = adb;
|
||||
}
|
||||
|
||||
export function getAdbInstance() {
|
||||
return AdbInstance.instance;
|
||||
}
|
||||
|
||||
export async function executeCmd(command: string) {
|
||||
let instance = getAdbInstance();
|
||||
|
||||
if (!instance) {
|
||||
console.error('instance not found');
|
||||
return {};
|
||||
}
|
||||
|
||||
try {
|
||||
if (instance?.subprocess.shellProtocol?.isSupported) {
|
||||
const result = await instance.subprocess.shellProtocol.spawnWaitText(command);
|
||||
return {
|
||||
output: result.stdout,
|
||||
error: result.stderr,
|
||||
exitCode: result.exitCode
|
||||
};
|
||||
} else {
|
||||
const process = await instance.subprocess.noneProtocol.spawn(command);
|
||||
const reader = process.output.getReader();
|
||||
const chunks = [];
|
||||
const decoder = new TextDecoder();
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
chunks.push(decoder.decode(value, { stream: true }));
|
||||
}
|
||||
|
||||
return {
|
||||
output: chunks.join('')
|
||||
};
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.log(e.message);
|
||||
//ExactReadable ended
|
||||
if (e.message.includes('ExactReadable ended')) {
|
||||
console.error('connection cut off');
|
||||
return {
|
||||
error: 'ExactReadableEndedError'
|
||||
};
|
||||
}
|
||||
|
||||
console.error('error while execute command', e);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
export async function disconnect() {
|
||||
let instance = getAdbInstance();
|
||||
if (instance) {
|
||||
await instance.close();
|
||||
console.log('close instance');
|
||||
saveAdbInstance(undefined);
|
||||
}
|
||||
}
|
||||
|
||||
export async function pull(filename: string) {
|
||||
let instance = getAdbInstance();
|
||||
if (instance) {
|
||||
let chunkList: Uint8Array<ArrayBufferLike>[] = [];
|
||||
let sync = await instance.sync();
|
||||
const content = sync.read(filename);
|
||||
let result = content.values();
|
||||
let res;
|
||||
|
||||
let result_string = '';
|
||||
|
||||
while ((res = await result.next()) != null) {
|
||||
// console.log(res.value);
|
||||
if (res.value != undefined) {
|
||||
result_string += new TextDecoder().decode(res.value);
|
||||
}
|
||||
if (res.done) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return result_string;
|
||||
}
|
||||
}
|
||||
|
||||
export async function push(path: string, obj: string) {
|
||||
let instance = getAdbInstance();
|
||||
if (instance) {
|
||||
let sync = await instance.sync();
|
||||
const encoder = new TextEncoder();
|
||||
|
||||
const file: ReadableStream<MaybeConsumable<Uint8Array>> = new ReadableStream({
|
||||
start(controller) {
|
||||
controller.enqueue(new Uint8Array(encoder.encode(obj)));
|
||||
controller.close();
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
console.log('support push v2', sync.supportsSendReceiveV2);
|
||||
|
||||
await sync.write({
|
||||
filename: path,
|
||||
file
|
||||
});
|
||||
} catch (error) {
|
||||
console.log('error while trying to write to machine', error);
|
||||
} finally {
|
||||
await sync.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// logcat stream
|
||||
|
||||
// TODO: screen mirror
|
||||
export function getScrcpyBinaryFromSource() {
|
||||
//https://github.com/Genymobile/scrcpy/releases
|
||||
}
|
||||
106
src/lib/core/adb/deviceCredManager.ts
Normal file
106
src/lib/core/adb/deviceCredManager.ts
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
import AdbWebCredentialStore from '@yume-chan/adb-credential-web';
|
||||
|
||||
export class DeviceCredentialManager {
|
||||
#credentialStore;
|
||||
constructor() {
|
||||
this.#credentialStore = new AdbWebCredentialStore();
|
||||
}
|
||||
|
||||
async saveDeviceInfo(device: any) {
|
||||
try {
|
||||
const deviceInfo = {
|
||||
name: device.name,
|
||||
serial: device.serial,
|
||||
vendorId: device.vendorId,
|
||||
productId: device.productId,
|
||||
lastConnected: new Date().toISOString()
|
||||
};
|
||||
|
||||
const storedDevices = this.getStoredDeviceInfos();
|
||||
storedDevices[device.serial] = deviceInfo;
|
||||
|
||||
localStorage.setItem('adb_device_infos', JSON.stringify(storedDevices));
|
||||
console.log('save device info', deviceInfo);
|
||||
} catch (error) {
|
||||
console.error('save device info error', error);
|
||||
}
|
||||
}
|
||||
|
||||
getStoredDeviceInfos() {
|
||||
try {
|
||||
const stored = localStorage.getItem('adb_device_infos');
|
||||
return stored ? JSON.parse(stored) : {};
|
||||
} catch (error) {
|
||||
console.error('unable to get stored device info', error);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
async hasStoredKeys() {
|
||||
try {
|
||||
for await (const key of this.#credentialStore.iterateKeys()) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
} catch (error) {
|
||||
console.error('check stored keys fail', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async getStoredKeyCount() {
|
||||
try {
|
||||
let count = 0;
|
||||
for await (const key of this.#credentialStore.iterateKeys()) {
|
||||
count++;
|
||||
}
|
||||
|
||||
return count;
|
||||
} catch (error) {
|
||||
console.error('get key stored count error', error);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
async clearAllCredentials() {
|
||||
try {
|
||||
let clearedCount = 0;
|
||||
const keys = [];
|
||||
for await (const key of this.#credentialStore.iterateKeys()) {
|
||||
keys.push(key);
|
||||
}
|
||||
|
||||
for (const key of keys) {
|
||||
try {
|
||||
clearedCount++;
|
||||
} catch (error) {
|
||||
console.error('clear error', error);
|
||||
}
|
||||
}
|
||||
|
||||
localStorage.removeItem('adb_device_infos');
|
||||
|
||||
try {
|
||||
const dbName = 'webadb-credentials';
|
||||
const request = indexedDB.deleteDatabase(dbName);
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
request.onsuccess = () => resolve(null);
|
||||
request.onerror = () => reject(request.error);
|
||||
request.onblocked = () => {
|
||||
console.warn('request delete got blocked');
|
||||
resolve(null);
|
||||
};
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
return clearedCount;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const deviceCredentialManager = new DeviceCredentialManager();
|
||||
17
src/lib/core/auth/domainBlocker.ts
Normal file
17
src/lib/core/auth/domainBlocker.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import { doc, getDoc } from "firebase/firestore";
|
||||
import { db } from "../client/firebase";
|
||||
|
||||
|
||||
export async function checkAllowAccess(userDomain: string): Promise<boolean> {
|
||||
|
||||
const docRef = doc(db, "whitelist", "allowedDomains");
|
||||
const snapshot = await getDoc(docRef);
|
||||
|
||||
if(snapshot.exists()){
|
||||
let domains = snapshot.data();
|
||||
// console.log(`domains: ${JSON.stringify(domains)}`);
|
||||
return domains["account_email"].includes(userDomain);
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
90
src/lib/core/auth/userPermissions.ts
Normal file
90
src/lib/core/auth/userPermissions.ts
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
import type { User } from "firebase/auth";
|
||||
import { addDoc, collection, doc, getDoc, setDoc, updateDoc } from "firebase/firestore";
|
||||
import { db } from "../client/firebase";
|
||||
|
||||
export enum UserPermissions {
|
||||
NO_PERMISSION,
|
||||
THAI_PERMISSION = 1 << 0,
|
||||
MALAY_PERMISSION = 1 << 1,
|
||||
AUS_PERMISSION = 1 << 2,
|
||||
ALPHA3_PERMISSION = 1 << 3,
|
||||
|
||||
VIEWER = 1 << 4,
|
||||
EDITOR = 1 << 7,
|
||||
|
||||
DUBAI_PERMISSION = 1 << 8,
|
||||
COUNTER_PERMISSION = 1 << 9,
|
||||
SINGAPORE_PERMISSION = 1 << 10,
|
||||
COCKTAIL_PERMISSION = 1 << 11,
|
||||
|
||||
// add new permission by shifting after 7. eg. 8,9,...
|
||||
// also do add at server
|
||||
|
||||
SUPER_ADMIN_PERMISSION = THAI_PERMISSION |
|
||||
MALAY_PERMISSION |
|
||||
AUS_PERMISSION |
|
||||
ALPHA3_PERMISSION |
|
||||
COUNTER_PERMISSION |
|
||||
SINGAPORE_PERMISSION |
|
||||
DUBAI_PERMISSION |
|
||||
COCKTAIL_PERMISSION |
|
||||
(EDITOR | VIEWER),
|
||||
}
|
||||
|
||||
export function getPermissions(perms: number): UserPermissions[] {
|
||||
return Object.values(UserPermissions).filter(
|
||||
(permission) =>
|
||||
typeof permission === "number" && (perms & permission) !== 0,
|
||||
) as UserPermissions[];
|
||||
}
|
||||
|
||||
export function getDefaultPermission(): UserPermissions {
|
||||
return UserPermissions.NO_PERMISSION;
|
||||
}
|
||||
|
||||
export async function getUserPermission(user: User | null): Promise<string[]> {
|
||||
if(user == null){
|
||||
return [];
|
||||
}
|
||||
let qid = user.uid;
|
||||
let defaultPerms = ["no_permission"];
|
||||
// TODO: collect only important fields
|
||||
const ignoredFields = [
|
||||
"apiKey",
|
||||
];
|
||||
const docRef = doc(db, "users", "data");
|
||||
|
||||
|
||||
const snapshot = await getDoc(docRef);
|
||||
if(snapshot.exists()){
|
||||
let user_data = snapshot.data();
|
||||
if(Object.keys(user_data).includes(qid)){
|
||||
return user_data[qid]["permissions"];
|
||||
} else {
|
||||
|
||||
let umap: any = user.toJSON();
|
||||
umap["permissions"] = defaultPerms;
|
||||
umap["role"] = "guest";
|
||||
|
||||
for(let ignoredField of ignoredFields){
|
||||
umap[ignoredField] = undefined;
|
||||
}
|
||||
|
||||
let cleaned_umap: any = {};
|
||||
for(let k of Object.keys(umap)){
|
||||
if(umap[k] != undefined){
|
||||
cleaned_umap[k] = umap[k];
|
||||
}
|
||||
}
|
||||
|
||||
let fmap: any = {};
|
||||
fmap[qid] = cleaned_umap;
|
||||
|
||||
await updateDoc(doc(db, "users", "data"), fmap);
|
||||
|
||||
return defaultPerms;
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
38
src/lib/core/client/server.ts
Normal file
38
src/lib/core/client/server.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import { get } from 'svelte/store';
|
||||
import { departmentStore } from '../stores/departments';
|
||||
import { sendMessage } from '../handlers/ws_messageSender';
|
||||
import { auth } from '../stores/auth';
|
||||
import { extractCookieOnNonBrowser } from '$lib/helpers/cookie';
|
||||
import { browser } from '$app/environment';
|
||||
|
||||
export async function getRecipes() {
|
||||
if (browser && !get(departmentStore)) {
|
||||
console.log('cannot get dep', get(departmentStore));
|
||||
return [];
|
||||
}
|
||||
|
||||
let countryTarget = get(departmentStore);
|
||||
let country = '';
|
||||
|
||||
// if (!countryTarget && !browser) {
|
||||
// countryTarget = extractCookieOnNonBrowser()['department'];
|
||||
// }
|
||||
|
||||
// construct path. fetch (GET) {server}/recipe/{countryTarget}/{version}
|
||||
let idToken = await get(auth)?.getIdToken();
|
||||
|
||||
console.log('country target get recipe', country);
|
||||
|
||||
sendMessage({
|
||||
type: 'recipe',
|
||||
payload: {
|
||||
auth: idToken ?? '',
|
||||
partial: false,
|
||||
country: countryTarget ?? '',
|
||||
version: -1,
|
||||
parameters: ''
|
||||
}
|
||||
});
|
||||
|
||||
return [];
|
||||
}
|
||||
98
src/lib/core/handlers/messageHandler.ts
Normal file
98
src/lib/core/handlers/messageHandler.ts
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
import { get, writable } from 'svelte/store';
|
||||
import { addNotification, notiStore } from '../stores/noti';
|
||||
import {
|
||||
recipeData,
|
||||
recipeDataError,
|
||||
recipeLoading,
|
||||
recipeOverviewData,
|
||||
recipeStreamMeta
|
||||
} from '../stores/recipeStore';
|
||||
|
||||
export const messages = writable<string[]>([]);
|
||||
|
||||
type WSMessage = { type: string; payload: any };
|
||||
|
||||
const handlers: Record<string, (payload: any) => void> = {
|
||||
chat: (p) => messages.update((m) => [...m, p]),
|
||||
ping: (p) => console.log('ping from server'),
|
||||
recipeResponse: (p) => {
|
||||
let recipe_result = p.result;
|
||||
let recipe_request = p.request;
|
||||
|
||||
if (recipe_result) {
|
||||
addNotification('INFO:Start fetch recipe!');
|
||||
}
|
||||
},
|
||||
stream_data_start: (p) => {
|
||||
let stream_id = p.stream_id;
|
||||
let total_size = p.total_size;
|
||||
let chunk_size = p.chunk_size;
|
||||
|
||||
if (stream_id) {
|
||||
addNotification('INFO:Start streaming data');
|
||||
recipeLoading.set(true);
|
||||
recipeStreamMeta.set({
|
||||
id: stream_id,
|
||||
total_size: total_size,
|
||||
chunk_size: chunk_size,
|
||||
progress: 0
|
||||
});
|
||||
recipeData.set([]);
|
||||
recipeOverviewData.set([]);
|
||||
}
|
||||
},
|
||||
stream_data_error: (p) => {
|
||||
recipeLoading.set(false);
|
||||
recipeDataError.set(p);
|
||||
|
||||
setTimeout(() => {
|
||||
addNotification(`ERROR:${p.error}`);
|
||||
}, 2000);
|
||||
},
|
||||
stream_data_chunk: (p) => {
|
||||
let current_meta = get(recipeStreamMeta);
|
||||
if (current_meta) {
|
||||
let stream_id = current_meta.id;
|
||||
|
||||
let progress_response_id = p.stream_id;
|
||||
if (stream_id === progress_response_id) {
|
||||
let current_response_end = p.start_idx + current_meta.chunk_size;
|
||||
let percent = (current_response_end / current_meta.total_size) * 100;
|
||||
if (percent > 100) {
|
||||
percent = 100;
|
||||
}
|
||||
let data = p.data;
|
||||
let currentData = get(recipeData);
|
||||
for (let rp of data) {
|
||||
currentData.push(rp);
|
||||
}
|
||||
recipeData.set(currentData);
|
||||
recipeStreamMeta.set({
|
||||
...current_meta,
|
||||
progress: percent
|
||||
});
|
||||
|
||||
// build overview
|
||||
|
||||
if (percent == 100) {
|
||||
addNotification(`INFO:Current progress ${percent}%`);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
stream_data_end: (p) => {
|
||||
recipeLoading.set(false);
|
||||
},
|
||||
stream_patch_update: (p) => {}
|
||||
};
|
||||
|
||||
export function handleIncomingMessages(raw: string) {
|
||||
const msg: WSMessage = JSON.parse(raw);
|
||||
console.log(`${new Date().toLocaleTimeString()}:ws msg`, msg);
|
||||
if (msg == null) {
|
||||
// error response
|
||||
addNotification('ERR:No response from server');
|
||||
return;
|
||||
}
|
||||
handlers[msg.type]?.(msg.payload);
|
||||
}
|
||||
41
src/lib/core/handlers/permissionHandler.ts
Normal file
41
src/lib/core/handlers/permissionHandler.ts
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import { get } from "svelte/store";
|
||||
import { permission as currentPermissions } from "$lib/core/stores/permissions";
|
||||
|
||||
|
||||
|
||||
const splitPermCache = new Map<string, string[]>();
|
||||
function splitPerm(p: string): string[]{
|
||||
if(!splitPermCache.has(p)){
|
||||
splitPermCache.set(p, p.split("."));
|
||||
}
|
||||
return splitPermCache.get(p)!;
|
||||
}
|
||||
|
||||
/// Check if current user has exacted permissions
|
||||
export function requirePermission(...permissions: string[]): boolean {
|
||||
// let perms = get(currentPermissions);
|
||||
// let countOk = 0;
|
||||
// for(let perm of perms){
|
||||
// if(permissions.includes(perm)){
|
||||
// countOk += 1;
|
||||
// }
|
||||
// }
|
||||
// return countOk > 0 && countOk == perms.length;
|
||||
const userPerms = get(currentPermissions);
|
||||
return permissions.every(req => {
|
||||
return userPerms.includes(req);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/// Check permission of user by
|
||||
export function needPermission(...permissions: string[]): boolean {
|
||||
const userPerms = get(currentPermissions).map(p => splitPerm(p));
|
||||
return permissions.every(req => {
|
||||
const reqParts = splitPerm(req);
|
||||
return userPerms.some(userParts => {
|
||||
if(userParts.length !== reqParts.length) return false;
|
||||
return reqParts.every((part, i) => part === "*" || part === userParts[i]);
|
||||
});
|
||||
});
|
||||
}
|
||||
26
src/lib/core/handlers/ws_messageSender.ts
Normal file
26
src/lib/core/handlers/ws_messageSender.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import { get, writable } from 'svelte/store';
|
||||
import type { OutMessage } from '../types/outMessage';
|
||||
import { socketStore } from '../stores/websocketStore';
|
||||
import { addNotification } from '../stores/noti';
|
||||
|
||||
export const queue = writable<string[]>([]);
|
||||
|
||||
export function sendMessage(msg: OutMessage): boolean {
|
||||
const socket = get(socketStore);
|
||||
const data = JSON.stringify(msg);
|
||||
|
||||
if (!socket || socket.readyState !== WebSocket.OPEN) {
|
||||
console.warn('WebSocket not connected, put to queue');
|
||||
|
||||
let currentQueue = get(queue);
|
||||
currentQueue.push(data);
|
||||
queue.set(currentQueue);
|
||||
|
||||
addNotification('WARN:Queuing overview view request');
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
socket.send(data);
|
||||
return true;
|
||||
}
|
||||
9
src/lib/core/stores/auth.ts
Normal file
9
src/lib/core/stores/auth.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import type { User } from "firebase/auth";
|
||||
import { writable } from "svelte/store";
|
||||
|
||||
// type User = {
|
||||
// uid: string,
|
||||
// email: string,
|
||||
// };
|
||||
|
||||
export const auth = writable<User | null>(null);
|
||||
3
src/lib/core/stores/departments.ts
Normal file
3
src/lib/core/stores/departments.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
import { writable } from 'svelte/store';
|
||||
|
||||
export const departmentStore = writable<string | undefined>();
|
||||
1
src/lib/core/stores/machineFiles.ts
Normal file
1
src/lib/core/stores/machineFiles.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
/// save files' content
|
||||
4
src/lib/core/stores/machineInfoStore.ts
Normal file
4
src/lib/core/stores/machineInfoStore.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
import type { MachineInfo } from '$lib/models/machineInfo.model';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
export const machineInfoStore = writable<MachineInfo | undefined>();
|
||||
41
src/lib/core/stores/noti.ts
Normal file
41
src/lib/core/stores/noti.ts
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import { toast } from 'svelte-sonner';
|
||||
import { get, writable } from 'svelte/store';
|
||||
|
||||
// save notifications to user
|
||||
export const notiStore = writable<string[]>([]);
|
||||
|
||||
export function addNotification(msg: string) {
|
||||
let current = get(notiStore);
|
||||
current.push(msg);
|
||||
notiStore.set(current);
|
||||
}
|
||||
|
||||
export function getNotification() {
|
||||
let current = get(notiStore);
|
||||
let first = current.shift();
|
||||
if (first) {
|
||||
let msg_p = first.split(':');
|
||||
let msg_level_type = msg_p[0];
|
||||
let msg = msg_p[1];
|
||||
|
||||
switch (msg_level_type) {
|
||||
case 'ERR':
|
||||
toast.error('Error', {
|
||||
description: msg
|
||||
});
|
||||
break;
|
||||
case 'WARN':
|
||||
toast.warning('Warning', {
|
||||
description: msg
|
||||
});
|
||||
default:
|
||||
toast(msg);
|
||||
}
|
||||
}
|
||||
|
||||
notiStore.set(current);
|
||||
}
|
||||
|
||||
setInterval(() => {
|
||||
getNotification();
|
||||
}, 100);
|
||||
4
src/lib/core/stores/permissions.ts
Normal file
4
src/lib/core/stores/permissions.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
import { writable } from "svelte/store";
|
||||
|
||||
// blocking views by permission of user
|
||||
export const permission = writable<string[]>([]);
|
||||
67
src/lib/core/stores/recipeStore.ts
Normal file
67
src/lib/core/stores/recipeStore.ts
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
import { writable } from 'svelte/store';
|
||||
import type { RecipeOverview } from '../../../routes/(authed)/recipe/overview/columns';
|
||||
import type { Material } from '$lib/models/material.model';
|
||||
|
||||
export const recipeData = writable<any>(null);
|
||||
export const recipeLoading = writable(false);
|
||||
export const recipeDataError = writable<string | null>(null);
|
||||
export const recipeStreamMeta = writable<{
|
||||
id: string;
|
||||
total_size: number;
|
||||
chunk_size: number;
|
||||
progress: number;
|
||||
} | null>(null);
|
||||
|
||||
// from server
|
||||
export const recipeOverviewData = writable<RecipeOverview[] | null>(null);
|
||||
export const materialData = writable<Material | undefined>();
|
||||
|
||||
// machine recipe
|
||||
export const recipeFromMachine = writable<any>(null);
|
||||
export const recipeFromMachineLoading = writable(false);
|
||||
export const recipeFromMachineError = writable<string | null>(null);
|
||||
|
||||
// NOTE: must not have any nested structures
|
||||
// { recipe: {}, materials: {}, toppings: { groups: {}, lists: {} } }
|
||||
export const recipeFromMachineQuery = writable<any>({});
|
||||
export const materialFromMachineQuery = writable<any>({});
|
||||
|
||||
export const referenceFromPage = writable<string>('');
|
||||
|
||||
let worker: Worker | null = null;
|
||||
let initialized = false;
|
||||
|
||||
export function loadRecipe(url: string) {
|
||||
if (initialized) return;
|
||||
initialized = true;
|
||||
|
||||
recipeLoading.set(true);
|
||||
|
||||
worker = new Worker(new URL('../../workers/data.worker.ts', import.meta.url), {
|
||||
type: 'module'
|
||||
});
|
||||
|
||||
worker.onmessage = (e) => {
|
||||
const { type, payload } = e.data;
|
||||
if (type === 'data') {
|
||||
recipeData.set(payload);
|
||||
recipeLoading.set(false);
|
||||
}
|
||||
|
||||
if (type === 'error') {
|
||||
recipeDataError.set(payload);
|
||||
recipeLoading.set(false);
|
||||
}
|
||||
};
|
||||
|
||||
worker.postMessage({ url });
|
||||
}
|
||||
|
||||
export function getWorker() {
|
||||
if (!worker) {
|
||||
worker = new Worker(new URL('../../workers/data.worker.ts', import.meta.url), {
|
||||
type: 'module'
|
||||
});
|
||||
}
|
||||
return worker;
|
||||
}
|
||||
3
src/lib/core/stores/sidebar.ts
Normal file
3
src/lib/core/stores/sidebar.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
import { writable } from 'svelte/store';
|
||||
|
||||
export const sidebarStore = writable<boolean>(true);
|
||||
46
src/lib/core/stores/websocketStore.ts
Normal file
46
src/lib/core/stores/websocketStore.ts
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
import { browser } from '$app/environment';
|
||||
import { env } from '$env/dynamic/public';
|
||||
import { get, writable } from 'svelte/store';
|
||||
import { handleIncomingMessages } from '../handlers/messageHandler';
|
||||
import { queue as msgQueue } from '../handlers/ws_messageSender';
|
||||
|
||||
export const socketStore = writable<WebSocket | null>(null, (set) => {
|
||||
if (browser) {
|
||||
console.log('connecting to ', env.PUBLIC_WSS);
|
||||
const socket = new WebSocket(`${env.PUBLIC_WSS}`);
|
||||
|
||||
socket.addEventListener('open', () => {
|
||||
set(socket);
|
||||
|
||||
// recover messages on connect, flushing
|
||||
while (get(msgQueue).length) {
|
||||
let queue = get(msgQueue);
|
||||
let current = queue.shift();
|
||||
if (current) {
|
||||
socket.send(current);
|
||||
// set next
|
||||
msgQueue.set(queue);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
socket.addEventListener('message', (event) => {
|
||||
handleIncomingMessages(event.data);
|
||||
});
|
||||
|
||||
socket.addEventListener('close', () => {
|
||||
set(null);
|
||||
});
|
||||
|
||||
socket.addEventListener('error', (e) => {
|
||||
console.log('WebSocket error: ', e);
|
||||
set(null);
|
||||
});
|
||||
|
||||
return () => {
|
||||
if (socket.readyState === WebSocket.OPEN) {
|
||||
socket.close();
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
24
src/lib/core/types/menuStatus.ts
Normal file
24
src/lib/core/types/menuStatus.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
enum MenuStatus {
|
||||
ready,
|
||||
obsolete = 2,
|
||||
pendingOnline = 11,
|
||||
pendingOffline,
|
||||
drafted = 99
|
||||
}
|
||||
|
||||
function matchMenuStatus(status: number): MenuStatus {
|
||||
switch (status) {
|
||||
case 0:
|
||||
return MenuStatus.ready;
|
||||
case 2:
|
||||
return MenuStatus.obsolete;
|
||||
case 11:
|
||||
return MenuStatus.pendingOnline;
|
||||
case 12:
|
||||
return MenuStatus.pendingOffline;
|
||||
default:
|
||||
return MenuStatus.drafted;
|
||||
}
|
||||
}
|
||||
|
||||
export { MenuStatus, matchMenuStatus };
|
||||
25
src/lib/core/types/outMessage.ts
Normal file
25
src/lib/core/types/outMessage.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
export type OutMessage =
|
||||
| { type: 'chat'; payload: string }
|
||||
| { type: 'ping' }
|
||||
| { type: 'lock'; payload: { field: string } }
|
||||
| { type: 'general'; payload: string }
|
||||
| {
|
||||
type: 'recipe';
|
||||
payload: {
|
||||
auth: string;
|
||||
partial: boolean;
|
||||
country: string;
|
||||
version: number;
|
||||
parameters: string;
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: 'auth';
|
||||
payload: {
|
||||
user: {
|
||||
name: string;
|
||||
email: string;
|
||||
permissions: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue