mirror of
https://github.com/ivabus/gui
synced 2025-06-08 00:00:27 +03:00
parent
d300efd805
commit
6c3be19da2
166 changed files with 7717 additions and 7703 deletions
|
@ -91,7 +91,7 @@ pnpm dev
|
||||||
## Prettier
|
## Prettier
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
pnpm run -r format
|
pnpm run --reporter append-only -r format
|
||||||
```
|
```
|
||||||
|
|
||||||
## Dist
|
## Dist
|
||||||
|
|
|
@ -1,31 +1,31 @@
|
||||||
module.exports = {
|
module.exports = {
|
||||||
root: true,
|
root: true,
|
||||||
globals: {
|
globals: {
|
||||||
NodeJS: true
|
NodeJS: true
|
||||||
},
|
},
|
||||||
parser: "@typescript-eslint/parser",
|
parser: "@typescript-eslint/parser",
|
||||||
extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended", "prettier"],
|
extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended", "prettier"],
|
||||||
plugins: ["svelte3", "@typescript-eslint"],
|
plugins: ["svelte3", "@typescript-eslint"],
|
||||||
ignorePatterns: ["*.cjs"],
|
ignorePatterns: ["*.cjs"],
|
||||||
overrides: [
|
overrides: [
|
||||||
{
|
{
|
||||||
files: ["*.svelte"],
|
files: ["*.svelte"],
|
||||||
processor: "svelte3/svelte3"
|
processor: "svelte3/svelte3"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
settings: {
|
settings: {
|
||||||
"svelte3/typescript": () => require("typescript")
|
"svelte3/typescript": () => require("typescript")
|
||||||
},
|
},
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
sourceType: "module",
|
sourceType: "module",
|
||||||
ecmaVersion: 2020
|
ecmaVersion: 2020
|
||||||
},
|
},
|
||||||
env: {
|
env: {
|
||||||
browser: true,
|
browser: true,
|
||||||
es2017: true,
|
es2017: true,
|
||||||
node: true
|
node: true
|
||||||
},
|
},
|
||||||
rules: {
|
rules: {
|
||||||
"@typescript-eslint/ban-ts-comment": ["error", { "ts-ignore": "allow-with-description" }]
|
"@typescript-eslint/ban-ts-comment": ["error", { "ts-ignore": "allow-with-description" }]
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
{
|
{
|
||||||
"tabWidth": 2,
|
"tabWidth": 2,
|
||||||
"useTabs": true,
|
"useTabs": false,
|
||||||
"singleQuote": false,
|
"singleQuote": false,
|
||||||
"trailingComma": "none",
|
"trailingComma": "none",
|
||||||
"printWidth": 100,
|
"printWidth": 100,
|
||||||
"plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"],
|
"plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"],
|
||||||
"pluginSearchDirs": ["."],
|
"pluginSearchDirs": ["../../node_modules"],
|
||||||
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
|
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,59 +5,59 @@ const otaClient = require("@crowdin/ota-client");
|
||||||
const _ = require("lodash");
|
const _ = require("lodash");
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
appId: "xyz.tea.gui",
|
appId: "xyz.tea.gui",
|
||||||
productName: "tea",
|
productName: "tea",
|
||||||
asar: false,
|
asar: false,
|
||||||
directories: { output: "dist" },
|
directories: { output: "dist" },
|
||||||
files: ["electron/dist/electron.cjs", { from: "build", to: "" }],
|
files: ["electron/dist/electron.cjs", { from: "build", to: "" }],
|
||||||
linux: {
|
linux: {
|
||||||
icon: "./icon.png"
|
icon: "./icon.png"
|
||||||
},
|
},
|
||||||
mac: {
|
mac: {
|
||||||
icon: "./electron/icon.icns",
|
icon: "./electron/icon.icns",
|
||||||
target: {
|
target: {
|
||||||
target: "default",
|
target: "default",
|
||||||
arch: ["x64", "arm64"]
|
arch: ["x64", "arm64"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
afterSign: async (params) => {
|
afterSign: async (params) => {
|
||||||
if (process.platform !== "darwin" || process.env.CSC_IDENTITY_AUTO_DISCOVERY === "false") {
|
if (process.platform !== "darwin" || process.env.CSC_IDENTITY_AUTO_DISCOVERY === "false") {
|
||||||
console.log("not notarizing app");
|
console.log("not notarizing app");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("afterSign hook triggered");
|
console.log("afterSign hook triggered");
|
||||||
|
|
||||||
const appBundleId = "xyz.tea.gui";
|
const appBundleId = "xyz.tea.gui";
|
||||||
|
|
||||||
let appPath = path.join(params.appOutDir, `${params.packager.appInfo.productFilename}.app`);
|
let appPath = path.join(params.appOutDir, `${params.packager.appInfo.productFilename}.app`);
|
||||||
if (!fs.existsSync(appPath)) {
|
if (!fs.existsSync(appPath)) {
|
||||||
console.log("skip");
|
console.log("skip");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
`Notarizing ${appBundleId} found at ${appPath} with Apple ID ${process.env.APPLE_ID}`
|
`Notarizing ${appBundleId} found at ${appPath} with Apple ID ${process.env.APPLE_ID}`
|
||||||
);
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await notarize({
|
await notarize({
|
||||||
appBundleId,
|
appBundleId,
|
||||||
appPath,
|
appPath,
|
||||||
appleId: process.env.APPLE_ID,
|
appleId: process.env.APPLE_ID,
|
||||||
appleIdPassword: process.env.APPLE_APP_SPECIFIC_PASSWORD
|
appleIdPassword: process.env.APPLE_APP_SPECIFIC_PASSWORD
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Done notarizing`);
|
console.log(`Done notarizing`);
|
||||||
},
|
},
|
||||||
// this determines the configuration of the auto-update feature
|
// this determines the configuration of the auto-update feature
|
||||||
publish: {
|
publish: {
|
||||||
provider: "generic",
|
provider: "generic",
|
||||||
// TODO: replace this with tea branded domain: gui-dist.tea.xyz
|
// TODO: replace this with tea branded domain: gui-dist.tea.xyz
|
||||||
// url: "https://d2ovumu63qzbn6.cloudfront.net/"
|
// url: "https://d2ovumu63qzbn6.cloudfront.net/"
|
||||||
url: "https://s3.amazonaws.com/preview.gui.tea.xyz/release"
|
url: "https://s3.amazonaws.com/preview.gui.tea.xyz/release"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -11,7 +11,7 @@ import { checkUpdater } from "./libs/auto-updater";
|
||||||
|
|
||||||
import initializeHandlers, { setProtocolPath } from "./libs/ipc";
|
import initializeHandlers, { setProtocolPath } from "./libs/ipc";
|
||||||
import initializePushNotification, {
|
import initializePushNotification, {
|
||||||
syncPackageTopicSubscriptions
|
syncPackageTopicSubscriptions
|
||||||
} from "./libs/push-notification";
|
} from "./libs/push-notification";
|
||||||
|
|
||||||
import init from "./libs/initialize";
|
import init from "./libs/initialize";
|
||||||
|
@ -19,26 +19,26 @@ import { readSessionData } from "./libs/auth";
|
||||||
|
|
||||||
log.info("App starting...");
|
log.info("App starting...");
|
||||||
if (app.isPackaged) {
|
if (app.isPackaged) {
|
||||||
Sentry.init({
|
Sentry.init({
|
||||||
dsn: "https://5ff29bb5b3b64cd4bd4f4960ef1db2e3@o4504750197899264.ingest.sentry.io/4504750206746624",
|
dsn: "https://5ff29bb5b3b64cd4bd4f4960ef1db2e3@o4504750197899264.ingest.sentry.io/4504750206746624",
|
||||||
debug: true,
|
debug: true,
|
||||||
transportOptions: {
|
transportOptions: {
|
||||||
maxQueueAgeDays: 30,
|
maxQueueAgeDays: 30,
|
||||||
maxQueueCount: 30,
|
maxQueueCount: 30,
|
||||||
beforeSend: async () => {
|
beforeSend: async () => {
|
||||||
const ol = await net.isOnline();
|
const ol = await net.isOnline();
|
||||||
return ol ? "send" : "queue";
|
return ol ? "send" : "queue";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
Sentry.configureScope(async (scope) => {
|
Sentry.configureScope(async (scope) => {
|
||||||
const session = await readSessionData();
|
const session = await readSessionData();
|
||||||
scope.setUser({
|
scope.setUser({
|
||||||
id: session.device_id, // device_id this should exist in our pg db: developer_id is to many device_id
|
id: session.device_id, // device_id this should exist in our pg db: developer_id is to many device_id
|
||||||
username: session?.user?.login || "" // github username or handler
|
username: session?.user?.login || "" // github username or handler
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
setSentryLogging(Sentry);
|
setSentryLogging(Sentry);
|
||||||
}
|
}
|
||||||
|
|
||||||
init();
|
init();
|
||||||
|
@ -56,148 +56,148 @@ setupTitlebar();
|
||||||
initializeHandlers({ notifyMainWindow });
|
initializeHandlers({ notifyMainWindow });
|
||||||
|
|
||||||
function createWindow() {
|
function createWindow() {
|
||||||
const windowState = windowStateManager({
|
const windowState = windowStateManager({
|
||||||
defaultWidth: 1000,
|
defaultWidth: 1000,
|
||||||
defaultHeight: 600
|
defaultHeight: 600
|
||||||
});
|
});
|
||||||
|
|
||||||
const mainWindow = new BrowserWindow({
|
const mainWindow = new BrowserWindow({
|
||||||
titleBarStyle: "hidden",
|
titleBarStyle: "hidden",
|
||||||
backgroundColor: "black",
|
backgroundColor: "black",
|
||||||
autoHideMenuBar: true,
|
autoHideMenuBar: true,
|
||||||
trafficLightPosition: {
|
trafficLightPosition: {
|
||||||
x: 14,
|
x: 14,
|
||||||
y: 15
|
y: 15
|
||||||
},
|
},
|
||||||
minHeight: 600,
|
minHeight: 600,
|
||||||
minWidth: 1000,
|
minWidth: 1000,
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
contextIsolation: false,
|
contextIsolation: false,
|
||||||
nodeIntegration: true,
|
nodeIntegration: true,
|
||||||
spellcheck: false,
|
spellcheck: false,
|
||||||
webSecurity: false,
|
webSecurity: false,
|
||||||
devTools: allowDebug,
|
devTools: allowDebug,
|
||||||
preload: path.join(app.getAppPath(), "preload.cjs")
|
preload: path.join(app.getAppPath(), "preload.cjs")
|
||||||
},
|
},
|
||||||
x: windowState.x,
|
x: windowState.x,
|
||||||
y: windowState.y,
|
y: windowState.y,
|
||||||
width: windowState.width,
|
width: windowState.width,
|
||||||
height: windowState.height
|
height: windowState.height
|
||||||
});
|
});
|
||||||
|
|
||||||
windowState.manage(mainWindow);
|
windowState.manage(mainWindow);
|
||||||
mainWindow.webContents.openDevTools();
|
mainWindow.webContents.openDevTools();
|
||||||
mainWindow.once("ready-to-show", () => {
|
mainWindow.once("ready-to-show", () => {
|
||||||
mainWindow.show();
|
mainWindow.show();
|
||||||
mainWindow.focus();
|
mainWindow.focus();
|
||||||
});
|
});
|
||||||
|
|
||||||
mainWindow.on("close", () => {
|
mainWindow.on("close", () => {
|
||||||
windowState.saveState(mainWindow);
|
windowState.saveState(mainWindow);
|
||||||
});
|
});
|
||||||
|
|
||||||
mainWindow.webContents.on("did-finish-load", () => {
|
mainWindow.webContents.on("did-finish-load", () => {
|
||||||
initializePushNotification(mainWindow);
|
initializePushNotification(mainWindow);
|
||||||
});
|
});
|
||||||
|
|
||||||
attachTitlebarToWindow(mainWindow);
|
attachTitlebarToWindow(mainWindow);
|
||||||
return mainWindow;
|
return mainWindow;
|
||||||
}
|
}
|
||||||
|
|
||||||
contextMenu({
|
contextMenu({
|
||||||
showLookUpSelection: false,
|
showLookUpSelection: false,
|
||||||
showSearchWithGoogle: false,
|
showSearchWithGoogle: false,
|
||||||
showCopyImage: false
|
showCopyImage: false
|
||||||
});
|
});
|
||||||
|
|
||||||
function loadVite(port) {
|
function loadVite(port) {
|
||||||
mainWindow?.loadURL(`http://localhost:${port}?is-vite=true`).catch((e) => {
|
mainWindow?.loadURL(`http://localhost:${port}?is-vite=true`).catch((e) => {
|
||||||
console.log("Error loading URL, retrying", e);
|
console.log("Error loading URL, retrying", e);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
loadVite(port);
|
loadVite(port);
|
||||||
}, 200);
|
}, 200);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function createMainWindow() {
|
function createMainWindow() {
|
||||||
if (mainWindow) {
|
if (mainWindow) {
|
||||||
if (mainWindow.isMinimized()) mainWindow.restore();
|
if (mainWindow.isMinimized()) mainWindow.restore();
|
||||||
mainWindow.focus();
|
mainWindow.focus();
|
||||||
} else {
|
} else {
|
||||||
mainWindow = createWindow();
|
mainWindow = createWindow();
|
||||||
}
|
}
|
||||||
|
|
||||||
checkUpdater(mainWindow);
|
checkUpdater(mainWindow);
|
||||||
|
|
||||||
mainWindow.once("close", () => {
|
mainWindow.once("close", () => {
|
||||||
mainWindow = null;
|
mainWindow = null;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!app.isPackaged) {
|
if (!app.isPackaged) {
|
||||||
// dev
|
// dev
|
||||||
loadVite(port);
|
loadVite(port);
|
||||||
} else {
|
} else {
|
||||||
serveURL(mainWindow);
|
serveURL(mainWindow);
|
||||||
}
|
}
|
||||||
|
|
||||||
syncPackageTopicSubscriptions();
|
syncPackageTopicSubscriptions();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (process.defaultApp) {
|
if (process.defaultApp) {
|
||||||
app.setAsDefaultProtocolClient("tea", process.execPath, [path.resolve(process.argv[1])]);
|
app.setAsDefaultProtocolClient("tea", process.execPath, [path.resolve(process.argv[1])]);
|
||||||
} else {
|
} else {
|
||||||
app.setAsDefaultProtocolClient("tea");
|
app.setAsDefaultProtocolClient("tea");
|
||||||
}
|
}
|
||||||
|
|
||||||
app.once("ready", createMainWindow);
|
app.once("ready", createMainWindow);
|
||||||
|
|
||||||
app.on("activate", () => {
|
app.on("activate", () => {
|
||||||
if (!mainWindow) {
|
if (!mainWindow) {
|
||||||
createMainWindow();
|
createMainWindow();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
app.on("window-all-closed", () => {
|
app.on("window-all-closed", () => {
|
||||||
// mac ux is just minimize them when closed unless forced quite CMD+Q
|
// mac ux is just minimize them when closed unless forced quite CMD+Q
|
||||||
macWindowClosed = true;
|
macWindowClosed = true;
|
||||||
if (process.platform !== "darwin") {
|
if (process.platform !== "darwin") {
|
||||||
app.quit();
|
app.quit();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// NOTE: this doesnt work in linux
|
// NOTE: this doesnt work in linux
|
||||||
// you have to loop through process.argv to figure out which url launched the app
|
// you have to loop through process.argv to figure out which url launched the app
|
||||||
app.on("open-url", (event, url) => {
|
app.on("open-url", (event, url) => {
|
||||||
// ie url: tea://packages/slug
|
// ie url: tea://packages/slug
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
const packagesPrefix = "/packages/";
|
const packagesPrefix = "/packages/";
|
||||||
|
|
||||||
let rawPath = url.replace("tea:/", "");
|
let rawPath = url.replace("tea:/", "");
|
||||||
|
|
||||||
const isPackage = url.includes(packagesPrefix);
|
const isPackage = url.includes(packagesPrefix);
|
||||||
if (isPackage) {
|
if (isPackage) {
|
||||||
// /packages/github.com/pypa/twine -> /packages/github_com_pypa_twine
|
// /packages/github.com/pypa/twine -> /packages/github_com_pypa_twine
|
||||||
const packageSlug = nameToSlug(rawPath.replace(packagesPrefix, ""));
|
const packageSlug = nameToSlug(rawPath.replace(packagesPrefix, ""));
|
||||||
rawPath = [packagesPrefix, packageSlug].join("");
|
rawPath = [packagesPrefix, packageSlug].join("");
|
||||||
}
|
}
|
||||||
|
|
||||||
setProtocolPath(rawPath);
|
setProtocolPath(rawPath);
|
||||||
|
|
||||||
if (mainWindow) {
|
if (mainWindow) {
|
||||||
if (mainWindow.isMinimized()) {
|
if (mainWindow.isMinimized()) {
|
||||||
mainWindow.restore();
|
mainWindow.restore();
|
||||||
log.info("restored");
|
log.info("restored");
|
||||||
}
|
}
|
||||||
mainWindow.webContents.send("sync-path");
|
mainWindow.webContents.send("sync-path");
|
||||||
log.info("synced path", rawPath);
|
log.info("synced path", rawPath);
|
||||||
} else if (macWindowClosed) {
|
} else if (macWindowClosed) {
|
||||||
log.info("open new window");
|
log.info("open new window");
|
||||||
createMainWindow();
|
createMainWindow();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function notifyMainWindow(channel: string, data: unknown) {
|
function notifyMainWindow(channel: string, data: unknown) {
|
||||||
if (mainWindow) {
|
if (mainWindow) {
|
||||||
mainWindow.webContents.send(channel, data);
|
mainWindow.webContents.send(channel, data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,145 +10,145 @@ const sessionFilePath = path.join(getTeaPath(), "tea.xyz/gui/tmp.dat");
|
||||||
const sessionFolder = path.join(getTeaPath(), "tea.xyz/gui");
|
const sessionFolder = path.join(getTeaPath(), "tea.xyz/gui");
|
||||||
|
|
||||||
export interface Session {
|
export interface Session {
|
||||||
device_id?: string;
|
device_id?: string;
|
||||||
key?: string;
|
key?: string;
|
||||||
user?: any;
|
user?: any;
|
||||||
locale?: string;
|
locale?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
let sessionMemory: Session = { device_id: "", locale: "en" };
|
let sessionMemory: Session = { device_id: "", locale: "en" };
|
||||||
const initialized: Promise<Session> = new Promise((resolve, reject) => {
|
const initialized: Promise<Session> = new Promise((resolve, reject) => {
|
||||||
try {
|
try {
|
||||||
log.info("initializing GUI session folder");
|
log.info("initializing GUI session folder");
|
||||||
createInitialSessionFile().then((newSession) => {
|
createInitialSessionFile().then((newSession) => {
|
||||||
resolve(newSession);
|
resolve(newSession);
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error(error);
|
log.error(error);
|
||||||
reject(error);
|
reject(error);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
async function addEmptySessionFile(): Promise<Session> {
|
async function addEmptySessionFile(): Promise<Session> {
|
||||||
const locale = app.getLocale();
|
const locale = app.getLocale();
|
||||||
await mkdirp(sessionFolder);
|
await mkdirp(sessionFolder);
|
||||||
const data = {
|
const data = {
|
||||||
device_id: await getDeviceId(),
|
device_id: await getDeviceId(),
|
||||||
locale
|
locale
|
||||||
};
|
};
|
||||||
await writeSessionData(data, true);
|
await writeSessionData(data, true);
|
||||||
log.info("new session file created");
|
log.info("new session file created");
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createInitialSessionFile(): Promise<Session> {
|
export async function createInitialSessionFile(): Promise<Session> {
|
||||||
// TODO: this looks nasty, refactor this
|
// TODO: this looks nasty, refactor this
|
||||||
// the app is too dependent that this function succeeds
|
// the app is too dependent that this function succeeds
|
||||||
let session = {
|
let session = {
|
||||||
...sessionMemory
|
...sessionMemory
|
||||||
};
|
};
|
||||||
const locale = app.getLocale();
|
const locale = app.getLocale();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (fs.existsSync(sessionFilePath)) {
|
if (fs.existsSync(sessionFilePath)) {
|
||||||
log.info("session file exists!");
|
log.info("session file exists!");
|
||||||
const sessionBuffer = await fs.readFileSync(sessionFilePath);
|
const sessionBuffer = await fs.readFileSync(sessionFilePath);
|
||||||
const sessionData = JSON.parse(sessionBuffer.toString()) as Session;
|
const sessionData = JSON.parse(sessionBuffer.toString()) as Session;
|
||||||
|
|
||||||
if (!sessionData?.device_id) {
|
if (!sessionData?.device_id) {
|
||||||
throw new Error("device_id is empty!");
|
throw new Error("device_id is empty!");
|
||||||
} else {
|
} else {
|
||||||
session = sessionData;
|
session = sessionData;
|
||||||
session.locale = locale;
|
session.locale = locale;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error(error);
|
log.error(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!session?.device_id) {
|
if (!session?.device_id) {
|
||||||
try {
|
try {
|
||||||
const newSession = await addEmptySessionFile();
|
const newSession = await addEmptySessionFile();
|
||||||
if (newSession) {
|
if (newSession) {
|
||||||
session = newSession;
|
session = newSession;
|
||||||
session.locale = locale;
|
session.locale = locale;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error(error);
|
log.error(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sessionMemory = session;
|
sessionMemory = session;
|
||||||
|
|
||||||
return session;
|
return session;
|
||||||
}
|
}
|
||||||
|
|
||||||
let deviceIdRetryCount = 0;
|
let deviceIdRetryCount = 0;
|
||||||
async function getDeviceId() {
|
async function getDeviceId() {
|
||||||
let deviceId = "";
|
let deviceId = "";
|
||||||
try {
|
try {
|
||||||
const req = await axios.get<{ deviceId: string }>("https://api.tea.xyz/v1/auth/registerDevice");
|
const req = await axios.get<{ deviceId: string }>("https://api.tea.xyz/v1/auth/registerDevice");
|
||||||
deviceId = req.data.deviceId;
|
deviceId = req.data.deviceId;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error(error);
|
log.error(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (deviceIdRetryCount < 3 && !deviceId) {
|
if (deviceIdRetryCount < 3 && !deviceId) {
|
||||||
deviceIdRetryCount++;
|
deviceIdRetryCount++;
|
||||||
deviceId = await getDeviceId();
|
deviceId = await getDeviceId();
|
||||||
}
|
}
|
||||||
|
|
||||||
return deviceId;
|
return deviceId;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function readSessionData(): Promise<Session> {
|
export async function readSessionData(): Promise<Session> {
|
||||||
log.info("read session data.");
|
log.info("read session data.");
|
||||||
const data = await initialized;
|
const data = await initialized;
|
||||||
log.info(
|
log.info(
|
||||||
"initialized session device_id:",
|
"initialized session device_id:",
|
||||||
data?.device_id,
|
data?.device_id,
|
||||||
"developer_id:",
|
"developer_id:",
|
||||||
data?.user?.developer_id
|
data?.user?.developer_id
|
||||||
);
|
);
|
||||||
if (sessionMemory?.device_id) {
|
if (sessionMemory?.device_id) {
|
||||||
log.debug("use session cache");
|
log.debug("use session cache");
|
||||||
return sessionMemory;
|
return sessionMemory;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
log.info("re-reading session data");
|
log.info("re-reading session data");
|
||||||
const locale = app.getLocale();
|
const locale = app.getLocale();
|
||||||
const sessionBuffer = await fs.readFileSync(sessionFilePath);
|
const sessionBuffer = await fs.readFileSync(sessionFilePath);
|
||||||
const session = JSON.parse(sessionBuffer.toString()) as Session;
|
const session = JSON.parse(sessionBuffer.toString()) as Session;
|
||||||
if (!session?.device_id) throw new Error("device_id is empty!");
|
if (!session?.device_id) throw new Error("device_id is empty!");
|
||||||
|
|
||||||
session.locale = locale;
|
session.locale = locale;
|
||||||
sessionMemory = session;
|
sessionMemory = session;
|
||||||
log.info("re-read session data done");
|
log.info("re-read session data done");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
sessionMemory = await createInitialSessionFile();
|
sessionMemory = await createInitialSessionFile();
|
||||||
log.error(error);
|
log.error(error);
|
||||||
}
|
}
|
||||||
return sessionMemory;
|
return sessionMemory;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function writeSessionData(data: Session, force?: boolean) {
|
export async function writeSessionData(data: Session, force?: boolean) {
|
||||||
try {
|
try {
|
||||||
const existingData = force ? sessionMemory : await readSessionData();
|
const existingData = force ? sessionMemory : await readSessionData();
|
||||||
sessionMemory = {
|
sessionMemory = {
|
||||||
...existingData,
|
...existingData,
|
||||||
...data
|
...data
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!sessionMemory.device_id) throw new Error("writing without device_id is not allowed!");
|
if (!sessionMemory.device_id) throw new Error("writing without device_id is not allowed!");
|
||||||
|
|
||||||
log.info("creating:", sessionFolder);
|
log.info("creating:", sessionFolder);
|
||||||
await mkdirp(sessionFolder);
|
await mkdirp(sessionFolder);
|
||||||
log.info("writing session data:", sessionMemory); // rm this
|
log.info("writing session data:", sessionMemory); // rm this
|
||||||
await fs.writeFileSync(sessionFilePath, JSON.stringify(sessionMemory), {
|
await fs.writeFileSync(sessionFilePath, JSON.stringify(sessionMemory), {
|
||||||
encoding: "utf-8"
|
encoding: "utf-8"
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error(error);
|
log.error(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,8 +3,8 @@ import log from "./logger";
|
||||||
import { BrowserWindow } from "electron";
|
import { BrowserWindow } from "electron";
|
||||||
|
|
||||||
type AutoUpdateStatus = {
|
type AutoUpdateStatus = {
|
||||||
status: "up-to-date" | "available" | "ready";
|
status: "up-to-date" | "available" | "ready";
|
||||||
version?: string;
|
version?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
autoUpdater.logger = log;
|
autoUpdater.logger = log;
|
||||||
|
@ -18,59 +18,59 @@ let lastStatus: AutoUpdateStatus = { status: "up-to-date" };
|
||||||
export const getUpdater = () => autoUpdater;
|
export const getUpdater = () => autoUpdater;
|
||||||
|
|
||||||
export function checkUpdater(mainWindow: BrowserWindow): AppUpdater {
|
export function checkUpdater(mainWindow: BrowserWindow): AppUpdater {
|
||||||
try {
|
try {
|
||||||
window = mainWindow;
|
window = mainWindow;
|
||||||
autoUpdater.checkForUpdatesAndNotify();
|
autoUpdater.checkForUpdatesAndNotify();
|
||||||
|
|
||||||
if (!initalized) {
|
if (!initalized) {
|
||||||
initalized = true;
|
initalized = true;
|
||||||
|
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
autoUpdater.checkForUpdatesAndNotify();
|
autoUpdater.checkForUpdatesAndNotify();
|
||||||
}, 1000 * 60 * 30); // check for updates every 30 minutes
|
}, 1000 * 60 * 30); // check for updates every 30 minutes
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error(error);
|
log.error(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
return autoUpdater;
|
return autoUpdater;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The auto update runs in the background so the window might not be open when the status changes
|
// The auto update runs in the background so the window might not be open when the status changes
|
||||||
// When the update store gets created as part of the window it will request the latest status.
|
// When the update store gets created as part of the window it will request the latest status.
|
||||||
export function getAutoUpdateStatus() {
|
export function getAutoUpdateStatus() {
|
||||||
return lastStatus;
|
return lastStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
function sendStatusToWindow(status: AutoUpdateStatus) {
|
function sendStatusToWindow(status: AutoUpdateStatus) {
|
||||||
lastStatus = status;
|
lastStatus = status;
|
||||||
window?.webContents.send("app-update-status", status);
|
window?.webContents.send("app-update-status", status);
|
||||||
}
|
}
|
||||||
|
|
||||||
autoUpdater.on("checking-for-update", () => {
|
autoUpdater.on("checking-for-update", () => {
|
||||||
log.info("checking for tea gui update");
|
log.info("checking for tea gui update");
|
||||||
});
|
});
|
||||||
|
|
||||||
autoUpdater.on("update-available", (info) => {
|
autoUpdater.on("update-available", (info) => {
|
||||||
sendStatusToWindow({ status: "available" });
|
sendStatusToWindow({ status: "available" });
|
||||||
});
|
});
|
||||||
|
|
||||||
autoUpdater.on("update-not-available", () => {
|
autoUpdater.on("update-not-available", () => {
|
||||||
log.info("no update for tea gui");
|
log.info("no update for tea gui");
|
||||||
sendStatusToWindow({ status: "up-to-date" });
|
sendStatusToWindow({ status: "up-to-date" });
|
||||||
});
|
});
|
||||||
|
|
||||||
autoUpdater.on("error", (err) => {
|
autoUpdater.on("error", (err) => {
|
||||||
log.error("auto update:", err);
|
log.error("auto update:", err);
|
||||||
});
|
});
|
||||||
|
|
||||||
autoUpdater.on("download-progress", (progressObj) => {
|
autoUpdater.on("download-progress", (progressObj) => {
|
||||||
let log_message = "Download speed: " + progressObj.bytesPerSecond;
|
let log_message = "Download speed: " + progressObj.bytesPerSecond;
|
||||||
log_message = log_message + " - Downloaded " + progressObj.percent + "%";
|
log_message = log_message + " - Downloaded " + progressObj.percent + "%";
|
||||||
log_message = log_message + " (" + progressObj.transferred + "/" + progressObj.total + ")";
|
log_message = log_message + " (" + progressObj.transferred + "/" + progressObj.total + ")";
|
||||||
log.info("tea gui:", log_message);
|
log.info("tea gui:", log_message);
|
||||||
});
|
});
|
||||||
|
|
||||||
autoUpdater.on("update-downloaded", (info) => {
|
autoUpdater.on("update-downloaded", (info) => {
|
||||||
sendStatusToWindow({ status: "ready", version: info.version });
|
sendStatusToWindow({ status: "ready", version: info.version });
|
||||||
});
|
});
|
||||||
|
|
|
@ -13,188 +13,188 @@ const destinationDirectory = getGuiPath();
|
||||||
export const cliBinPath = path.join(destinationDirectory, "tea");
|
export const cliBinPath = path.join(destinationDirectory, "tea");
|
||||||
|
|
||||||
export async function installPackage(
|
export async function installPackage(
|
||||||
full_name: string,
|
full_name: string,
|
||||||
version: string,
|
version: string,
|
||||||
notifyMainWindow: MainWindowNotifier
|
notifyMainWindow: MainWindowNotifier
|
||||||
) {
|
) {
|
||||||
const teaVersion = await initializeTeaCli();
|
const teaVersion = await initializeTeaCli();
|
||||||
const progressNotifier = newInstallProgressNotifier(full_name, notifyMainWindow);
|
const progressNotifier = newInstallProgressNotifier(full_name, notifyMainWindow);
|
||||||
|
|
||||||
if (!teaVersion) throw new Error("no tea");
|
if (!teaVersion) throw new Error("no tea");
|
||||||
|
|
||||||
const qualifedPackage = `${full_name}@${version}`;
|
const qualifedPackage = `${full_name}@${version}`;
|
||||||
|
|
||||||
log.info(`installing package ${qualifedPackage}`);
|
log.info(`installing package ${qualifedPackage}`);
|
||||||
|
|
||||||
let stdout = "";
|
let stdout = "";
|
||||||
let stderr = "";
|
let stderr = "";
|
||||||
|
|
||||||
await new Promise((resolve, reject) => {
|
await new Promise((resolve, reject) => {
|
||||||
// tea requires HOME to be set.
|
// tea requires HOME to be set.
|
||||||
const opts = { env: { HOME: app.getPath("home"), NO_COLOR: "1" } };
|
const opts = { env: { HOME: app.getPath("home"), NO_COLOR: "1" } };
|
||||||
|
|
||||||
const child = spawn(
|
const child = spawn(
|
||||||
`${destinationDirectory}/tea`,
|
`${destinationDirectory}/tea`,
|
||||||
["--env=false", "--sync", "--json", `+${qualifedPackage}`],
|
["--env=false", "--sync", "--json", `+${qualifedPackage}`],
|
||||||
opts
|
opts
|
||||||
);
|
);
|
||||||
|
|
||||||
child.stdout.on("data", (data) => {
|
child.stdout.on("data", (data) => {
|
||||||
stdout += data.toString().trim();
|
stdout += data.toString().trim();
|
||||||
});
|
});
|
||||||
|
|
||||||
child.stderr.on("data", (data) => {
|
child.stderr.on("data", (data) => {
|
||||||
try {
|
try {
|
||||||
const msg = JSON.parse(data.toString().trim());
|
const msg = JSON.parse(data.toString().trim());
|
||||||
progressNotifier(msg);
|
progressNotifier(msg);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
//swallow it
|
//swallow it
|
||||||
}
|
}
|
||||||
|
|
||||||
stderr += data.toString().trim();
|
stderr += data.toString().trim();
|
||||||
});
|
});
|
||||||
|
|
||||||
child.on("exit", (code) => {
|
child.on("exit", (code) => {
|
||||||
console.log("stdout:", stdout);
|
console.log("stdout:", stdout);
|
||||||
if (code !== 0) {
|
if (code !== 0) {
|
||||||
reject(new Error("tea exited with non-zero code: " + code));
|
reject(new Error("tea exited with non-zero code: " + code));
|
||||||
} else {
|
} else {
|
||||||
resolve(null);
|
resolve(null);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
child.on("error", () => {
|
child.on("error", () => {
|
||||||
reject(new Error(stderr));
|
reject(new Error(stderr));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// This is hacky and kind of complex because of the output we get from the CLI. When the CLI
|
// This is hacky and kind of complex because of the output we get from the CLI. When the CLI
|
||||||
// gives better output this definitely should get looked at.
|
// gives better output this definitely should get looked at.
|
||||||
function newInstallProgressNotifier(full_name: string, notifyMainWindow: MainWindowNotifier) {
|
function newInstallProgressNotifier(full_name: string, notifyMainWindow: MainWindowNotifier) {
|
||||||
// the install progress is super spammy, only send every 10th update
|
// the install progress is super spammy, only send every 10th update
|
||||||
let counter = 0;
|
let counter = 0;
|
||||||
|
|
||||||
// the totall number of packages to install - this is set by the "resolved" message
|
// the totall number of packages to install - this is set by the "resolved" message
|
||||||
let numberOfPackages = 1;
|
let numberOfPackages = 1;
|
||||||
|
|
||||||
// the current package number - this is incremented by the "installed" or "downloaded" message
|
// the current package number - this is incremented by the "installed" or "downloaded" message
|
||||||
let currentPackageNumber = 0;
|
let currentPackageNumber = 0;
|
||||||
|
|
||||||
return function (msg: any) {
|
return function (msg: any) {
|
||||||
if (msg.status !== "downloading") {
|
if (msg.status !== "downloading") {
|
||||||
log.info("cli:", msg);
|
log.info("cli:", msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (msg.status === "resolved") {
|
if (msg.status === "resolved") {
|
||||||
numberOfPackages = msg.pkgs?.length ?? 1;
|
numberOfPackages = msg.pkgs?.length ?? 1;
|
||||||
log.info(`installing ${numberOfPackages} packages`);
|
log.info(`installing ${numberOfPackages} packages`);
|
||||||
} else if (msg.status === "downloading") {
|
} else if (msg.status === "downloading") {
|
||||||
counter++;
|
counter++;
|
||||||
if (counter % 10 !== 0) return;
|
if (counter % 10 !== 0) return;
|
||||||
|
|
||||||
const { received = 0, "content-size": contentSize = 0 } = msg;
|
const { received = 0, "content-size": contentSize = 0 } = msg;
|
||||||
if (contentSize > 0) {
|
if (contentSize > 0) {
|
||||||
// how many total pacakges are completed
|
// how many total pacakges are completed
|
||||||
const overallProgress = (currentPackageNumber / numberOfPackages) * 100;
|
const overallProgress = (currentPackageNumber / numberOfPackages) * 100;
|
||||||
// how much of the current package is completed
|
// how much of the current package is completed
|
||||||
const packageProgress = (received / contentSize) * 100;
|
const packageProgress = (received / contentSize) * 100;
|
||||||
// progress is the total packages completed plus the percentage of the current package
|
// progress is the total packages completed plus the percentage of the current package
|
||||||
const progress = overallProgress + packageProgress / numberOfPackages;
|
const progress = overallProgress + packageProgress / numberOfPackages;
|
||||||
notifyMainWindow("install-progress", { full_name, progress });
|
notifyMainWindow("install-progress", { full_name, progress });
|
||||||
}
|
}
|
||||||
} else if (msg.status === "installed") {
|
} else if (msg.status === "installed") {
|
||||||
currentPackageNumber++;
|
currentPackageNumber++;
|
||||||
const progress = (currentPackageNumber / numberOfPackages) * 100;
|
const progress = (currentPackageNumber / numberOfPackages) * 100;
|
||||||
notifyMainWindow("install-progress", { full_name, progress });
|
notifyMainWindow("install-progress", { full_name, progress });
|
||||||
notifyPackageInstalled(msg.pkg, notifyMainWindow);
|
notifyPackageInstalled(msg.pkg, notifyMainWindow);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const notifyPackageInstalled = (rawPkg: string, notifyMainWindow: MainWindowNotifier) => {
|
const notifyPackageInstalled = (rawPkg: string, notifyMainWindow: MainWindowNotifier) => {
|
||||||
try {
|
try {
|
||||||
const [full_name, version] = rawPkg.split("=");
|
const [full_name, version] = rawPkg.split("=");
|
||||||
notifyMainWindow("pkg-installed", { full_name, version });
|
notifyMainWindow("pkg-installed", { full_name, version });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log.error("failed to notify package installed", err);
|
log.error("failed to notify package installed", err);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function openPackageEntrypointInTerminal(pkg: string) {
|
export async function openPackageEntrypointInTerminal(pkg: string) {
|
||||||
let sh = `${cliBinPath} --sync --env=false +${pkg} `;
|
let sh = `${cliBinPath} --sync --env=false +${pkg} `;
|
||||||
if (pkg == "github.com/AUTOMATIC1111/stable-diffusion-webui") {
|
if (pkg == "github.com/AUTOMATIC1111/stable-diffusion-webui") {
|
||||||
sh += `~/.tea/${pkg}/v*/entrypoint.sh`;
|
sh += `~/.tea/${pkg}/v*/entrypoint.sh`;
|
||||||
} else {
|
} else {
|
||||||
sh += "sh";
|
sh += "sh";
|
||||||
}
|
}
|
||||||
|
|
||||||
const scriptPath = await createCommandScriptFile(sh);
|
const scriptPath = await createCommandScriptFile(sh);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let stdout = "";
|
let stdout = "";
|
||||||
let stderr = "";
|
let stderr = "";
|
||||||
|
|
||||||
await new Promise((resolve, reject) => {
|
await new Promise((resolve, reject) => {
|
||||||
const child = spawn("/usr/bin/osascript", [scriptPath]);
|
const child = spawn("/usr/bin/osascript", [scriptPath]);
|
||||||
child.stdout.on("data", (data) => {
|
child.stdout.on("data", (data) => {
|
||||||
stdout += data.toString().trim();
|
stdout += data.toString().trim();
|
||||||
});
|
});
|
||||||
child.stderr.on("data", (data) => {
|
child.stderr.on("data", (data) => {
|
||||||
stderr += data.toString().trim();
|
stderr += data.toString().trim();
|
||||||
});
|
});
|
||||||
|
|
||||||
child.on("exit", (code) => {
|
child.on("exit", (code) => {
|
||||||
log.info("exit:", code, `\`${stdout}\``);
|
log.info("exit:", code, `\`${stdout}\``);
|
||||||
if (code == 0) {
|
if (code == 0) {
|
||||||
resolve(stdout);
|
resolve(stdout);
|
||||||
} else {
|
} else {
|
||||||
reject(new Error("failed to open terminal and run tea sh"));
|
reject(new Error("failed to open terminal and run tea sh"));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
child.on("error", () => {
|
child.on("error", () => {
|
||||||
reject(new Error(stderr));
|
reject(new Error(stderr));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
if (scriptPath) await fs.unlinkSync(scriptPath);
|
if (scriptPath) await fs.unlinkSync(scriptPath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const createCommandScriptFile = async (cmd: string): Promise<string> => {
|
const createCommandScriptFile = async (cmd: string): Promise<string> => {
|
||||||
const guiFolder = getGuiPath();
|
const guiFolder = getGuiPath();
|
||||||
const tmpFilePath = path.join(guiFolder, `${+new Date()}.scpt`);
|
const tmpFilePath = path.join(guiFolder, `${+new Date()}.scpt`);
|
||||||
const command = `${cmd.replace(/"/g, '\\"')}`;
|
const command = `${cmd.replace(/"/g, '\\"')}`;
|
||||||
const script = `
|
const script = `
|
||||||
tell application "Terminal"
|
tell application "Terminal"
|
||||||
activate
|
activate
|
||||||
do script "${command}"
|
do script "${command}"
|
||||||
end tell
|
end tell
|
||||||
`.trim();
|
`.trim();
|
||||||
|
|
||||||
await fs.writeFileSync(tmpFilePath, script, "utf-8");
|
await fs.writeFileSync(tmpFilePath, script, "utf-8");
|
||||||
return tmpFilePath;
|
return tmpFilePath;
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function asyncExec(cmd: string): Promise<string> {
|
export async function asyncExec(cmd: string): Promise<string> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
exec(cmd, (err, stdout) => {
|
exec(cmd, (err, stdout) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
console.log("err:", err);
|
console.log("err:", err);
|
||||||
reject(err);
|
reject(err);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
console.log("stdout:", stdout);
|
console.log("stdout:", stdout);
|
||||||
resolve(stdout);
|
resolve(stdout);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function syncPantry() {
|
export async function syncPantry() {
|
||||||
const teaVersion = await initializeTeaCli();
|
const teaVersion = await initializeTeaCli();
|
||||||
|
|
||||||
if (!teaVersion) throw new Error("no tea");
|
if (!teaVersion) throw new Error("no tea");
|
||||||
log.info("Syncing pantry");
|
log.info("Syncing pantry");
|
||||||
await asyncExec(`cd '${destinationDirectory}' && ./tea -S`);
|
await asyncExec(`cd '${destinationDirectory}' && ./tea -S`);
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,73 +19,73 @@ const binaryUrl = "https://tea.xyz/$(uname)/$(uname -m)";
|
||||||
let initializePromise: Promise<string> | null = null;
|
let initializePromise: Promise<string> | null = null;
|
||||||
|
|
||||||
export async function initializeTeaCli(): Promise<string> {
|
export async function initializeTeaCli(): Promise<string> {
|
||||||
if (initializePromise) {
|
if (initializePromise) {
|
||||||
return initializePromise;
|
return initializePromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info("Initializing tea cli");
|
log.info("Initializing tea cli");
|
||||||
initializePromise = initializeTeaCliInternal();
|
initializePromise = initializeTeaCliInternal();
|
||||||
|
|
||||||
initializePromise.catch((error) => {
|
initializePromise.catch((error) => {
|
||||||
log.info("Error initializing tea cli, resetting promise:", error);
|
log.info("Error initializing tea cli, resetting promise:", error);
|
||||||
initializePromise = null;
|
initializePromise = null;
|
||||||
});
|
});
|
||||||
|
|
||||||
return initializePromise;
|
return initializePromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function initializeTeaCliInternal(): Promise<string> {
|
async function initializeTeaCliInternal(): Promise<string> {
|
||||||
try {
|
try {
|
||||||
let binCheck = "";
|
let binCheck = "";
|
||||||
let needsUpdate = false;
|
let needsUpdate = false;
|
||||||
|
|
||||||
// Create the destination directory if it doesn't exist
|
// Create the destination directory if it doesn't exist
|
||||||
if (!fs.existsSync(destinationDirectory)) {
|
if (!fs.existsSync(destinationDirectory)) {
|
||||||
fs.mkdirSync(destinationDirectory, { recursive: true });
|
fs.mkdirSync(destinationDirectory, { recursive: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
const curlCommand = `curl -L -o "${cliBinPath}" "${binaryUrl}"`;
|
const curlCommand = `curl -L -o "${cliBinPath}" "${binaryUrl}"`;
|
||||||
|
|
||||||
const exists = fs.existsSync(cliBinPath);
|
const exists = fs.existsSync(cliBinPath);
|
||||||
if (exists) {
|
if (exists) {
|
||||||
log.info("binary tea already exists at", cliBinPath);
|
log.info("binary tea already exists at", cliBinPath);
|
||||||
try {
|
try {
|
||||||
binCheck = await asyncExec(`cd ${destinationDirectory} && ./tea --version`);
|
binCheck = await asyncExec(`cd ${destinationDirectory} && ./tea --version`);
|
||||||
const teaVersion = binCheck.toString().split(" ")[1];
|
const teaVersion = binCheck.toString().split(" ")[1];
|
||||||
if (semverCompare(teaVersion, MINIMUM_TEA_VERSION) < 0) {
|
if (semverCompare(teaVersion, MINIMUM_TEA_VERSION) < 0) {
|
||||||
log.info("binary tea version is too old, updating");
|
log.info("binary tea version is too old, updating");
|
||||||
needsUpdate = true;
|
needsUpdate = true;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// probably binary is not executable or no permission
|
// probably binary is not executable or no permission
|
||||||
log.error("Error checking tea binary version:", error);
|
log.error("Error checking tea binary version:", error);
|
||||||
needsUpdate = true;
|
needsUpdate = true;
|
||||||
await asyncExec(`cd ${destinationDirectory} && rm tea`);
|
await asyncExec(`cd ${destinationDirectory} && rm tea`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!exists || needsUpdate) {
|
if (!exists || needsUpdate) {
|
||||||
try {
|
try {
|
||||||
await asyncExec(curlCommand);
|
await asyncExec(curlCommand);
|
||||||
log.info("Binary downloaded and saved to", cliBinPath);
|
log.info("Binary downloaded and saved to", cliBinPath);
|
||||||
await asyncExec("chmod u+x " + cliBinPath);
|
await asyncExec("chmod u+x " + cliBinPath);
|
||||||
log.info("Binary is now ready for use at", cliBinPath);
|
log.info("Binary is now ready for use at", cliBinPath);
|
||||||
binCheck = await asyncExec(`cd ${destinationDirectory} && ./tea --version`);
|
binCheck = await asyncExec(`cd ${destinationDirectory} && ./tea --version`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error("Error setting-up tea binary:", error);
|
log.error("Error setting-up tea binary:", error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const version = binCheck.toString().split(" ")[1];
|
const version = binCheck.toString().split(" ")[1];
|
||||||
log.info("binary tea version:", version);
|
log.info("binary tea version:", version);
|
||||||
return semver.valid(version.trim()) ? version : "";
|
return semver.valid(version.trim()) ? version : "";
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error(error);
|
log.error(error);
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function initialize(): Promise<string> {
|
export default async function initialize(): Promise<string> {
|
||||||
const [version] = await Promise.all([initializeTeaCli(), createInitialSessionFile()]);
|
const [version] = await Promise.all([initializeTeaCli(), createInitialSessionFile()]);
|
||||||
return version;
|
return version;
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,194 +15,194 @@ import { nanoid } from "nanoid";
|
||||||
import { MainWindowNotifier } from "./types";
|
import { MainWindowNotifier } from "./types";
|
||||||
|
|
||||||
export type HandlerOptions = {
|
export type HandlerOptions = {
|
||||||
// A function to call back to the current main
|
// A function to call back to the current main
|
||||||
notifyMainWindow: MainWindowNotifier;
|
notifyMainWindow: MainWindowNotifier;
|
||||||
};
|
};
|
||||||
|
|
||||||
let teaProtocolPath = ""; // this should be empty string
|
let teaProtocolPath = ""; // this should be empty string
|
||||||
export const setProtocolPath = (path: string) => {
|
export const setProtocolPath = (path: string) => {
|
||||||
teaProtocolPath = path;
|
teaProtocolPath = path;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function initializeHandlers({ notifyMainWindow }: HandlerOptions) {
|
export default function initializeHandlers({ notifyMainWindow }: HandlerOptions) {
|
||||||
ipcMain.handle("get-installed-packages", async () => {
|
ipcMain.handle("get-installed-packages", async () => {
|
||||||
try {
|
try {
|
||||||
log.info("getting installed packages");
|
log.info("getting installed packages");
|
||||||
const pkgs = await getInstalledPackages();
|
const pkgs = await getInstalledPackages();
|
||||||
log.info(`got installed packages: ${pkgs.length}`);
|
log.info(`got installed packages: ${pkgs.length}`);
|
||||||
return pkgs;
|
return pkgs;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error(error);
|
log.error(error);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle("get-session", async () => {
|
ipcMain.handle("get-session", async () => {
|
||||||
try {
|
try {
|
||||||
log.info("getting session");
|
log.info("getting session");
|
||||||
const session = await readSessionData();
|
const session = await readSessionData();
|
||||||
log.debug(session ? "found session data" : "no session data found");
|
log.debug(session ? "found session data" : "no session data found");
|
||||||
return session;
|
return session;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error(error);
|
log.error(error);
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle("update-session", async (_, data) => {
|
ipcMain.handle("update-session", async (_, data) => {
|
||||||
try {
|
try {
|
||||||
log.info("updating session data with", data); // rm this
|
log.info("updating session data with", data); // rm this
|
||||||
await writeSessionData(data as Session);
|
await writeSessionData(data as Session);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error(error);
|
log.error(error);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle("install-package", async (_, { full_name, version }) => {
|
ipcMain.handle("install-package", async (_, { full_name, version }) => {
|
||||||
try {
|
try {
|
||||||
return await installPackage(full_name, version, notifyMainWindow);
|
return await installPackage(full_name, version, notifyMainWindow);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error(error);
|
log.error(error);
|
||||||
return error;
|
return error;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle("sync-pantry", async () => {
|
ipcMain.handle("sync-pantry", async () => {
|
||||||
try {
|
try {
|
||||||
return await syncPantry();
|
return await syncPantry();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error(error);
|
log.error(error);
|
||||||
return error;
|
return error;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle("open-terminal", async (_, data) => {
|
ipcMain.handle("open-terminal", async (_, data) => {
|
||||||
const { pkg } = data as { pkg: string };
|
const { pkg } = data as { pkg: string };
|
||||||
try {
|
try {
|
||||||
// TODO: detect if mac or linux
|
// TODO: detect if mac or linux
|
||||||
// current openTerminal is only design for Mac
|
// current openTerminal is only design for Mac
|
||||||
log.info("open tea entrypoint in terminal for pkg:", pkg);
|
log.info("open tea entrypoint in terminal for pkg:", pkg);
|
||||||
await openPackageEntrypointInTerminal(pkg);
|
await openPackageEntrypointInTerminal(pkg);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error(error);
|
log.error(error);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle("relaunch", async () => {
|
ipcMain.handle("relaunch", async () => {
|
||||||
try {
|
try {
|
||||||
log.info("relaunching app");
|
log.info("relaunching app");
|
||||||
const autoUpdater = getUpdater();
|
const autoUpdater = getUpdater();
|
||||||
await autoUpdater.quitAndInstall();
|
await autoUpdater.quitAndInstall();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error(error);
|
log.error(error);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle("get-protocol-path", async () => {
|
ipcMain.handle("get-protocol-path", async () => {
|
||||||
const path = teaProtocolPath;
|
const path = teaProtocolPath;
|
||||||
teaProtocolPath = "";
|
teaProtocolPath = "";
|
||||||
return path;
|
return path;
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle("submit-logs", async () => {
|
ipcMain.handle("submit-logs", async () => {
|
||||||
try {
|
try {
|
||||||
log.info("syncing logs");
|
log.info("syncing logs");
|
||||||
const { device_id } = await readSessionData();
|
const { device_id } = await readSessionData();
|
||||||
const logId = [device_id, nanoid()].join("---");
|
const logId = [device_id, nanoid()].join("---");
|
||||||
|
|
||||||
// sync in background
|
// sync in background
|
||||||
syncLogsAt(logId)
|
syncLogsAt(logId)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
log.info("logs synced:", logId);
|
log.info("logs synced:", logId);
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
log.error(error);
|
log.error(error);
|
||||||
});
|
});
|
||||||
|
|
||||||
return logId;
|
return logId;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error(error);
|
log.error(error);
|
||||||
return error.message;
|
return error.message;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle("set-badge-count", async (_, { count }) => {
|
ipcMain.handle("set-badge-count", async (_, { count }) => {
|
||||||
if (count) {
|
if (count) {
|
||||||
app.dock.setBadge(count.toString());
|
app.dock.setBadge(count.toString());
|
||||||
} else {
|
} else {
|
||||||
app.dock.setBadge("");
|
app.dock.setBadge("");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle(
|
ipcMain.handle(
|
||||||
"delete-package",
|
"delete-package",
|
||||||
async (_, { fullName, version }: { fullName: string; version: string }) => {
|
async (_, { fullName, version }: { fullName: string; version: string }) => {
|
||||||
try {
|
try {
|
||||||
log.info("deleting package:", fullName);
|
log.info("deleting package:", fullName);
|
||||||
await deletePackageFolder(fullName, version);
|
await deletePackageFolder(fullName, version);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
log.error(e);
|
log.error(e);
|
||||||
return e;
|
return e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
ipcMain.handle("write-package-cache", async (_, data) => {
|
ipcMain.handle("write-package-cache", async (_, data) => {
|
||||||
try {
|
try {
|
||||||
await writePackageCache(data as Packages);
|
await writePackageCache(data as Packages);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error(error);
|
log.error(error);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle("load-package-cache", async () => {
|
ipcMain.handle("load-package-cache", async () => {
|
||||||
try {
|
try {
|
||||||
return await loadPackageCache();
|
return await loadPackageCache();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error(error);
|
log.error(error);
|
||||||
return { version: "1", packages: {} };
|
return { version: "1", packages: {} };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle("get-tea-version", async () => {
|
ipcMain.handle("get-tea-version", async () => {
|
||||||
try {
|
try {
|
||||||
log.info("installing tea cli");
|
log.info("installing tea cli");
|
||||||
const version = await initializeTeaCli();
|
const version = await initializeTeaCli();
|
||||||
if (!version) {
|
if (!version) {
|
||||||
throw new Error("failed to install tea cli");
|
throw new Error("failed to install tea cli");
|
||||||
}
|
}
|
||||||
|
|
||||||
return { version, message: "" };
|
return { version, message: "" };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error(error);
|
log.error(error);
|
||||||
return { version: "", message: error.message };
|
return { version: "", message: error.message };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle("topbar-double-click", async (event: Electron.IpcMainInvokeEvent) => {
|
ipcMain.handle("topbar-double-click", async (event: Electron.IpcMainInvokeEvent) => {
|
||||||
const mainWindow = BrowserWindow.fromWebContents(event.sender);
|
const mainWindow = BrowserWindow.fromWebContents(event.sender);
|
||||||
if (mainWindow) {
|
if (mainWindow) {
|
||||||
mainWindow.isMaximized() ? mainWindow.unmaximize() : mainWindow.maximize();
|
mainWindow.isMaximized() ? mainWindow.unmaximize() : mainWindow.maximize();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle("cache-image", async (_event, url) => {
|
ipcMain.handle("cache-image", async (_event, url) => {
|
||||||
try {
|
try {
|
||||||
log.info("caching:", url);
|
log.info("caching:", url);
|
||||||
const cachedImagePath = await cacheImage(url);
|
const cachedImagePath = await cacheImage(url);
|
||||||
return cachedImagePath;
|
return cachedImagePath;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error("Failed to cache image:", error);
|
log.error("Failed to cache image:", error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle("get-auto-update-status", async () => {
|
ipcMain.handle("get-auto-update-status", async () => {
|
||||||
try {
|
try {
|
||||||
log.info("getting auto update status");
|
log.info("getting auto update status");
|
||||||
return getAutoUpdateStatus();
|
return getAutoUpdateStatus();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error(error);
|
log.error(error);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
import log from "electron-log";
|
import log from "electron-log";
|
||||||
|
|
||||||
export const setSentryLogging = (sentry: any) => {
|
export const setSentryLogging = (sentry: any) => {
|
||||||
const oldError = log.error;
|
const oldError = log.error;
|
||||||
|
|
||||||
log.error = (...params: any[]) => {
|
log.error = (...params: any[]) => {
|
||||||
oldError(params);
|
oldError(params);
|
||||||
sentry.captureException(params[0].message);
|
sentry.captureException(params[0].message);
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// Export the log object to use it throughout the app
|
// Export the log object to use it throughout the app
|
||||||
|
|
|
@ -9,37 +9,37 @@ const pkgsFilePath = path.join(getTeaPath(), "tea.xyz/gui/pkgs.json");
|
||||||
const pkgsFolder = path.join(getTeaPath(), "tea.xyz/gui");
|
const pkgsFolder = path.join(getTeaPath(), "tea.xyz/gui");
|
||||||
|
|
||||||
export async function writePackageCache(pkgs: Packages) {
|
export async function writePackageCache(pkgs: Packages) {
|
||||||
try {
|
try {
|
||||||
if (!pkgs || !Object.keys(pkgs.packages).length) {
|
if (!pkgs || !Object.keys(pkgs.packages).length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info(`writing data for ${Object.keys(pkgs.packages).length} packages to ${pkgsFilePath}`);
|
log.info(`writing data for ${Object.keys(pkgs.packages).length} packages to ${pkgsFilePath}`);
|
||||||
await mkdirp(pkgsFolder);
|
await mkdirp(pkgsFolder);
|
||||||
fs.writeFileSync(pkgsFilePath, JSON.stringify(pkgs), {
|
fs.writeFileSync(pkgsFilePath, JSON.stringify(pkgs), {
|
||||||
encoding: "utf-8"
|
encoding: "utf-8"
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error(error);
|
log.error(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function loadPackageCache(): Promise<Packages> {
|
export async function loadPackageCache(): Promise<Packages> {
|
||||||
try {
|
try {
|
||||||
log.info(`loading package cache from ${pkgsFilePath}`);
|
log.info(`loading package cache from ${pkgsFilePath}`);
|
||||||
const pkgData = fs.readFileSync(pkgsFilePath);
|
const pkgData = fs.readFileSync(pkgsFilePath);
|
||||||
return JSON.parse(pkgData.toString()) as Packages;
|
return JSON.parse(pkgData.toString()) as Packages;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err.code !== "ENOENT") {
|
if (err.code !== "ENOENT") {
|
||||||
log.error(err);
|
log.error(err);
|
||||||
}
|
}
|
||||||
return { version: "1", packages: {} };
|
return { version: "1", packages: {} };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const nameToSlug = (name: string) => {
|
export const nameToSlug = (name: string) => {
|
||||||
// github.com/Pypa/twine -> github_com_pypa_twine
|
// github.com/Pypa/twine -> github_com_pypa_twine
|
||||||
const [nameOnly] = name.split("@");
|
const [nameOnly] = name.split("@");
|
||||||
const slug = nameOnly.replace(/[^\w\s]/gi, "_").toLocaleLowerCase();
|
const slug = nameOnly.replace(/[^\w\s]/gi, "_").toLocaleLowerCase();
|
||||||
return slug;
|
return slug;
|
||||||
};
|
};
|
||||||
|
|
|
@ -5,10 +5,10 @@ import log from "./logger";
|
||||||
import { Notification, BrowserWindow } from "electron";
|
import { Notification, BrowserWindow } from "electron";
|
||||||
import { nameToSlug } from "./package";
|
import { nameToSlug } from "./package";
|
||||||
import {
|
import {
|
||||||
getInstalledPackages,
|
getInstalledPackages,
|
||||||
getPackagesInstalledList,
|
getPackagesInstalledList,
|
||||||
updatePackageInstalledList,
|
updatePackageInstalledList,
|
||||||
getGuiPath
|
getGuiPath
|
||||||
} from "./tea-dir";
|
} from "./tea-dir";
|
||||||
import { app } from "electron";
|
import { app } from "electron";
|
||||||
import { promisify } from "util";
|
import { promisify } from "util";
|
||||||
|
@ -21,162 +21,162 @@ const readFile = promisify(fs.readFile);
|
||||||
const writeFile = promisify(fs.writeFile);
|
const writeFile = promisify(fs.writeFile);
|
||||||
|
|
||||||
export default function initialize(mainWindow: BrowserWindow) {
|
export default function initialize(mainWindow: BrowserWindow) {
|
||||||
if (config.PUSHY_APP_ID) {
|
if (config.PUSHY_APP_ID) {
|
||||||
Pushy.listen();
|
Pushy.listen();
|
||||||
// Register device for push notifications
|
// Register device for push notifications
|
||||||
Pushy.register({ appId: config.PUSHY_APP_ID })
|
Pushy.register({ appId: config.PUSHY_APP_ID })
|
||||||
.then(async (push_token) => {
|
.then(async (push_token) => {
|
||||||
const { device_id } = await readSessionData();
|
const { device_id } = await readSessionData();
|
||||||
log.info(
|
log.info(
|
||||||
`Registering device ${device_id} for push notifications with token: ${push_token}`
|
`Registering device ${device_id} for push notifications with token: ${push_token}`
|
||||||
);
|
);
|
||||||
if (device_id) await post(`/auth/device/${device_id}/register-push-token`, { push_token });
|
if (device_id) await post(`/auth/device/${device_id}/register-push-token`, { push_token });
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
log.error(err);
|
log.error(err);
|
||||||
// Display error dialog
|
// Display error dialog
|
||||||
// Pushy.alert(mainWindow, 'Pushy registration error: ' + err.message);
|
// Pushy.alert(mainWindow, 'Pushy registration error: ' + err.message);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Listen for incoming notifications
|
// Listen for incoming notifications
|
||||||
Pushy.setNotificationListener(async (data: any) => {
|
Pushy.setNotificationListener(async (data: any) => {
|
||||||
try {
|
try {
|
||||||
log.info("new notification received", data);
|
log.info("new notification received", data);
|
||||||
|
|
||||||
const isDup = await wasReceivedBefore(data);
|
const isDup = await wasReceivedBefore(data);
|
||||||
if (!isDup) {
|
if (!isDup) {
|
||||||
new Notification({
|
new Notification({
|
||||||
title: "tea",
|
title: "tea",
|
||||||
body: data?.message as string
|
body: data?.message as string
|
||||||
}).show();
|
}).show();
|
||||||
|
|
||||||
const v = app.dock.getBadge();
|
const v = app.dock.getBadge();
|
||||||
if (!v) {
|
if (!v) {
|
||||||
app.dock.setBadge("1");
|
app.dock.setBadge("1");
|
||||||
} else {
|
} else {
|
||||||
app.dock.setBadge((parseInt(v) + 1).toString());
|
app.dock.setBadge((parseInt(v) + 1).toString());
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
log.info("notification was already received before", data);
|
log.info("notification was already received before", data);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error("notification listener", error);
|
log.error("notification listener", error);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function subscribeToPackageTopic(pkgFullname: string) {
|
export async function subscribeToPackageTopic(pkgFullname: string) {
|
||||||
try {
|
try {
|
||||||
if (Pushy.isRegistered()) {
|
if (Pushy.isRegistered()) {
|
||||||
const slug = nameToSlug(pkgFullname);
|
const slug = nameToSlug(pkgFullname);
|
||||||
|
|
||||||
// override rules for brewkit_mnt
|
// override rules for brewkit_mnt
|
||||||
if (slug.includes("brewkit_mnt")) return;
|
if (slug.includes("brewkit_mnt")) return;
|
||||||
|
|
||||||
const platformArch = getTopicArch();
|
const platformArch = getTopicArch();
|
||||||
const topic = `packages-${slug}_${platformArch}`;
|
const topic = `packages-${slug}_${platformArch}`;
|
||||||
|
|
||||||
await Pushy.subscribe(topic);
|
await Pushy.subscribe(topic);
|
||||||
log.info("push: registered to pkg-topic: ", topic);
|
log.info("push: registered to pkg-topic: ", topic);
|
||||||
} else {
|
} else {
|
||||||
log.info("pushy is not registered");
|
log.info("pushy is not registered");
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error(error);
|
log.error(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function unsubscribeToPackageTopic(pkgFullname: string) {
|
export async function unsubscribeToPackageTopic(pkgFullname: string) {
|
||||||
try {
|
try {
|
||||||
if (Pushy.isRegistered()) {
|
if (Pushy.isRegistered()) {
|
||||||
const slug = nameToSlug(pkgFullname);
|
const slug = nameToSlug(pkgFullname);
|
||||||
const topic = `packages-${slug}`;
|
const topic = `packages-${slug}`;
|
||||||
await Pushy.unsubscribe(topic);
|
await Pushy.unsubscribe(topic);
|
||||||
log.info("push: unregistered from pkg-topic: ", topic);
|
log.info("push: unregistered from pkg-topic: ", topic);
|
||||||
} else {
|
} else {
|
||||||
log.info("pushy is not registered");
|
log.info("pushy is not registered");
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error(error);
|
log.error(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function syncPackageTopicSubscriptions() {
|
export async function syncPackageTopicSubscriptions() {
|
||||||
try {
|
try {
|
||||||
log.info("syncing package topic subscriptions");
|
log.info("syncing package topic subscriptions");
|
||||||
const [installedPackages, lastInstalledList] = await Promise.all([
|
const [installedPackages, lastInstalledList] = await Promise.all([
|
||||||
getInstalledPackages(),
|
getInstalledPackages(),
|
||||||
getPackagesInstalledList()
|
getPackagesInstalledList()
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const previouslyInstalledNames = lastInstalledList.map((pkg) => pkg.full_name);
|
const previouslyInstalledNames = lastInstalledList.map((pkg) => pkg.full_name);
|
||||||
const currentlyInstalledNames = installedPackages.map((pkg) => pkg.full_name);
|
const currentlyInstalledNames = installedPackages.map((pkg) => pkg.full_name);
|
||||||
|
|
||||||
const subscribedTo = currentlyInstalledNames.filter(
|
const subscribedTo = currentlyInstalledNames.filter(
|
||||||
(pkg) => !previouslyInstalledNames.includes(pkg)
|
(pkg) => !previouslyInstalledNames.includes(pkg)
|
||||||
);
|
);
|
||||||
const unsubscribedFrom = previouslyInstalledNames.filter(
|
const unsubscribedFrom = previouslyInstalledNames.filter(
|
||||||
(pkg) => !currentlyInstalledNames.includes(pkg)
|
(pkg) => !currentlyInstalledNames.includes(pkg)
|
||||||
);
|
);
|
||||||
|
|
||||||
for (const subscribe of subscribedTo) {
|
for (const subscribe of subscribedTo) {
|
||||||
await subscribeToPackageTopic(subscribe);
|
await subscribeToPackageTopic(subscribe);
|
||||||
}
|
}
|
||||||
for (const unsubscribe of unsubscribedFrom) {
|
for (const unsubscribe of unsubscribedFrom) {
|
||||||
await unsubscribeToPackageTopic(unsubscribe);
|
await unsubscribeToPackageTopic(unsubscribe);
|
||||||
}
|
}
|
||||||
|
|
||||||
await updatePackageInstalledList(installedPackages);
|
await updatePackageInstalledList(installedPackages);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error(error);
|
log.error(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum PlatformArch {
|
enum PlatformArch {
|
||||||
DarwinAarch64 = "darwin_aarch64",
|
DarwinAarch64 = "darwin_aarch64",
|
||||||
DarwinX86_64 = "darwin_x86-64",
|
DarwinX86_64 = "darwin_x86-64",
|
||||||
LinuxAarch64 = "linux_aarch64",
|
LinuxAarch64 = "linux_aarch64",
|
||||||
LinuxX86_64 = "linux_x86-64"
|
LinuxX86_64 = "linux_x86-64"
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getTopicArch() {
|
export function getTopicArch() {
|
||||||
const arch = (process.arch as string) === "arm64" ? "aarch64" : "x86-64";
|
const arch = (process.arch as string) === "arm64" ? "aarch64" : "x86-64";
|
||||||
const platform = process.platform === "darwin" ? "darwin" : "linux";
|
const platform = process.platform === "darwin" ? "darwin" : "linux";
|
||||||
return `${platform}_${arch}` as PlatformArch;
|
return `${platform}_${arch}` as PlatformArch;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function wasReceivedBefore({
|
async function wasReceivedBefore({
|
||||||
url,
|
url,
|
||||||
version
|
version
|
||||||
}: {
|
}: {
|
||||||
url: string;
|
url: string;
|
||||||
version: string;
|
version: string;
|
||||||
}): Promise<boolean> {
|
}): Promise<boolean> {
|
||||||
if (!url || !version) return false;
|
if (!url || !version) return false;
|
||||||
|
|
||||||
let received = false;
|
let received = false;
|
||||||
const pkg = url.replace("tea://packages/", "");
|
const pkg = url.replace("tea://packages/", "");
|
||||||
const searchString = `${pkg}:::${version}`;
|
const searchString = `${pkg}:::${version}`;
|
||||||
const notificationPath = path.join(getGuiPath(), "notifications");
|
const notificationPath = path.join(getGuiPath(), "notifications");
|
||||||
try {
|
try {
|
||||||
const fileContent = await readFile(notificationPath, "utf-8");
|
const fileContent = await readFile(notificationPath, "utf-8");
|
||||||
|
|
||||||
if (fileContent.includes(searchString)) {
|
if (fileContent.includes(searchString)) {
|
||||||
log.info("user has already been notified before of ", searchString);
|
log.info("user has already been notified before of ", searchString);
|
||||||
received = true;
|
received = true;
|
||||||
} else {
|
} else {
|
||||||
const appendString = fileContent ? `\n${searchString}` : searchString;
|
const appendString = fileContent ? `\n${searchString}` : searchString;
|
||||||
await writeFile(notificationPath, fileContent + appendString, "utf-8");
|
await writeFile(notificationPath, fileContent + appendString, "utf-8");
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.code === "ENOENT") {
|
if (error.code === "ENOENT") {
|
||||||
// If the file does not exist, create the file and write the string
|
// If the file does not exist, create the file and write the string
|
||||||
await writeFile(notificationPath, searchString, "utf-8");
|
await writeFile(notificationPath, searchString, "utf-8");
|
||||||
log.info("notification file created with the ", searchString);
|
log.info("notification file created with the ", searchString);
|
||||||
} else {
|
} else {
|
||||||
log.error("Error processing the file:", error);
|
log.error("Error processing the file:", error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return received;
|
return received;
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,195 +10,195 @@ import { mkdirp } from "mkdirp";
|
||||||
import fetch from "node-fetch";
|
import fetch from "node-fetch";
|
||||||
|
|
||||||
type Dir = {
|
type Dir = {
|
||||||
name: string;
|
name: string;
|
||||||
path: string;
|
path: string;
|
||||||
children?: Dir[];
|
children?: Dir[];
|
||||||
};
|
};
|
||||||
|
|
||||||
type ParsedVersion = { full_name: string; semVer: SemVer };
|
type ParsedVersion = { full_name: string; semVer: SemVer };
|
||||||
|
|
||||||
export const getTeaPath = () => {
|
export const getTeaPath = () => {
|
||||||
const homePath = app.getPath("home");
|
const homePath = app.getPath("home");
|
||||||
const teaPath = path.join(homePath, "./.tea");
|
const teaPath = path.join(homePath, "./.tea");
|
||||||
return teaPath;
|
return teaPath;
|
||||||
};
|
};
|
||||||
|
|
||||||
const guiFolder = path.join(getTeaPath(), "tea.xyz/gui");
|
const guiFolder = path.join(getTeaPath(), "tea.xyz/gui");
|
||||||
|
|
||||||
export const getGuiPath = () => {
|
export const getGuiPath = () => {
|
||||||
return path.join(getTeaPath(), "tea.xyz/gui");
|
return path.join(getTeaPath(), "tea.xyz/gui");
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function getInstalledPackages(): Promise<InstalledPackage[]> {
|
export async function getInstalledPackages(): Promise<InstalledPackage[]> {
|
||||||
const pkgsPath = getTeaPath();
|
const pkgsPath = getTeaPath();
|
||||||
log.info("recursively reading:", pkgsPath);
|
log.info("recursively reading:", pkgsPath);
|
||||||
const folders = await deepReadDir({
|
const folders = await deepReadDir({
|
||||||
dir: pkgsPath,
|
dir: pkgsPath,
|
||||||
continueDeeper: (name: string) => !semver.valid(name) && name !== ".tea",
|
continueDeeper: (name: string) => !semver.valid(name) && name !== ".tea",
|
||||||
filter: (name: string) => !!semver.valid(name) && name !== ".tea"
|
filter: (name: string) => !!semver.valid(name) && name !== ".tea"
|
||||||
});
|
});
|
||||||
|
|
||||||
const bottles = folders
|
const bottles = folders
|
||||||
.map((p: string) => p.split(".tea/")[1])
|
.map((p: string) => p.split(".tea/")[1])
|
||||||
.map(parseVersionFromPath)
|
.map(parseVersionFromPath)
|
||||||
.filter((v): v is ParsedVersion => !!v)
|
.filter((v): v is ParsedVersion => !!v)
|
||||||
.sort((a, b) => semverCompare(b.semVer, a.semVer));
|
.sort((a, b) => semverCompare(b.semVer, a.semVer));
|
||||||
|
|
||||||
log.info("installed bottles:", bottles.length);
|
log.info("installed bottles:", bottles.length);
|
||||||
|
|
||||||
return bottles.reduce<InstalledPackage[]>((pkgs, bottle) => {
|
return bottles.reduce<InstalledPackage[]>((pkgs, bottle) => {
|
||||||
const pkg = pkgs.find((v) => v.full_name === bottle.full_name);
|
const pkg = pkgs.find((v) => v.full_name === bottle.full_name);
|
||||||
if (pkg) {
|
if (pkg) {
|
||||||
pkg.installed_versions.push(bottle.semVer.version);
|
pkg.installed_versions.push(bottle.semVer.version);
|
||||||
} else {
|
} else {
|
||||||
pkgs.push({
|
pkgs.push({
|
||||||
full_name: bottle.full_name,
|
full_name: bottle.full_name,
|
||||||
installed_versions: [bottle.semVer.version]
|
installed_versions: [bottle.semVer.version]
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return pkgs;
|
return pkgs;
|
||||||
}, []);
|
}, []);
|
||||||
}
|
}
|
||||||
|
|
||||||
const parseVersionFromPath = (versionPath: string): ParsedVersion | null => {
|
const parseVersionFromPath = (versionPath: string): ParsedVersion | null => {
|
||||||
try {
|
try {
|
||||||
const path = versionPath.trim().split("/");
|
const path = versionPath.trim().split("/");
|
||||||
const version = path.pop();
|
const version = path.pop();
|
||||||
return {
|
return {
|
||||||
semVer: new SemVer(semver.clean(version || "") || ""),
|
semVer: new SemVer(semver.clean(version || "") || ""),
|
||||||
full_name: path.join("/")
|
full_name: path.join("/")
|
||||||
};
|
};
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
log.error("error parsing version from path: ", versionPath);
|
log.error("error parsing version from path: ", versionPath);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const semverTest =
|
const semverTest =
|
||||||
/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/g;
|
/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/g;
|
||||||
|
|
||||||
export const getPkgBottles = (packageDir: Dir): string[] => {
|
export const getPkgBottles = (packageDir: Dir): string[] => {
|
||||||
log.info("getting installed bottle for ", packageDir);
|
log.info("getting installed bottle for ", packageDir);
|
||||||
const bottles: string[] = [];
|
const bottles: string[] = [];
|
||||||
|
|
||||||
const pkg = packageDir.path.split(".tea/")[1];
|
const pkg = packageDir.path.split(".tea/")[1];
|
||||||
const version = pkg.split("/v")[1];
|
const version = pkg.split("/v")[1];
|
||||||
|
|
||||||
const isVersion = semverTest.test(version) || !isNaN(+version) || version === "*";
|
const isVersion = semverTest.test(version) || !isNaN(+version) || version === "*";
|
||||||
|
|
||||||
if (version && isVersion) {
|
if (version && isVersion) {
|
||||||
bottles.push(pkg);
|
bottles.push(pkg);
|
||||||
} else if (packageDir?.children?.length) {
|
} else if (packageDir?.children?.length) {
|
||||||
const childBottles = packageDir.children
|
const childBottles = packageDir.children
|
||||||
.map(getPkgBottles)
|
.map(getPkgBottles)
|
||||||
.reduce((arr, bottles) => [...arr, ...bottles], []);
|
.reduce((arr, bottles) => [...arr, ...bottles], []);
|
||||||
bottles.push(...childBottles);
|
bottles.push(...childBottles);
|
||||||
}
|
}
|
||||||
|
|
||||||
const foundBottles = bottles.filter((b) => b !== undefined).sort(); // ie: ["gohugo.io/v*", "gohugo.io/v0", "gohugo.io/v0.108", "gohugo.io/v0.108.0"]
|
const foundBottles = bottles.filter((b) => b !== undefined).sort(); // ie: ["gohugo.io/v*", "gohugo.io/v0", "gohugo.io/v0.108", "gohugo.io/v0.108.0"]
|
||||||
|
|
||||||
log.info(`Found ${foundBottles.length} bottles from `, packageDir);
|
log.info(`Found ${foundBottles.length} bottles from `, packageDir);
|
||||||
return foundBottles;
|
return foundBottles;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const deepReadDir = async ({
|
export const deepReadDir = async ({
|
||||||
dir,
|
dir,
|
||||||
continueDeeper,
|
continueDeeper,
|
||||||
filter
|
filter
|
||||||
}: {
|
}: {
|
||||||
dir: string;
|
dir: string;
|
||||||
continueDeeper?: (name: string) => boolean;
|
continueDeeper?: (name: string) => boolean;
|
||||||
filter?: (name: string) => boolean;
|
filter?: (name: string) => boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const arrayOfFiles: string[] = [];
|
const arrayOfFiles: string[] = [];
|
||||||
try {
|
try {
|
||||||
const files = fs.readdirSync(dir, { withFileTypes: true });
|
const files = fs.readdirSync(dir, { withFileTypes: true });
|
||||||
for (const f of files) {
|
for (const f of files) {
|
||||||
const nextPath = path.join(dir, f.name);
|
const nextPath = path.join(dir, f.name);
|
||||||
const deeper = continueDeeper ? continueDeeper(f.name) : true;
|
const deeper = continueDeeper ? continueDeeper(f.name) : true;
|
||||||
if (f.isDirectory() && deeper) {
|
if (f.isDirectory() && deeper) {
|
||||||
const nextFiles = await deepReadDir({ dir: nextPath, continueDeeper, filter });
|
const nextFiles = await deepReadDir({ dir: nextPath, continueDeeper, filter });
|
||||||
arrayOfFiles.push(...nextFiles);
|
arrayOfFiles.push(...nextFiles);
|
||||||
} else if (filter && filter(f.name)) {
|
} else if (filter && filter(f.name)) {
|
||||||
arrayOfFiles.push(nextPath);
|
arrayOfFiles.push(nextPath);
|
||||||
} else if (!filter) {
|
} else if (!filter) {
|
||||||
arrayOfFiles.push(nextPath);
|
arrayOfFiles.push(nextPath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
log.error(e);
|
log.error(e);
|
||||||
}
|
}
|
||||||
return arrayOfFiles;
|
return arrayOfFiles;
|
||||||
};
|
};
|
||||||
|
|
||||||
const listFilePath = path.join(getGuiPath(), "installed.json");
|
const listFilePath = path.join(getGuiPath(), "installed.json");
|
||||||
export const getPackagesInstalledList = async (): Promise<InstalledPackage[]> => {
|
export const getPackagesInstalledList = async (): Promise<InstalledPackage[]> => {
|
||||||
let list: InstalledPackage[] = [];
|
let list: InstalledPackage[] = [];
|
||||||
try {
|
try {
|
||||||
if (fs.existsSync(listFilePath)) {
|
if (fs.existsSync(listFilePath)) {
|
||||||
log.info("gui/installed.json file exists!");
|
log.info("gui/installed.json file exists!");
|
||||||
const listBuffer = await fs.readFileSync(listFilePath);
|
const listBuffer = await fs.readFileSync(listFilePath);
|
||||||
list = JSON.parse(listBuffer.toString()) as InstalledPackage[];
|
list = JSON.parse(listBuffer.toString()) as InstalledPackage[];
|
||||||
} else {
|
} else {
|
||||||
log.info("gui/installed.json does not exists!");
|
log.info("gui/installed.json does not exists!");
|
||||||
await mkdirp(guiFolder);
|
await mkdirp(guiFolder);
|
||||||
await updatePackageInstalledList([]);
|
await updatePackageInstalledList([]);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error(error);
|
log.error(error);
|
||||||
}
|
}
|
||||||
return list;
|
return list;
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function updatePackageInstalledList(list: InstalledPackage[]) {
|
export async function updatePackageInstalledList(list: InstalledPackage[]) {
|
||||||
try {
|
try {
|
||||||
log.info("creating:", listFilePath);
|
log.info("creating:", listFilePath);
|
||||||
await mkdirp(guiFolder);
|
await mkdirp(guiFolder);
|
||||||
await fs.writeFileSync(listFilePath, JSON.stringify(list), {
|
await fs.writeFileSync(listFilePath, JSON.stringify(list), {
|
||||||
encoding: "utf-8"
|
encoding: "utf-8"
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error(error);
|
log.error(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deletePackageFolder(fullName, version) {
|
export async function deletePackageFolder(fullName, version) {
|
||||||
try {
|
try {
|
||||||
const foldPath = path.join(getTeaPath(), fullName, `v${version}`);
|
const foldPath = path.join(getTeaPath(), fullName, `v${version}`);
|
||||||
log.info("rm:", foldPath);
|
log.info("rm:", foldPath);
|
||||||
await fs.rmdirSync(foldPath, { recursive: true });
|
await fs.rmdirSync(foldPath, { recursive: true });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error(error);
|
log.error(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function downloadImage(url: string, imagePath: string): Promise<void> {
|
async function downloadImage(url: string, imagePath: string): Promise<void> {
|
||||||
const response = await fetch(url);
|
const response = await fetch(url);
|
||||||
await new Promise<void>((resolve, reject) => {
|
await new Promise<void>((resolve, reject) => {
|
||||||
const fileStream = fs.createWriteStream(imagePath);
|
const fileStream = fs.createWriteStream(imagePath);
|
||||||
response.body.pipe(fileStream);
|
response.body.pipe(fileStream);
|
||||||
fileStream.on("finish", () => resolve());
|
fileStream.on("finish", () => resolve());
|
||||||
fileStream.on("error", (error) => reject(error));
|
fileStream.on("error", (error) => reject(error));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function cacheImage(url: string): Promise<string> {
|
export async function cacheImage(url: string): Promise<string> {
|
||||||
const imageFolder = path.join(getGuiPath(), "cached_images");
|
const imageFolder = path.join(getGuiPath(), "cached_images");
|
||||||
const imageName = path.basename(url);
|
const imageName = path.basename(url);
|
||||||
const imagePath = path.join(imageFolder, imageName);
|
const imagePath = path.join(imageFolder, imageName);
|
||||||
|
|
||||||
await mkdirp(imageFolder);
|
await mkdirp(imageFolder);
|
||||||
|
|
||||||
if (!fs.existsSync(imagePath)) {
|
if (!fs.existsSync(imagePath)) {
|
||||||
try {
|
try {
|
||||||
await downloadImage(url, imagePath);
|
await downloadImage(url, imagePath);
|
||||||
console.log("Image downloaded and cached:", imagePath);
|
console.log("Image downloaded and cached:", imagePath);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to download image:", error);
|
console.error("Failed to download image:", error);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log("Image already cached:", imagePath);
|
console.log("Image already cached:", imagePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
return `file://${imagePath}`;
|
return `file://${imagePath}`;
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,104 +12,104 @@ import { readSessionData, type Session } from "./auth";
|
||||||
const base = "https://api.tea.xyz";
|
const base = "https://api.tea.xyz";
|
||||||
const publicHeader = { Authorization: "public" };
|
const publicHeader = { Authorization: "public" };
|
||||||
export async function get<T>(urlPath: string) {
|
export async function get<T>(urlPath: string) {
|
||||||
try {
|
try {
|
||||||
log.info(`GET /v1/${urlPath}`);
|
log.info(`GET /v1/${urlPath}`);
|
||||||
|
|
||||||
const session = await readSessionData();
|
const session = await readSessionData();
|
||||||
const headers =
|
const headers =
|
||||||
session?.device_id && session?.user
|
session?.device_id && session?.user
|
||||||
? await getHeaders(`GET/${urlPath}`, session)
|
? await getHeaders(`GET/${urlPath}`, session)
|
||||||
: publicHeader;
|
: publicHeader;
|
||||||
|
|
||||||
const url = new URL(path.join("v1", urlPath), base).toString();
|
const url = new URL(path.join("v1", urlPath), base).toString();
|
||||||
// TODO: add headers
|
// TODO: add headers
|
||||||
const req = await axios.request<T>({
|
const req = await axios.request<T>({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
url,
|
url,
|
||||||
headers
|
headers
|
||||||
});
|
});
|
||||||
|
|
||||||
log.info("REQUEST:", urlPath, req.status);
|
log.info("REQUEST:", urlPath, req.status);
|
||||||
|
|
||||||
return req.data;
|
return req.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error(error);
|
log.error(error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function post<T>(urlPath: string, data: { [key: string]: any }) {
|
export async function post<T>(urlPath: string, data: { [key: string]: any }) {
|
||||||
try {
|
try {
|
||||||
log.info(`POST /v1/${urlPath}`);
|
log.info(`POST /v1/${urlPath}`);
|
||||||
|
|
||||||
const session = await readSessionData();
|
const session = await readSessionData();
|
||||||
const headers =
|
const headers =
|
||||||
session?.device_id && session?.user
|
session?.device_id && session?.user
|
||||||
? await getHeaders(`GET/${urlPath}`, session)
|
? await getHeaders(`GET/${urlPath}`, session)
|
||||||
: publicHeader;
|
: publicHeader;
|
||||||
|
|
||||||
const url = new URL(path.join("v1", urlPath), base).toString();
|
const url = new URL(path.join("v1", urlPath), base).toString();
|
||||||
const req = await axios.request<T>({
|
const req = await axios.request<T>({
|
||||||
method: "POST",
|
method: "POST",
|
||||||
url,
|
url,
|
||||||
headers,
|
headers,
|
||||||
data
|
data
|
||||||
});
|
});
|
||||||
|
|
||||||
log.info("REQUEST:", urlPath, req.status);
|
log.info("REQUEST:", urlPath, req.status);
|
||||||
|
|
||||||
return req.data;
|
return req.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error(error);
|
log.error(error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getHeaders(path: string, session: Session) {
|
async function getHeaders(path: string, session: Session) {
|
||||||
const unixMs = new Date().getTime();
|
const unixMs = new Date().getTime();
|
||||||
const unixHexSecs = Math.round(unixMs / 1000).toString(16); // hex
|
const unixHexSecs = Math.round(unixMs / 1000).toString(16); // hex
|
||||||
const deviceId = session.device_id?.split("-")[0];
|
const deviceId = session.device_id?.split("-")[0];
|
||||||
const preHash = [unixHexSecs, session.key, deviceId, path].join("");
|
const preHash = [unixHexSecs, session.key, deviceId, path].join("");
|
||||||
|
|
||||||
const Authorization = bcrypt.hashSync(preHash, 10);
|
const Authorization = bcrypt.hashSync(preHash, 10);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
Authorization,
|
Authorization,
|
||||||
["tea-ts"]: unixMs.toString(),
|
["tea-ts"]: unixMs.toString(),
|
||||||
["tea-uid"]: session.user?.developer_id,
|
["tea-uid"]: session.user?.developer_id,
|
||||||
["tea-gui_id"]: session.device_id
|
["tea-gui_id"]: session.device_id
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function syncLogsAt(prefix: string) {
|
export async function syncLogsAt(prefix: string) {
|
||||||
const logDir = path.join(app.getPath("home"), "Library/Logs/tea");
|
const logDir = path.join(app.getPath("home"), "Library/Logs/tea");
|
||||||
// ['/Users/neil/Library/Logs/tea/main.log']
|
// ['/Users/neil/Library/Logs/tea/main.log']
|
||||||
const logFiles = await deepReadDir({ dir: logDir });
|
const logFiles = await deepReadDir({ dir: logDir });
|
||||||
const files = logFiles.map((p) => {
|
const files = logFiles.map((p) => {
|
||||||
const paths = p.split("/");
|
const paths = p.split("/");
|
||||||
return paths.pop();
|
return paths.pop();
|
||||||
});
|
});
|
||||||
|
|
||||||
const signedUrls = await post<{ [key: string]: string }>(`/gui/${prefix}/sync-log-files`, {
|
const signedUrls = await post<{ [key: string]: string }>(`/gui/${prefix}/sync-log-files`, {
|
||||||
files
|
files
|
||||||
});
|
});
|
||||||
if (signedUrls) {
|
if (signedUrls) {
|
||||||
for (const key in signedUrls) {
|
for (const key in signedUrls) {
|
||||||
const fileIndex = files.indexOf(key);
|
const fileIndex = files.indexOf(key);
|
||||||
const filePath = logFiles[fileIndex];
|
const filePath = logFiles[fileIndex];
|
||||||
if (filePath) {
|
if (filePath) {
|
||||||
const payload = createReadStream(filePath);
|
const payload = createReadStream(filePath);
|
||||||
const response = await fetch(signedUrls[key], {
|
const response = await fetch(signedUrls[key], {
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
body: payload,
|
body: payload,
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Length": statSync(filePath).size.toString()
|
"Content-Length": statSync(filePath).size.toString()
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
log.info("uploading log:", key, response.status);
|
log.info("uploading log:", key, response.status);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default get;
|
export default get;
|
||||||
|
|
|
@ -1,19 +1,19 @@
|
||||||
const isVite = () => {
|
const isVite = () => {
|
||||||
try {
|
try {
|
||||||
return window.location.href.includes("is-vite");
|
return window.location.href.includes("is-vite");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!isVite()) {
|
if (!isVite()) {
|
||||||
const { init } = window.require("@sentry/electron/renderer");
|
const { init } = window.require("@sentry/electron/renderer");
|
||||||
const SvelteSentry = window.require("@sentry/svelte");
|
const SvelteSentry = window.require("@sentry/svelte");
|
||||||
init(
|
init(
|
||||||
{
|
{
|
||||||
dsn: "https://5ff29bb5b3b64cd4bd4f4960ef1db2e3@o4504750197899264.ingest.sentry.io/4504750206746624",
|
dsn: "https://5ff29bb5b3b64cd4bd4f4960ef1db2e3@o4504750197899264.ingest.sentry.io/4504750206746624",
|
||||||
debug: true
|
debug: true
|
||||||
},
|
},
|
||||||
SvelteSentry.init
|
SvelteSentry.init
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,41 +9,41 @@ const PROJECT_ROOT = join(PACKAGE_ROOT, "../..");
|
||||||
* @see https://vitejs.dev/config/
|
* @see https://vitejs.dev/config/
|
||||||
*/
|
*/
|
||||||
const config = {
|
const config = {
|
||||||
root: PACKAGE_ROOT,
|
root: PACKAGE_ROOT,
|
||||||
envDir: PROJECT_ROOT,
|
envDir: PROJECT_ROOT,
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
"/@/": join(PACKAGE_ROOT, "src") + "/"
|
"/@/": join(PACKAGE_ROOT, "src") + "/"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
build: {
|
build: {
|
||||||
ssr: true,
|
ssr: true,
|
||||||
sourcemap: "inline",
|
sourcemap: "inline",
|
||||||
outDir: "dist",
|
outDir: "dist",
|
||||||
assetsDir: ".",
|
assetsDir: ".",
|
||||||
minify: process.env.MODE !== "development",
|
minify: process.env.MODE !== "development",
|
||||||
lib: {
|
lib: {
|
||||||
entry: "electron.ts",
|
entry: "electron.ts",
|
||||||
formats: ["cjs"]
|
formats: ["cjs"]
|
||||||
},
|
},
|
||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
output: {
|
output: {
|
||||||
entryFileNames: "[name].cjs"
|
entryFileNames: "[name].cjs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
emptyOutDir: true,
|
emptyOutDir: true,
|
||||||
reportCompressedSize: false
|
reportCompressedSize: false
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
viteStaticCopy({
|
viteStaticCopy({
|
||||||
targets: [
|
targets: [
|
||||||
{
|
{
|
||||||
src: "./preload.cjs",
|
src: "./preload.cjs",
|
||||||
dest: "."
|
dest: "."
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
export default config;
|
export default config;
|
||||||
|
|
|
@ -1,121 +1,121 @@
|
||||||
{
|
{
|
||||||
"name": "tea",
|
"name": "tea",
|
||||||
"version": "0.0.45",
|
"version": "0.0.45",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "tea gui app",
|
"description": "tea gui app",
|
||||||
"author": "tea.xyz",
|
"author": "tea.xyz",
|
||||||
"main": "electron/dist/electron.cjs",
|
"main": "electron/dist/electron.cjs",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"prepare": "svelte-kit sync",
|
"prepare": "svelte-kit sync",
|
||||||
"dev": "cross-env NODE_ENV=dev npm run dev:all",
|
"dev": "cross-env NODE_ENV=dev npm run dev:all",
|
||||||
"dev:all": "concurrently -n=svelte,electron -c='#ff3e00',blue \"pnpm dev:main\" \"pnpm dev:svelte\" \"pnpm dev:electron\"",
|
"dev:all": "concurrently -n=svelte,electron -c='#ff3e00',blue \"pnpm dev:main\" \"pnpm dev:svelte\" \"pnpm dev:electron\"",
|
||||||
"dev:svelte": "vite dev",
|
"dev:svelte": "vite dev",
|
||||||
"dev:electron": "electron electron/dist/electron.cjs",
|
"dev:electron": "electron electron/dist/electron.cjs",
|
||||||
"dev:main": "cd ./electron && vite build --config ./vite.config.js --watch",
|
"dev:main": "cd ./electron && vite build --config ./vite.config.js --watch",
|
||||||
"build:main": "cp package.json electron && cd ./electron && vite --config ./vite.config.js build --base . && rm package.json",
|
"build:main": "cp package.json electron && cd ./electron && vite --config ./vite.config.js build --base . && rm package.json",
|
||||||
"pack": "electron-builder --dir --config electron-builder.config.cjs",
|
"pack": "electron-builder --dir --config electron-builder.config.cjs",
|
||||||
"predist": "node ./scripts/predist.cjs",
|
"predist": "node ./scripts/predist.cjs",
|
||||||
"dist": "pnpm build && electron-builder --config electron-builder.config.cjs",
|
"dist": "pnpm build && electron-builder --config electron-builder.config.cjs",
|
||||||
"package": "pnpm build && electron-builder --config electron-builder.config.cjs",
|
"package": "pnpm build && electron-builder --config electron-builder.config.cjs",
|
||||||
"dev:package": "pnpm build && electron-builder --config electron-builder.config.cjs --dir",
|
"dev:package": "pnpm build && electron-builder --config electron-builder.config.cjs --dir",
|
||||||
"electron": "concurrently --kill-others \"vite dev\" \"electron electron/dist/electron.cjs\"",
|
"electron": "concurrently --kill-others \"vite dev\" \"electron electron/dist/electron.cjs\"",
|
||||||
"olddev": "vite dev",
|
"olddev": "vite dev",
|
||||||
"build": "pnpm build:main && vite build && cp build/app.html build/index.html",
|
"build": "pnpm build:main && vite build && cp build/app.html build/index.html",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"unit:test": "vitest",
|
"unit:test": "vitest",
|
||||||
"coverage": "vitest run --coverage",
|
"coverage": "vitest run --coverage",
|
||||||
"test": "playwright test",
|
"test": "playwright test",
|
||||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --compiler-warnings \"css-unused-selector:ignore\"",
|
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --compiler-warnings \"css-unused-selector:ignore\"",
|
||||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --compiler-warnings \"css-unused-selector:ignore\" --watch",
|
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --compiler-warnings \"css-unused-selector:ignore\" --watch",
|
||||||
"lint": "prettier --plugin-search-dir . --check . && eslint .",
|
"lint": "prettier --check . && eslint .",
|
||||||
"format": "prettier --plugin-search-dir . --write ."
|
"format": "prettier --write ."
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@electron/notarize": "^1.2.3",
|
"@electron/notarize": "^1.2.3",
|
||||||
"@playwright/experimental-ct-svelte": "^1.29.2",
|
"@playwright/experimental-ct-svelte": "^1.29.2",
|
||||||
"@playwright/test": "1.25.0",
|
"@playwright/test": "1.25.0",
|
||||||
"@sveltejs/adapter-auto": "^1.0.0",
|
"@sveltejs/adapter-auto": "^1.0.0",
|
||||||
"@sveltejs/adapter-node": "^1.0.0-next.101",
|
"@sveltejs/adapter-node": "^1.0.0-next.101",
|
||||||
"@sveltejs/adapter-static": "^1.0.0-next.48",
|
"@sveltejs/adapter-static": "^1.0.0-next.48",
|
||||||
"@sveltejs/kit": "^1.0.0-next.562",
|
"@sveltejs/kit": "^1.0.0-next.562",
|
||||||
"@tea/ui": "workspace:*",
|
"@tea/ui": "workspace:*",
|
||||||
"@testing-library/jest-dom": "^5.16.5",
|
"@testing-library/jest-dom": "^5.16.5",
|
||||||
"@testing-library/svelte": "^3.2.2",
|
"@testing-library/svelte": "^3.2.2",
|
||||||
"@types/bcryptjs": "^2.4.2",
|
"@types/bcryptjs": "^2.4.2",
|
||||||
"@types/js-yaml": "^4.0.5",
|
"@types/js-yaml": "^4.0.5",
|
||||||
"@types/mixpanel-browser": "^2.38.1",
|
"@types/mixpanel-browser": "^2.38.1",
|
||||||
"@types/testing-library__jest-dom": "^5.14.5",
|
"@types/testing-library__jest-dom": "^5.14.5",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.27.0",
|
"@typescript-eslint/eslint-plugin": "^5.27.0",
|
||||||
"@typescript-eslint/parser": "^5.27.0",
|
"@typescript-eslint/parser": "^5.27.0",
|
||||||
"autoprefixer": "^10.4.13",
|
"autoprefixer": "^10.4.13",
|
||||||
"concurrently": "^7.6.0",
|
"concurrently": "^7.6.0",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"electron": "22.1.0",
|
"electron": "22.1.0",
|
||||||
"electron-builder": "^23.6.0",
|
"electron-builder": "^23.6.0",
|
||||||
"electron-reloader": "^1.2.3",
|
"electron-reloader": "^1.2.3",
|
||||||
"eslint": "^8.16.0",
|
"eslint": "^8.16.0",
|
||||||
"eslint-config-prettier": "^8.3.0",
|
"eslint-config-prettier": "^8.3.0",
|
||||||
"eslint-plugin-svelte3": "^4.0.0",
|
"eslint-plugin-svelte3": "^4.0.0",
|
||||||
"jsdom": "^21.0.0",
|
"jsdom": "^21.0.0",
|
||||||
"postcss": "^8.4.19",
|
"postcss": "^8.4.19",
|
||||||
"prettier": "^2.7.1",
|
"prettier": "^2.7.1",
|
||||||
"prettier-plugin-svelte": "^2.7.0",
|
"prettier-plugin-svelte": "^2.7.0",
|
||||||
"prettier-plugin-tailwindcss": "^0.2.0",
|
"prettier-plugin-tailwindcss": "^0.2.0",
|
||||||
"svelte": "^3.55.1",
|
"svelte": "^3.55.1",
|
||||||
"svelte-check": "^2.8.0",
|
"svelte-check": "^2.8.0",
|
||||||
"svelte-preprocess": "^5.0.1",
|
"svelte-preprocess": "^5.0.1",
|
||||||
"svelte2tsx": "^0.5.20",
|
"svelte2tsx": "^0.5.20",
|
||||||
"tailwindcss": "^3.2.4",
|
"tailwindcss": "^3.2.4",
|
||||||
"tslib": "^2.3.1",
|
"tslib": "^2.3.1",
|
||||||
"typescript": "^4.7.4",
|
"typescript": "^4.7.4",
|
||||||
"vite": "^4.1.1",
|
"vite": "^4.1.1",
|
||||||
"vitest": "^0.28.3"
|
"vitest": "^0.28.3"
|
||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@crowdin/ota-client": "^0.7.0",
|
"@crowdin/ota-client": "^0.7.0",
|
||||||
"@electron/asar": "^3.2.3",
|
"@electron/asar": "^3.2.3",
|
||||||
"@sentry/browser": "^7.49.0",
|
"@sentry/browser": "^7.49.0",
|
||||||
"@sentry/electron": "^4.4.0",
|
"@sentry/electron": "^4.4.0",
|
||||||
"@sentry/svelte": "^7.47.0",
|
"@sentry/svelte": "^7.47.0",
|
||||||
"@types/electron": "^1.6.10",
|
"@types/electron": "^1.6.10",
|
||||||
"@types/mousetrap": "^1.6.11",
|
"@types/mousetrap": "^1.6.11",
|
||||||
"@vitest/coverage-c8": "^0.27.1",
|
"@vitest/coverage-c8": "^0.27.1",
|
||||||
"axios": "^1.3.2",
|
"axios": "^1.3.2",
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
"buffer": "^6.0.3",
|
"buffer": "^6.0.3",
|
||||||
"custom-electron-titlebar": "4.2.0-beta.0",
|
"custom-electron-titlebar": "4.2.0-beta.0",
|
||||||
"dayjs": "^1.11.7",
|
"dayjs": "^1.11.7",
|
||||||
"electron-context-menu": "^3.6.1",
|
"electron-context-menu": "^3.6.1",
|
||||||
"electron-log": "^4.4.8",
|
"electron-log": "^4.4.8",
|
||||||
"electron-serve": "^1.1.0",
|
"electron-serve": "^1.1.0",
|
||||||
"electron-updater": "^5.3.0",
|
"electron-updater": "^5.3.0",
|
||||||
"electron-vite": "^1.0.18",
|
"electron-vite": "^1.0.18",
|
||||||
"electron-window-state": "^5.0.3",
|
"electron-window-state": "^5.0.3",
|
||||||
"fuse.js": "^6.6.2",
|
"fuse.js": "^6.6.2",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"lorem-ipsum": "^2.0.8",
|
"lorem-ipsum": "^2.0.8",
|
||||||
"mixpanel-browser": "^2.45.0",
|
"mixpanel-browser": "^2.45.0",
|
||||||
"mkdirp": "^2.1.3",
|
"mkdirp": "^2.1.3",
|
||||||
"moment": "^2.29.4",
|
"moment": "^2.29.4",
|
||||||
"mousetrap": "^1.6.5",
|
"mousetrap": "^1.6.5",
|
||||||
"pushy-electron": "^1.0.11",
|
"pushy-electron": "^1.0.11",
|
||||||
"renderer": "link:@types/electron/renderer",
|
"renderer": "link:@types/electron/renderer",
|
||||||
"semver": "^7.3.8",
|
"semver": "^7.3.8",
|
||||||
"svelte-infinite-scroll": "^2.0.1",
|
"svelte-infinite-scroll": "^2.0.1",
|
||||||
"svelte-markdown": "^0.2.3",
|
"svelte-markdown": "^0.2.3",
|
||||||
"svelte-watch-resize": "^1.0.3",
|
"svelte-watch-resize": "^1.0.3",
|
||||||
"sveltekit-i18n": "^2.2.2",
|
"sveltekit-i18n": "^2.2.2",
|
||||||
"upath": "^2.0.1",
|
"upath": "^2.0.1",
|
||||||
"vite-plugin-static-copy": "^0.13.1",
|
"vite-plugin-static-copy": "^0.13.1",
|
||||||
"yaml": "^2.2.1"
|
"yaml": "^2.2.1"
|
||||||
},
|
},
|
||||||
"pnpm": {
|
"pnpm": {
|
||||||
"onlyBuiltDependencies": [
|
"onlyBuiltDependencies": [
|
||||||
"@tea/ui"
|
"@tea/ui"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"homepage": "https://tea.xyz",
|
"homepage": "https://tea.xyz",
|
||||||
"repository": "https://github.com/teaxyz/gui.git"
|
"repository": "https://github.com/teaxyz/gui.git"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import type { PlaywrightTestConfig } from "@playwright/test";
|
import type { PlaywrightTestConfig } from "@playwright/test";
|
||||||
|
|
||||||
const config: PlaywrightTestConfig = {
|
const config: PlaywrightTestConfig = {
|
||||||
webServer: {
|
webServer: {
|
||||||
command: "npm run build && npm run preview",
|
command: "npm run build && npm run preview",
|
||||||
port: 4173
|
port: 4173
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export default config;
|
export default config;
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
const { theme, plugins } = require("@tea/ui/tailwind.config.cjs");
|
const { theme, plugins } = require("@tea/ui/tailwind.config.cjs");
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
plugins: {
|
plugins: {
|
||||||
tailwindcss: {
|
tailwindcss: {
|
||||||
content: ["./src/**/*.{html,svelte,ts,js}", "../ui/src/**/*.{html,svelte,ts,js}"],
|
content: ["./src/**/*.{html,svelte,ts,js}", "../ui/src/**/*.{html,svelte,ts,js}"],
|
||||||
theme,
|
theme,
|
||||||
plugins: [...plugins]
|
plugins: [...plugins]
|
||||||
},
|
},
|
||||||
autoprefixer: {}
|
autoprefixer: {}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -10,52 +10,52 @@ const hash = "cf849610ca66250f0954379ct4t";
|
||||||
const client = new otaClient.default(hash);
|
const client = new otaClient.default(hash);
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
const configPath = path.join(__dirname, "../electron/config.json");
|
const configPath = path.join(__dirname, "../electron/config.json");
|
||||||
const config = {
|
const config = {
|
||||||
PUSHY_APP_ID: process.env.PUSHY_APP_ID || ""
|
PUSHY_APP_ID: process.env.PUSHY_APP_ID || ""
|
||||||
};
|
};
|
||||||
await fs.writeFileSync(configPath, JSON.stringify(config, null, 2), "utf-8");
|
await fs.writeFileSync(configPath, JSON.stringify(config, null, 2), "utf-8");
|
||||||
|
|
||||||
console.log("getting latest translation!");
|
console.log("getting latest translation!");
|
||||||
if (!process.env.SYNC_I18N) return;
|
if (!process.env.SYNC_I18N) return;
|
||||||
|
|
||||||
const [languagesList, translationsRaw] = await Promise.all([
|
const [languagesList, translationsRaw] = await Promise.all([
|
||||||
client.getLanguageObjects(),
|
client.getLanguageObjects(),
|
||||||
client.getStrings()
|
client.getStrings()
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const lang = languagesList.reduce((map, lang) => {
|
const lang = languagesList.reduce((map, lang) => {
|
||||||
map[lang.id] = lang.name;
|
map[lang.id] = lang.name;
|
||||||
return map;
|
return map;
|
||||||
}, {});
|
}, {});
|
||||||
|
|
||||||
const translations = languagesList.reduce((map, langRaw) => {
|
const translations = languagesList.reduce((map, langRaw) => {
|
||||||
map[langRaw.id] = {
|
map[langRaw.id] = {
|
||||||
lang
|
lang
|
||||||
};
|
};
|
||||||
const translation = translationsRaw[langRaw.id];
|
const translation = translationsRaw[langRaw.id];
|
||||||
for (const k in translation) {
|
for (const k in translation) {
|
||||||
const key = [langRaw.id, k].join(".");
|
const key = [langRaw.id, k].join(".");
|
||||||
_.set(map, key, translation[k]);
|
_.set(map, key, translation[k]);
|
||||||
}
|
}
|
||||||
return map;
|
return map;
|
||||||
}, {});
|
}, {});
|
||||||
|
|
||||||
const translationsPath = path.join(__dirname, "../src/libs/translations/translations.json");
|
const translationsPath = path.join(__dirname, "../src/libs/translations/translations.json");
|
||||||
|
|
||||||
defaultEnTranslation.en.lang = translations.en.lang;
|
defaultEnTranslation.en.lang = translations.en.lang;
|
||||||
await fs.writeFileSync(
|
await fs.writeFileSync(
|
||||||
translationsPath,
|
translationsPath,
|
||||||
JSON.stringify(
|
JSON.stringify(
|
||||||
{
|
{
|
||||||
...translations,
|
...translations,
|
||||||
...defaultEnTranslation
|
...defaultEnTranslation
|
||||||
},
|
},
|
||||||
null,
|
null,
|
||||||
2
|
2
|
||||||
),
|
),
|
||||||
"utf-8"
|
"utf-8"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
main();
|
main();
|
||||||
|
|
|
@ -7,8 +7,8 @@ const token = process.env.CROWDIN_API_TOKEN;
|
||||||
const projectId = 570715;
|
const projectId = 570715;
|
||||||
const fileId = 7;
|
const fileId = 7;
|
||||||
const headers = {
|
const headers = {
|
||||||
Authorization: `Bearer ${token}`,
|
Authorization: `Bearer ${token}`,
|
||||||
"Content-Type": "application/json"
|
"Content-Type": "application/json"
|
||||||
};
|
};
|
||||||
|
|
||||||
const englishRaw = translations["en"];
|
const englishRaw = translations["en"];
|
||||||
|
@ -16,85 +16,85 @@ const englishRaw = translations["en"];
|
||||||
delete englishRaw.lang;
|
delete englishRaw.lang;
|
||||||
|
|
||||||
function flattenObject(o, prefix = "", result = {}, keepNull = true) {
|
function flattenObject(o, prefix = "", result = {}, keepNull = true) {
|
||||||
if (_.isString(o) || _.isNumber(o) || _.isBoolean(o) || (keepNull && _.isNull(o))) {
|
if (_.isString(o) || _.isNumber(o) || _.isBoolean(o) || (keepNull && _.isNull(o))) {
|
||||||
result[prefix] = o;
|
result[prefix] = o;
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_.isArray(o) || _.isPlainObject(o)) {
|
if (_.isArray(o) || _.isPlainObject(o)) {
|
||||||
for (let i in o) {
|
for (let i in o) {
|
||||||
let pref = prefix;
|
let pref = prefix;
|
||||||
if (_.isArray(o)) {
|
if (_.isArray(o)) {
|
||||||
pref = pref + `[${i}]`;
|
pref = pref + `[${i}]`;
|
||||||
} else {
|
} else {
|
||||||
if (_.isEmpty(prefix)) {
|
if (_.isEmpty(prefix)) {
|
||||||
pref = i;
|
pref = i;
|
||||||
} else {
|
} else {
|
||||||
pref = prefix + "." + i;
|
pref = prefix + "." + i;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
flattenObject(o[i], pref, result, keepNull);
|
flattenObject(o[i], pref, result, keepNull);
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
const flattenedEnglish = flattenObject(englishRaw);
|
const flattenedEnglish = flattenObject(englishRaw);
|
||||||
|
|
||||||
const getStrings = async () => {
|
const getStrings = async () => {
|
||||||
const { data } = await axios({
|
const { data } = await axios({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
url: `https://api.crowdin.com/api/v2/projects/${projectId}/strings?limit=500`,
|
url: `https://api.crowdin.com/api/v2/projects/${projectId}/strings?limit=500`,
|
||||||
headers
|
headers
|
||||||
});
|
});
|
||||||
return data.data;
|
return data.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
const data = await getStrings();
|
const data = await getStrings();
|
||||||
for (const key in flattenedEnglish) {
|
for (const key in flattenedEnglish) {
|
||||||
const found = data.find((data) => data.data.identifier === `"${key}"`);
|
const found = data.find((data) => data.data.identifier === `"${key}"`);
|
||||||
if (found && found.data.text !== flattenedEnglish[key]) {
|
if (found && found.data.text !== flattenedEnglish[key]) {
|
||||||
console.log("update!", key, found.data.text, flattenedEnglish[key]);
|
console.log("update!", key, found.data.text, flattenedEnglish[key]);
|
||||||
await updateString(found.data.id, flattenedEnglish[key]);
|
await updateString(found.data.id, flattenedEnglish[key]);
|
||||||
} else if (!found) {
|
} else if (!found) {
|
||||||
// insert add
|
// insert add
|
||||||
await createString(key, flattenedEnglish[key]);
|
await createString(key, flattenedEnglish[key]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createString(key, text) {
|
async function createString(key, text) {
|
||||||
await axios({
|
await axios({
|
||||||
method: "POST",
|
method: "POST",
|
||||||
url: `https://api.crowdin.com/api/v2/projects/${projectId}/strings`,
|
url: `https://api.crowdin.com/api/v2/projects/${projectId}/strings`,
|
||||||
headers,
|
headers,
|
||||||
data: {
|
data: {
|
||||||
text,
|
text,
|
||||||
identifier: `"${key}"`,
|
identifier: `"${key}"`,
|
||||||
fileId,
|
fileId,
|
||||||
context: ` -> ${key}`,
|
context: ` -> ${key}`,
|
||||||
isHidden: false,
|
isHidden: false,
|
||||||
maxLength: 0,
|
maxLength: 0,
|
||||||
labelIds: []
|
labelIds: []
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateString(stringId, value) {
|
async function updateString(stringId, value) {
|
||||||
const d = await axios({
|
const d = await axios({
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
url: `https://api.crowdin.com/api/v2/projects/${projectId}/strings/${stringId}`,
|
url: `https://api.crowdin.com/api/v2/projects/${projectId}/strings/${stringId}`,
|
||||||
headers,
|
headers,
|
||||||
data: [
|
data: [
|
||||||
{
|
{
|
||||||
value,
|
value,
|
||||||
op: "replace",
|
op: "replace",
|
||||||
path: "/text"
|
path: "/text"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
main();
|
main();
|
||||||
|
|
|
@ -3,26 +3,26 @@
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: "mona-sans";
|
font-family: "mona-sans";
|
||||||
src: url("/fonts/mona-sans-bold.woff2");
|
src: url("/fonts/mona-sans-bold.woff2");
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: "inter";
|
font-family: "inter";
|
||||||
src: url("/fonts/inter-regular.woff2");
|
src: url("/fonts/inter-regular.woff2");
|
||||||
}
|
}
|
||||||
|
|
||||||
html {
|
html {
|
||||||
background-color: #1a1a1a;
|
background-color: #1a1a1a;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
cursor: default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
html {
|
html {
|
||||||
font-family: sono, sans-serif;
|
font-family: sono, sans-serif;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-primary,
|
.text-primary,
|
||||||
|
@ -33,9 +33,9 @@ h4,
|
||||||
h5,
|
h5,
|
||||||
h6,
|
h6,
|
||||||
.click-copy {
|
.click-copy {
|
||||||
font-family: "mona-sans";
|
font-family: "mona-sans";
|
||||||
}
|
}
|
||||||
|
|
||||||
.pk-version {
|
.pk-version {
|
||||||
font-family: "inter";
|
font-family: "inter";
|
||||||
}
|
}
|
||||||
|
|
18
modules/desktop/src/app.d.ts
vendored
18
modules/desktop/src/app.d.ts
vendored
|
@ -2,17 +2,17 @@
|
||||||
// for information about these interfaces
|
// for information about these interfaces
|
||||||
// and what to do when importing types
|
// and what to do when importing types
|
||||||
declare namespace App {
|
declare namespace App {
|
||||||
// interface Locals {}
|
// interface Locals {}
|
||||||
// interface PageData {}
|
// interface PageData {}
|
||||||
// interface Error {}
|
// interface Error {}
|
||||||
// interface Platform {}
|
// interface Platform {}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Declare custom event handlers here to make typscript happy.
|
// Declare custom event handlers here to make typscript happy.
|
||||||
declare namespace svelte.JSX {
|
declare namespace svelte.JSX {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
interface HTMLAttributes<T> {
|
interface HTMLAttributes<T> {
|
||||||
onclick_outside?: () => void;
|
onclick_outside?: () => void;
|
||||||
onleave_delay?: () => void;
|
onleave_delay?: () => void;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,19 +1,19 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||||
<meta name="viewport" content="width=device-width" />
|
<meta name="viewport" content="width=device-width" />
|
||||||
%sveltekit.head%
|
%sveltekit.head%
|
||||||
<style>
|
<style>
|
||||||
html {
|
html {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div>%sveltekit.body%</div>
|
<div>%sveltekit.body%</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import '$appcss';
|
import "$appcss";
|
||||||
import Placeholder from '$components/placeholder/placeholder.svelte';
|
import Placeholder from "$components/placeholder/placeholder.svelte";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Placeholder label="Badges" />
|
<Placeholder label="Badges" />
|
|
@ -1,5 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import '$appcss';
|
import "$appcss";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<section class="border-gray h-56 border bg-black" />
|
<section class="border-gray h-56 border bg-black" />
|
||||||
|
|
|
@ -1,78 +1,78 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import "$appcss";
|
import "$appcss";
|
||||||
// import { t } from '$libs/translations';
|
// import { t } from '$libs/translations';
|
||||||
import { SideMenuOptions } from "$libs/types";
|
import { SideMenuOptions } from "$libs/types";
|
||||||
import Preloader from "@tea/ui/Preloader/Preloader.svelte";
|
import Preloader from "@tea/ui/Preloader/Preloader.svelte";
|
||||||
import Package from "$components/packages/package.svelte";
|
import Package from "$components/packages/package.svelte";
|
||||||
import { packagesStore } from "$libs/stores";
|
import { packagesStore } from "$libs/stores";
|
||||||
|
|
||||||
const { packageList: allPackages } = packagesStore;
|
const { packageList: allPackages } = packagesStore;
|
||||||
export let packageFilter: SideMenuOptions = SideMenuOptions.discover;
|
export let packageFilter: SideMenuOptions = SideMenuOptions.discover;
|
||||||
|
|
||||||
export let scrollY = 0;
|
export let scrollY = 0;
|
||||||
|
|
||||||
const onScroll = (e: Event) => {
|
const onScroll = (e: Event) => {
|
||||||
const target = e.target as HTMLInputElement;
|
const target = e.target as HTMLInputElement;
|
||||||
scrollY = target.scrollTop || 0;
|
scrollY = target.scrollTop || 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
$: packages = $allPackages
|
$: packages = $allPackages
|
||||||
.filter((p) => p.categories.includes(SideMenuOptions.discover))
|
.filter((p) => p.categories.includes(SideMenuOptions.discover))
|
||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
return a.manual_sorting - b.manual_sorting;
|
return a.manual_sorting - b.manual_sorting;
|
||||||
});
|
});
|
||||||
console.log("test", packages);
|
console.log("test", packages);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="relative h-full w-full">
|
<div class="relative h-full w-full">
|
||||||
<ul class="flex flex-col items-stretch" on:scroll={onScroll}>
|
<ul class="flex flex-col items-stretch" on:scroll={onScroll}>
|
||||||
{#if packages.length > 0}
|
{#if packages.length > 0}
|
||||||
{#each packages as pkg, idx}
|
{#each packages as pkg, idx}
|
||||||
<div class="z-1 p-1">
|
<div class="z-1 p-1">
|
||||||
<Package tab={packageFilter} {pkg} layout={idx % 2 === 0 ? "left" : "right"} />
|
<Package tab={packageFilter} {pkg} layout={idx % 2 === 0 ? "left" : "right"} />
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
{:else}
|
{:else}
|
||||||
{#each Array(9) as _}
|
{#each Array(9) as _}
|
||||||
<section class="card p-1 h-{238}">
|
<section class="card p-1 h-{238}">
|
||||||
<div class="border-gray h-full w-full border">
|
<div class="border-gray h-full w-full border">
|
||||||
<Preloader />
|
<Preloader />
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
{/each}
|
{/each}
|
||||||
{/if}
|
{/if}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
ul {
|
ul {
|
||||||
margin-top: 0px;
|
margin-top: 0px;
|
||||||
padding-top: 80px;
|
padding-top: 80px;
|
||||||
padding-bottom: 8px;
|
padding-bottom: 8px;
|
||||||
height: calc(100vh - 49px);
|
height: calc(100vh - 49px);
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
padding-right: 4px;
|
padding-right: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* width */
|
/* width */
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
width: 6px;
|
width: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Track */
|
/* Track */
|
||||||
::-webkit-scrollbar-track {
|
::-webkit-scrollbar-track {
|
||||||
background: #272626;
|
background: #272626;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Handle */
|
/* Handle */
|
||||||
::-webkit-scrollbar-thumb {
|
::-webkit-scrollbar-thumb {
|
||||||
background: #949494;
|
background: #949494;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Handle on hover */
|
/* Handle on hover */
|
||||||
::-webkit-scrollbar-thumb:hover {
|
::-webkit-scrollbar-thumb:hover {
|
||||||
background: white;
|
background: white;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,25 +1,25 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import '$appcss';
|
import "$appcss";
|
||||||
import { t } from '$libs/translations';
|
import { t } from "$libs/translations";
|
||||||
import type { AirtablePost } from '@tea/ui/types';
|
import type { AirtablePost } from "@tea/ui/types";
|
||||||
import Posts from '@tea/ui/posts/posts.svelte';
|
import Posts from "@tea/ui/posts/posts.svelte";
|
||||||
import PanelHeader from '@tea/ui/panel-header/panel-header.svelte';
|
import PanelHeader from "@tea/ui/panel-header/panel-header.svelte";
|
||||||
import Preloader from '@tea/ui/Preloader/Preloader.svelte';
|
import Preloader from "@tea/ui/Preloader/Preloader.svelte";
|
||||||
import { postsStore } from '$libs/stores';
|
import { postsStore } from "$libs/stores";
|
||||||
|
|
||||||
export let title = 'Workshops';
|
export let title = "Workshops";
|
||||||
export let ctaLabel = 'View all';
|
export let ctaLabel = "View all";
|
||||||
|
|
||||||
let courses: AirtablePost[] = [];
|
let courses: AirtablePost[] = [];
|
||||||
|
|
||||||
postsStore.subscribeByTag('course', (posts) => (courses = posts));
|
postsStore.subscribeByTag("course", (posts) => (courses = posts));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<PanelHeader {title} {ctaLabel} ctaLink="/" />
|
<PanelHeader {title} {ctaLabel} ctaLink="/" />
|
||||||
{#if courses.length}
|
{#if courses.length}
|
||||||
<Posts posts={courses} linkTarget="_blank" />
|
<Posts posts={courses} linkTarget="_blank" />
|
||||||
{:else}
|
{:else}
|
||||||
<section class="border-gray h-64 border bg-black p-4">
|
<section class="border-gray h-64 border bg-black p-4">
|
||||||
<Preloader />
|
<Preloader />
|
||||||
</section>
|
</section>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -1,32 +1,32 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import '$appcss';
|
import "$appcss";
|
||||||
import { t } from '$libs/translations';
|
import { t } from "$libs/translations";
|
||||||
import { postsStore } from '$libs/stores';
|
import { postsStore } from "$libs/stores";
|
||||||
import type { Course } from '$libs/types';
|
import type { Course } from "$libs/types";
|
||||||
|
|
||||||
import Gallery from '@tea/ui/gallery/gallery.svelte';
|
import Gallery from "@tea/ui/gallery/gallery.svelte";
|
||||||
|
|
||||||
let courses: Course[] = [];
|
let courses: Course[] = [];
|
||||||
|
|
||||||
postsStore.subscribeByTag('featured_course', (posts) => {
|
postsStore.subscribeByTag("featured_course", (posts) => {
|
||||||
courses = posts.map((post) => {
|
courses = posts.map((post) => {
|
||||||
return {
|
return {
|
||||||
title: post.title,
|
title: post.title,
|
||||||
sub_title: post.sub_title,
|
sub_title: post.sub_title,
|
||||||
banner_image_url: post.thumb_image_url,
|
banner_image_url: post.thumb_image_url,
|
||||||
link: post.link
|
link: post.link
|
||||||
} as Course;
|
} as Course;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Gallery
|
<Gallery
|
||||||
title={$t("documentation.featured-courses-title").toUpperCase()}
|
title={$t("documentation.featured-courses-title").toUpperCase()}
|
||||||
items={courses.map((course) => ({
|
items={courses.map((course) => ({
|
||||||
title: course.title,
|
title: course.title,
|
||||||
subTitle: course.sub_title,
|
subTitle: course.sub_title,
|
||||||
imageUrl: course.banner_image_url,
|
imageUrl: course.banner_image_url,
|
||||||
link: course.link
|
link: course.link
|
||||||
}))}
|
}))}
|
||||||
linkTarget="_blank"
|
linkTarget="_blank"
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,33 +1,33 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import '$appcss';
|
import "$appcss";
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from "svelte";
|
||||||
import type { Package } from '@tea/ui/types';
|
import type { Package } from "@tea/ui/types";
|
||||||
|
|
||||||
import Gallery from '@tea/ui/gallery/gallery.svelte';
|
import Gallery from "@tea/ui/gallery/gallery.svelte";
|
||||||
import {
|
import {
|
||||||
featuredPackages as featuredPackagesStore,
|
featuredPackages as featuredPackagesStore,
|
||||||
initializeFeaturedPackages
|
initializeFeaturedPackages
|
||||||
} from '$libs/stores';
|
} from "$libs/stores";
|
||||||
|
|
||||||
let featuredPackages: Package[] = [];
|
let featuredPackages: Package[] = [];
|
||||||
|
|
||||||
featuredPackagesStore.subscribe((v) => {
|
featuredPackagesStore.subscribe((v) => {
|
||||||
featuredPackages = v;
|
featuredPackages = v;
|
||||||
});
|
});
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
if (!featuredPackages.length) {
|
if (!featuredPackages.length) {
|
||||||
initializeFeaturedPackages();
|
initializeFeaturedPackages();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Gallery
|
<Gallery
|
||||||
title="FEATURED PACKAGES"
|
title="FEATURED PACKAGES"
|
||||||
items={featuredPackages.map((pkg) => ({
|
items={featuredPackages.map((pkg) => ({
|
||||||
title: pkg.full_name,
|
title: pkg.full_name,
|
||||||
subTitle: pkg.maintainer || '',
|
subTitle: pkg.maintainer || "",
|
||||||
imageUrl: pkg.thumb_image_url,
|
imageUrl: pkg.thumb_image_url,
|
||||||
link: `/packages/${pkg.slug}`
|
link: `/packages/${pkg.slug}`
|
||||||
}))}
|
}))}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,75 +1,75 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { t } from '$libs/translations';
|
import { t } from "$libs/translations";
|
||||||
import Button from '@tea/ui/button/button.svelte';
|
import Button from "@tea/ui/button/button.svelte";
|
||||||
import * as pub from '$env/static/public';
|
import * as pub from "$env/static/public";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<footer class="relative h-auto w-full bg-black">
|
<footer class="relative h-auto w-full bg-black">
|
||||||
<section class="p-4 px-16 py-16">
|
<section class="p-4 px-16 py-16">
|
||||||
<h3 class="text-primary mb-5 text-2xl">{$t("footer.quick-links-title").toUpperCase()}</h3>
|
<h3 class="text-primary mb-5 text-2xl">{$t("footer.quick-links-title").toUpperCase()}</h3>
|
||||||
<menu class="flex gap-4">
|
<menu class="flex gap-4">
|
||||||
<div class="border-gray flex-grow border border-l-0 border-r-0">
|
<div class="border-gray flex-grow border border-l-0 border-r-0">
|
||||||
<a href="/">
|
<a href="/">
|
||||||
<Button>
|
<Button>
|
||||||
<div class="text-primary flex justify-between hover:text-black">
|
<div class="text-primary flex justify-between hover:text-black">
|
||||||
<div class="uppercase">{$t("footer.about-tea-store").toUpperCase()}</div>
|
<div class="uppercase">{$t("footer.about-tea-store").toUpperCase()}</div>
|
||||||
<div>→</div>
|
<div>→</div>
|
||||||
</div>
|
</div>
|
||||||
</Button>
|
</Button>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="border-gray flex-grow border border-l-0 border-r-0">
|
<div class="border-gray flex-grow border border-l-0 border-r-0">
|
||||||
<a href="/">
|
<a href="/">
|
||||||
<Button>
|
<Button>
|
||||||
<div class="text-primary flex justify-between hover:text-black">
|
<div class="text-primary flex justify-between hover:text-black">
|
||||||
<div class="uppercase">{$t("footer.report-a-problem").toUpperCase()}</div>
|
<div class="uppercase">{$t("footer.report-a-problem").toUpperCase()}</div>
|
||||||
<div>→</div>
|
<div>→</div>
|
||||||
</div>
|
</div>
|
||||||
</Button>
|
</Button>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="border-gray flex-grow border border-l-0 border-r-0">
|
<div class="border-gray flex-grow border border-l-0 border-r-0">
|
||||||
<a href="https://tea.xyz" target="_blank" rel="noreferrer">
|
<a href="https://tea.xyz" target="_blank" rel="noreferrer">
|
||||||
<Button>
|
<Button>
|
||||||
<div class="text-primary flex justify-between hover:text-black">
|
<div class="text-primary flex justify-between hover:text-black">
|
||||||
<div class="uppercase">{$t("footer.visit-website").toUpperCase()}</div>
|
<div class="uppercase">{$t("footer.visit-website").toUpperCase()}</div>
|
||||||
<div>→</div>
|
<div>→</div>
|
||||||
</div>
|
</div>
|
||||||
</Button>
|
</Button>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</menu>
|
</menu>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="border-gray h-16 border border-r-0 p-4 px-16 flex justify-between">
|
<section class="border-gray flex h-16 justify-between border border-r-0 p-4 px-16">
|
||||||
<div class="text-gray flex gap-4 text-xs">
|
<div class="text-gray flex gap-4 text-xs">
|
||||||
<a
|
<a
|
||||||
href="https://tea.xyz/terms-of-use/"
|
href="https://tea.xyz/terms-of-use/"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
class="hover:text-white"
|
class="hover:text-white"
|
||||||
>
|
>
|
||||||
{$t("footer.terms-services").toUpperCase()}
|
{$t("footer.terms-services").toUpperCase()}
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
href="https://tea.xyz/privacy-policy/"
|
href="https://tea.xyz/privacy-policy/"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
class="hover:text-white"
|
class="hover:text-white"
|
||||||
>
|
>
|
||||||
{$t("footer.privacy-policy").toUpperCase()}
|
{$t("footer.privacy-policy").toUpperCase()}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
{#if pub.PUBLIC_VERSION}
|
{#if pub.PUBLIC_VERSION}
|
||||||
<div class="text-gray flex gap-4 text-xs">
|
<div class="text-gray flex gap-4 text-xs">
|
||||||
<span>v{pub.PUBLIC_VERSION}</span>
|
<span>v{pub.PUBLIC_VERSION}</span>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</section>
|
</section>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
h3 {
|
h3 {
|
||||||
color: #00ffd0;
|
color: #00ffd0;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,45 +1,45 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import '$appcss';
|
import "$appcss";
|
||||||
import ArticleCard from '@tea/ui/article-card/article-card.svelte';
|
import ArticleCard from "@tea/ui/article-card/article-card.svelte";
|
||||||
|
|
||||||
const doStuff = () => {
|
const doStuff = () => {
|
||||||
console.log('do stuff!');
|
console.log("do stuff!");
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<header class="border-gray text-primary border bg-black p-4">GETTING STARTED WITH TEA</header>
|
<header class="border-gray text-primary border bg-black p-4">GETTING STARTED WITH TEA</header>
|
||||||
<section class="grid grid-cols-3 bg-black">
|
<section class="grid grid-cols-3 bg-black">
|
||||||
<div class="border-gray border p-4">
|
<div class="border-gray border p-4">
|
||||||
<ArticleCard
|
<ArticleCard
|
||||||
content={{
|
content={{
|
||||||
title: 'installing tea',
|
title: "installing tea",
|
||||||
copy: "It's time to take your first sip! Click below to visit our tea-cli documentation page.",
|
copy: "It's time to take your first sip! Click below to visit our tea-cli documentation page.",
|
||||||
img_url: '/images/bored-ape.png',
|
img_url: "/images/bored-ape.png",
|
||||||
cta_label: 'Get Started',
|
cta_label: "Get Started",
|
||||||
link: '/cli'
|
link: "/cli"
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="border-gray border p-4">
|
<div class="border-gray border p-4">
|
||||||
<ArticleCard
|
<ArticleCard
|
||||||
content={{
|
content={{
|
||||||
title: 'authenticating',
|
title: "authenticating",
|
||||||
copy: 'Using tea without authenticating is like playing a video game without the DLC. Join us today!',
|
copy: "Using tea without authenticating is like playing a video game without the DLC. Join us today!",
|
||||||
img_url: '/images/bored-ape.png',
|
img_url: "/images/bored-ape.png",
|
||||||
cta_label: 'Get Started',
|
cta_label: "Get Started",
|
||||||
link: ''
|
link: ""
|
||||||
}}
|
}}
|
||||||
onClick={doStuff}
|
onClick={doStuff}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="border-gray border p-4">
|
<div class="border-gray border p-4">
|
||||||
<ArticleCard
|
<ArticleCard
|
||||||
content={{
|
content={{
|
||||||
title: 'give us a star',
|
title: "give us a star",
|
||||||
copy: 'Revolutions are built on the will of the people. Show your support for a more equitable internet.',
|
copy: "Revolutions are built on the will of the people. Show your support for a more equitable internet.",
|
||||||
img_url: '/images/bored-ape.png',
|
img_url: "/images/bored-ape.png",
|
||||||
cta_label: 'Get Started'
|
cta_label: "Get Started"
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
|
@ -1,22 +1,22 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import '$appcss';
|
import "$appcss";
|
||||||
import { t } from '$libs/translations';
|
import { t } from "$libs/translations";
|
||||||
import { postsStore } from '$libs/stores';
|
import { postsStore } from "$libs/stores";
|
||||||
import type { AirtablePost } from '@tea/ui/types';
|
import type { AirtablePost } from "@tea/ui/types";
|
||||||
import Posts from '@tea/ui/posts/posts.svelte';
|
import Posts from "@tea/ui/posts/posts.svelte";
|
||||||
import PanelHeader from '@tea/ui/panel-header/panel-header.svelte';
|
import PanelHeader from "@tea/ui/panel-header/panel-header.svelte";
|
||||||
import Preloader from '@tea/ui/Preloader/Preloader.svelte';
|
import Preloader from "@tea/ui/Preloader/Preloader.svelte";
|
||||||
|
|
||||||
let news: AirtablePost[] = [];
|
let news: AirtablePost[] = [];
|
||||||
|
|
||||||
postsStore.subscribeByTag('news', (posts) => (news = posts));
|
postsStore.subscribeByTag("news", (posts) => (news = posts));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<PanelHeader title="OPEN-SOURCE NEWS" ctaLabel="Read more articles" ctaLink="/" />
|
<PanelHeader title="OPEN-SOURCE NEWS" ctaLabel="Read more articles" ctaLink="/" />
|
||||||
{#if news.length}
|
{#if news.length}
|
||||||
<Posts posts={news} linkTarget="_blank" />
|
<Posts posts={news} linkTarget="_blank" />
|
||||||
{:else}
|
{:else}
|
||||||
<section class="border-gray h-64 border bg-black p-4">
|
<section class="border-gray h-64 border bg-black p-4">
|
||||||
<Preloader />
|
<Preloader />
|
||||||
</section>
|
</section>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -1,19 +1,21 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import '$appcss';
|
import "$appcss";
|
||||||
import { t } from "$libs/translations";
|
import { t } from "$libs/translations";
|
||||||
import { notificationStore } from '$libs/stores';
|
import { notificationStore } from "$libs/stores";
|
||||||
import Notification from "@tea/ui/notification/notification.svelte";
|
import Notification from "@tea/ui/notification/notification.svelte";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="w-full flex flex-col gap-1">
|
<div class="flex w-full flex-col gap-1">
|
||||||
{#each $notificationStore as notification}
|
{#each $notificationStore as notification}
|
||||||
<Notification
|
<Notification
|
||||||
notification={{
|
notification={{
|
||||||
...notification,
|
...notification,
|
||||||
// TODO this looks nasty but cleanup later.
|
// TODO this looks nasty but cleanup later.
|
||||||
message: notification.i18n_key ? $t(notification.i18n_key, notification.params) : notification.message
|
message: notification.i18n_key
|
||||||
|
? $t(notification.i18n_key, notification.params)
|
||||||
|
: notification.message
|
||||||
}}
|
}}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
notificationStore.remove(notification.id);
|
notificationStore.remove(notification.id);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,175 +1,174 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import "$appcss";
|
import "$appcss";
|
||||||
import "@tea/ui/icons/icons.css";
|
import "@tea/ui/icons/icons.css";
|
||||||
import { t } from "$libs/translations";
|
import { t } from "$libs/translations";
|
||||||
import Button from "@tea/ui/button/button.svelte";
|
import Button from "@tea/ui/button/button.svelte";
|
||||||
import ButtonIcon from "@tea/ui/button-icon/button-icon.svelte";
|
import ButtonIcon from "@tea/ui/button-icon/button-icon.svelte";
|
||||||
import ToolTip from "@tea/ui/tool-tip/tool-tip.svelte";
|
import ToolTip from "@tea/ui/tool-tip/tool-tip.svelte";
|
||||||
import semverCompare from "semver/functions/compare";
|
import semverCompare from "semver/functions/compare";
|
||||||
import ProgressCircle from "@tea/ui/progress-circle/progress-circle.svelte";
|
import ProgressCircle from "@tea/ui/progress-circle/progress-circle.svelte";
|
||||||
|
|
||||||
import type { GUIPackage } from "$libs/types";
|
import type { GUIPackage } from "$libs/types";
|
||||||
import { packagesStore } from "$libs/stores";
|
import { packagesStore } from "$libs/stores";
|
||||||
import { openPackageEntrypointInTerminal, shellOpenExternal } from "@native";
|
import { openPackageEntrypointInTerminal, shellOpenExternal } from "@native";
|
||||||
import { findAvailableVersions, findRecentInstalledVersion } from "$libs/packages/pkg-utils";
|
import { findAvailableVersions, findRecentInstalledVersion } from "$libs/packages/pkg-utils";
|
||||||
import { trimGithubSlug } from "$libs/github";
|
import { trimGithubSlug } from "$libs/github";
|
||||||
import PackageImage from "../package-card/bg-image.svelte";
|
import PackageImage from "../package-card/bg-image.svelte";
|
||||||
import PackageVersionSelector from "$components/package-install-button/package-version-selector.svelte";
|
import PackageVersionSelector from "$components/package-install-button/package-version-selector.svelte";
|
||||||
import { isPackageInstalled } from "$libs/native-mock";
|
import { isPackageInstalled } from "$libs/native-mock";
|
||||||
|
|
||||||
|
export let pkg: GUIPackage;
|
||||||
|
let installing = false;
|
||||||
|
let pruning = false;
|
||||||
|
|
||||||
export let pkg: GUIPackage;
|
const install = async (version: string) => {
|
||||||
let installing = false;
|
installing = true;
|
||||||
let pruning = false;
|
await packagesStore.installPkg(pkg, version);
|
||||||
|
installing = false;
|
||||||
|
};
|
||||||
|
|
||||||
const install = async (version: string) => {
|
const prune = async () => {
|
||||||
installing = true;
|
pruning = true;
|
||||||
await packagesStore.installPkg(pkg, version);
|
const versions = (pkg?.installed_versions || []).sort((a, b) => semverCompare(b, a));
|
||||||
installing = false;
|
for (const [i, v] of versions.entries()) {
|
||||||
};
|
if (i) {
|
||||||
|
// skip the latest version = 0
|
||||||
|
try {
|
||||||
|
await packagesStore.deletePkg(pkg, v);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pruning = false;
|
||||||
|
};
|
||||||
|
|
||||||
const prune = async () => {
|
let copied = false;
|
||||||
pruning = true;
|
const copyPackagePantryLink = async () => {
|
||||||
const versions = (pkg?.installed_versions || []).sort((a, b) => semverCompare(b, a));
|
const pantryLink = `https://tea.xyz/+${pkg.full_name}`.toLowerCase();
|
||||||
for (const [i, v] of versions.entries()) {
|
await navigator.clipboard.writeText(pantryLink);
|
||||||
if (i) {
|
copied = true;
|
||||||
// skip the latest version = 0
|
};
|
||||||
try {
|
|
||||||
await packagesStore.deletePkg(pkg, v);
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pruning = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
let copied = false;
|
|
||||||
const copyPackagePantryLink = async () => {
|
|
||||||
const pantryLink = `https://tea.xyz/+${pkg.full_name}`.toLowerCase();
|
|
||||||
await navigator.clipboard.writeText(pantryLink);
|
|
||||||
copied = true;
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<section class="mt-4 bg-black">
|
<section class="mt-4 bg-black">
|
||||||
<header class="flex">
|
<header class="flex">
|
||||||
<figure class="grow-1 relative w-1/3">
|
<figure class="grow-1 relative w-1/3">
|
||||||
<PackageImage class="min-h-[300px] w-full overflow-hidden" {pkg} layout="none" />
|
<PackageImage class="min-h-[300px] w-full overflow-hidden" {pkg} layout="none" />
|
||||||
{#if pkg.install_progress_percentage && pkg.install_progress_percentage < 100}
|
{#if pkg.install_progress_percentage && pkg.install_progress_percentage < 100}
|
||||||
<div class="absolute left-0 top-0 z-40 h-full w-full bg-black bg-opacity-50">
|
<div class="absolute left-0 top-0 z-40 h-full w-full bg-black bg-opacity-50">
|
||||||
<div class="absolute left-0 right-0 top-1/2 m-auto -mt-12 h-24 w-24">
|
<div class="absolute left-0 right-0 top-1/2 m-auto -mt-12 h-24 w-24">
|
||||||
<ProgressCircle value={pkg.install_progress_percentage} />
|
<ProgressCircle value={pkg.install_progress_percentage} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</figure>
|
</figure>
|
||||||
<article class="w-2/3 p-4 pt-8">
|
<article class="w-2/3 p-4 pt-8">
|
||||||
<div class="align-center flex items-center gap-2">
|
<div class="align-center flex items-center gap-2">
|
||||||
<h3 class="text-primary text-3xl">{pkg.full_name}</h3>
|
<h3 class="text-primary text-3xl">{pkg.full_name}</h3>
|
||||||
<ButtonIcon
|
<ButtonIcon
|
||||||
icon="pencil"
|
icon="pencil"
|
||||||
helpText="edit package"
|
helpText="edit package"
|
||||||
on:click={() =>
|
on:click={() =>
|
||||||
shellOpenExternal(
|
shellOpenExternal(
|
||||||
`https://github.com/teaxyz/pantry/blob/main/projects/${pkg.full_name}/package.yml`
|
`https://github.com/teaxyz/pantry/blob/main/projects/${pkg.full_name}/package.yml`
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<ButtonIcon icon="link" helpText="share package" on:click={copyPackagePantryLink} />
|
<ButtonIcon icon="link" helpText="share package" on:click={copyPackagePantryLink} />
|
||||||
{#if copied}
|
{#if copied}
|
||||||
<p class="text-green">copied!</p>
|
<p class="text-green">copied!</p>
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{#if pkg.homepage}
|
|
||||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
|
||||||
<span
|
|
||||||
class="hover:text-primary cursor-pointer"
|
|
||||||
on:click={() => shellOpenExternal(pkg.homepage)}>{pkg.homepage}</span
|
|
||||||
>
|
|
||||||
{/if}
|
|
||||||
<p class="mt-4 text-sm">{pkg.desc}</p>
|
|
||||||
<menu class="mt-4 flex h-10 gap-4 text-xs">
|
|
||||||
<div class="min-w-[150px]">
|
|
||||||
<PackageVersionSelector
|
|
||||||
buttonSize="large"
|
|
||||||
{pkg}
|
|
||||||
availableVersions={findAvailableVersions(pkg)}
|
|
||||||
onClick={install}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{#if (pkg?.installed_versions?.length || 0) > 0}
|
|
||||||
<ToolTip class="ml-[-80px]">
|
|
||||||
<Button
|
|
||||||
slot="target"
|
|
||||||
class="h-10"
|
|
||||||
type="plain"
|
|
||||||
color="blue"
|
|
||||||
onClick={async () => {
|
|
||||||
packagesStore.uninstallPkg(pkg);
|
|
||||||
}}
|
|
||||||
loading={pruning}
|
|
||||||
>
|
|
||||||
<div class="version-item flex w-full items-center justify-center gap-x-1 text-xs">
|
|
||||||
<div class="icon-trash" />
|
|
||||||
<div>{$t("package.cta-UNINSTALL")}</div>
|
|
||||||
</div>
|
|
||||||
</Button>
|
|
||||||
<div slot="tooltip-content" class="flex flex-col items-center">
|
|
||||||
<div>Removes all the versions of the package</div>
|
|
||||||
</div>
|
|
||||||
</ToolTip>
|
|
||||||
{/if}
|
|
||||||
{#if (pkg?.installed_versions?.length || 0) > 1}
|
|
||||||
<ToolTip>
|
|
||||||
<Button
|
|
||||||
slot="target"
|
|
||||||
class="h-10"
|
|
||||||
type="plain"
|
|
||||||
color="blue"
|
|
||||||
onClick={prune}
|
|
||||||
loading={pruning}
|
|
||||||
>
|
|
||||||
<div class="version-item flex w-full items-center justify-center gap-x-1 text-xs">
|
|
||||||
<div class="icon-scissors" />
|
|
||||||
<div>{$t("package.cta-PRUNE")}</div>
|
|
||||||
</div>
|
|
||||||
</Button>
|
|
||||||
<div slot="tooltip-content" class="flex flex-col items-center">
|
|
||||||
<div>Removes {pkg.installed_versions?.length ?? 0 - 1} old versions</div>
|
|
||||||
<div>Keeps latest (v{findRecentInstalledVersion(pkg)})</div>
|
|
||||||
</div>
|
|
||||||
</ToolTip>
|
|
||||||
{/if}
|
|
||||||
{#if pkg.github}
|
|
||||||
<button
|
|
||||||
class="border-gray group flex h-[40px] w-[40px] items-center justify-center rounded-sm border hover:bg-[#e1e1e1] shrink-0"
|
|
||||||
on:click={() => {
|
|
||||||
if (pkg.github) {
|
|
||||||
const slug = trimGithubSlug(pkg.github);
|
|
||||||
shellOpenExternal(`https://github.com/${slug}`);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div class="icon-github text-gray flex text-xl group-hover:text-black" />
|
|
||||||
</button>
|
|
||||||
{/if}
|
|
||||||
{#if pkg.installed_versions?.length}
|
|
||||||
<Button
|
|
||||||
class="h-10"
|
|
||||||
type="plain"
|
|
||||||
color="black"
|
|
||||||
onClick={() => {
|
|
||||||
openPackageEntrypointInTerminal(pkg.full_name);
|
|
||||||
}}>
|
|
||||||
{#if pkg.full_name == "github.com/AUTOMATIC1111/stable-diffusion-webui"}
|
|
||||||
OPEN
|
|
||||||
{:else}
|
|
||||||
OPEN IN TERMINAL
|
|
||||||
{/if}
|
|
||||||
</Button
|
|
||||||
>
|
|
||||||
{/if}
|
{/if}
|
||||||
</menu>
|
</div>
|
||||||
</article>
|
{#if pkg.homepage}
|
||||||
</header>
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
|
<span
|
||||||
|
class="hover:text-primary cursor-pointer"
|
||||||
|
on:click={() => shellOpenExternal(pkg.homepage)}>{pkg.homepage}</span
|
||||||
|
>
|
||||||
|
{/if}
|
||||||
|
<p class="mt-4 text-sm">{pkg.desc}</p>
|
||||||
|
<menu class="mt-4 flex h-10 gap-4 text-xs">
|
||||||
|
<div class="min-w-[150px]">
|
||||||
|
<PackageVersionSelector
|
||||||
|
buttonSize="large"
|
||||||
|
{pkg}
|
||||||
|
availableVersions={findAvailableVersions(pkg)}
|
||||||
|
onClick={install}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{#if (pkg?.installed_versions?.length || 0) > 0}
|
||||||
|
<ToolTip class="ml-[-80px]">
|
||||||
|
<Button
|
||||||
|
slot="target"
|
||||||
|
class="h-10"
|
||||||
|
type="plain"
|
||||||
|
color="blue"
|
||||||
|
onClick={async () => {
|
||||||
|
packagesStore.uninstallPkg(pkg);
|
||||||
|
}}
|
||||||
|
loading={pruning}
|
||||||
|
>
|
||||||
|
<div class="version-item flex w-full items-center justify-center gap-x-1 text-xs">
|
||||||
|
<div class="icon-trash" />
|
||||||
|
<div>{$t("package.cta-UNINSTALL")}</div>
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
<div slot="tooltip-content" class="flex flex-col items-center">
|
||||||
|
<div>Removes all the versions of the package</div>
|
||||||
|
</div>
|
||||||
|
</ToolTip>
|
||||||
|
{/if}
|
||||||
|
{#if (pkg?.installed_versions?.length || 0) > 1}
|
||||||
|
<ToolTip>
|
||||||
|
<Button
|
||||||
|
slot="target"
|
||||||
|
class="h-10"
|
||||||
|
type="plain"
|
||||||
|
color="blue"
|
||||||
|
onClick={prune}
|
||||||
|
loading={pruning}
|
||||||
|
>
|
||||||
|
<div class="version-item flex w-full items-center justify-center gap-x-1 text-xs">
|
||||||
|
<div class="icon-scissors" />
|
||||||
|
<div>{$t("package.cta-PRUNE")}</div>
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
<div slot="tooltip-content" class="flex flex-col items-center">
|
||||||
|
<div>Removes {pkg.installed_versions?.length ?? 0 - 1} old versions</div>
|
||||||
|
<div>Keeps latest (v{findRecentInstalledVersion(pkg)})</div>
|
||||||
|
</div>
|
||||||
|
</ToolTip>
|
||||||
|
{/if}
|
||||||
|
{#if pkg.github}
|
||||||
|
<button
|
||||||
|
class="border-gray group flex h-[40px] w-[40px] shrink-0 items-center justify-center rounded-sm border hover:bg-[#e1e1e1]"
|
||||||
|
on:click={() => {
|
||||||
|
if (pkg.github) {
|
||||||
|
const slug = trimGithubSlug(pkg.github);
|
||||||
|
shellOpenExternal(`https://github.com/${slug}`);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class="icon-github text-gray flex text-xl group-hover:text-black" />
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
{#if pkg.installed_versions?.length}
|
||||||
|
<Button
|
||||||
|
class="h-10"
|
||||||
|
type="plain"
|
||||||
|
color="black"
|
||||||
|
onClick={() => {
|
||||||
|
openPackageEntrypointInTerminal(pkg.full_name);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{#if pkg.full_name == "github.com/AUTOMATIC1111/stable-diffusion-webui"}
|
||||||
|
OPEN
|
||||||
|
{:else}
|
||||||
|
OPEN IN TERMINAL
|
||||||
|
{/if}
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
|
</menu>
|
||||||
|
</article>
|
||||||
|
</header>
|
||||||
</section>
|
</section>
|
||||||
|
|
|
@ -1,172 +1,172 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { GUIPackage } from "$libs/types";
|
import type { GUIPackage } from "$libs/types";
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import { packagesStore } from "$libs/stores";
|
import { packagesStore } from "$libs/stores";
|
||||||
|
|
||||||
let clazz = "";
|
let clazz = "";
|
||||||
export { clazz as class };
|
export { clazz as class };
|
||||||
|
|
||||||
export let layout: "bottom" | "right" | "left" | "none" = "bottom";
|
export let layout: "bottom" | "right" | "left" | "none" = "bottom";
|
||||||
|
|
||||||
export let pkg: GUIPackage;
|
export let pkg: GUIPackage;
|
||||||
|
|
||||||
const defaultImgUrl = "/images/default-thumb.jpg";
|
const defaultImgUrl = "/images/default-thumb.jpg";
|
||||||
$: loadedImg = "";
|
$: loadedImg = "";
|
||||||
let loaded = false;
|
let loaded = false;
|
||||||
let lastProcessedPkg: GUIPackage | null = null;
|
let lastProcessedPkg: GUIPackage | null = null;
|
||||||
|
|
||||||
const loadImage = async (url: string): Promise<string> => {
|
const loadImage = async (url: string): Promise<string> => {
|
||||||
const image = new Image();
|
const image = new Image();
|
||||||
image.src = url;
|
image.src = url;
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
image.onload = () => {
|
image.onload = () => {
|
||||||
loadedImg = url;
|
loadedImg = url;
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
loaded = true;
|
loaded = true;
|
||||||
}, 300);
|
}, 300);
|
||||||
resolve(url);
|
resolve(url);
|
||||||
};
|
};
|
||||||
image.onerror = () => {
|
image.onerror = () => {
|
||||||
reject(new Error(`file/url does not exist ${url}`));
|
reject(new Error(`file/url does not exist ${url}`));
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const recachePkg = async () => {
|
const recachePkg = async () => {
|
||||||
const url = await packagesStore.cachePkgImage(pkg);
|
const url = await packagesStore.cachePkgImage(pkg);
|
||||||
loadImage(url);
|
loadImage(url);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getCache = async () => {
|
const getCache = async () => {
|
||||||
if (pkg.cached_image_url) {
|
if (pkg.cached_image_url) {
|
||||||
loadImage(pkg.cached_image_url).catch(() => {
|
loadImage(pkg.cached_image_url).catch(() => {
|
||||||
if (pkg.thumb_image_url) {
|
if (pkg.thumb_image_url) {
|
||||||
loadImage(pkg.thumb_image_url);
|
loadImage(pkg.thumb_image_url);
|
||||||
recachePkg();
|
recachePkg();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else if (pkg.thumb_image_url) {
|
} else if (pkg.thumb_image_url) {
|
||||||
recachePkg();
|
recachePkg();
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
$: {
|
$: {
|
||||||
if (pkg && pkg?.slug !== lastProcessedPkg?.slug) {
|
if (pkg && pkg?.slug !== lastProcessedPkg?.slug) {
|
||||||
loaded = false;
|
loaded = false;
|
||||||
loadedImg = "";
|
loadedImg = "";
|
||||||
lastProcessedPkg = pkg;
|
lastProcessedPkg = pkg;
|
||||||
getCache();
|
getCache();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<section class="bg-black {clazz} {layout}">
|
<section class="bg-black {clazz} {layout}">
|
||||||
<i class="logo icon-tea-logo-iconasset-1 text-gray animate-pulse text-3xl {layout}" />
|
<i class="logo icon-tea-logo-iconasset-1 text-gray animate-pulse text-3xl {layout}" />
|
||||||
<div
|
<div
|
||||||
class="bg-center opacity-0 transition-all duration-500"
|
class="bg-center opacity-0 transition-all duration-500"
|
||||||
class:opacity-100={loaded}
|
class:opacity-100={loaded}
|
||||||
style="background-image: url({loadedImg})"
|
style="background-image: url({loadedImg})"
|
||||||
>
|
>
|
||||||
<!-- dup image: save processing power instead of computing the blur across all the HTML layers -->
|
<!-- dup image: save processing power instead of computing the blur across all the HTML layers -->
|
||||||
{#if layout !== "none"}
|
{#if layout !== "none"}
|
||||||
<aside
|
<aside
|
||||||
class="blur-sm {layout} opacity-0 transition-all duration-500"
|
class="blur-sm {layout} opacity-0 transition-all duration-500"
|
||||||
class:opacity-100={loaded}
|
class:opacity-100={loaded}
|
||||||
>
|
>
|
||||||
<figure class="bg-center" style="background-image: url({loadedImg})" />
|
<figure class="bg-center" style="background-image: url({loadedImg})" />
|
||||||
</aside>
|
</aside>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
section {
|
section {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo {
|
.logo {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 30px;
|
width: 30px;
|
||||||
height: 30px;
|
height: 30px;
|
||||||
margin-left: -15px;
|
margin-left: -15px;
|
||||||
}
|
}
|
||||||
.logo.none {
|
.logo.none {
|
||||||
left: 50%;
|
left: 50%;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
}
|
}
|
||||||
.logo.bottom {
|
.logo.bottom {
|
||||||
left: 50%;
|
left: 50%;
|
||||||
top: 30%;
|
top: 30%;
|
||||||
}
|
}
|
||||||
.logo.right {
|
.logo.right {
|
||||||
left: 22%;
|
left: 22%;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
margin-top: -15px;
|
margin-top: -15px;
|
||||||
}
|
}
|
||||||
.logo.left {
|
.logo.left {
|
||||||
left: 70%;
|
left: 70%;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
margin-top: -15px;
|
margin-top: -15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
div {
|
div {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0px;
|
left: 0px;
|
||||||
bottom: 0px;
|
bottom: 0px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
}
|
}
|
||||||
aside {
|
aside {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 0px;
|
bottom: 0px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
aside.bottom {
|
aside.bottom {
|
||||||
left: 0px;
|
left: 0px;
|
||||||
height: 50%;
|
height: 50%;
|
||||||
}
|
}
|
||||||
|
|
||||||
aside.left {
|
aside.left {
|
||||||
left: 0px;
|
left: 0px;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 60%;
|
width: 60%;
|
||||||
}
|
}
|
||||||
|
|
||||||
aside.right {
|
aside.right {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
right: 0px;
|
right: 0px;
|
||||||
width: 60%;
|
width: 60%;
|
||||||
}
|
}
|
||||||
|
|
||||||
figure {
|
figure {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 0px;
|
bottom: 0px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 338px;
|
height: 338px;
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
}
|
}
|
||||||
|
|
||||||
aside.bottom figure {
|
aside.bottom figure {
|
||||||
left: 0px;
|
left: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
aside.right figure {
|
aside.right figure {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
/* the overlay is 60% of the image, so we need to oversize the background image back to 100% */
|
/* the overlay is 60% of the image, so we need to oversize the background image back to 100% */
|
||||||
width: 166.6666666%;
|
width: 166.6666666%;
|
||||||
right: 0px;
|
right: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
aside.left figure {
|
aside.left figure {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
/* the overlay is 60% of the image, so we need to oversize the background image back to 100% */
|
/* the overlay is 60% of the image, so we need to oversize the background image back to 100% */
|
||||||
width: 166.66666666%;
|
width: 166.66666666%;
|
||||||
left: 0px;
|
left: 0px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,219 +1,219 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import "../../app.css";
|
import "../../app.css";
|
||||||
import ProgressCircle from "@tea/ui/progress-circle/progress-circle.svelte";
|
import ProgressCircle from "@tea/ui/progress-circle/progress-circle.svelte";
|
||||||
import { PackageStates, type GUIPackage } from "$libs/types";
|
import { PackageStates, type GUIPackage } from "$libs/types";
|
||||||
import { findRecentInstalledVersion } from "$libs/packages/pkg-utils";
|
import { findRecentInstalledVersion } from "$libs/packages/pkg-utils";
|
||||||
import BgImage from "./bg-image.svelte";
|
import BgImage from "./bg-image.svelte";
|
||||||
import PackageInstallButton from "$components/package-install-button/package-install-button.svelte";
|
import PackageInstallButton from "$components/package-install-button/package-install-button.svelte";
|
||||||
import PackageInstalledBadge from "$components/package-install-button/package-installed-badge.svelte";
|
import PackageInstalledBadge from "$components/package-install-button/package-installed-badge.svelte";
|
||||||
|
|
||||||
export let pkg: GUIPackage;
|
export let pkg: GUIPackage;
|
||||||
export let link: string;
|
export let link: string;
|
||||||
export let progessLoading = 0;
|
export let progessLoading = 0;
|
||||||
|
|
||||||
export let layout: "bottom" | "right" | "left" = "bottom";
|
export let layout: "bottom" | "right" | "left" = "bottom";
|
||||||
|
|
||||||
export let onClickCTA = async () => {
|
export let onClickCTA = async () => {
|
||||||
console.log("do nothing");
|
console.log("do nothing");
|
||||||
};
|
};
|
||||||
|
|
||||||
const fixPackageName = (title: string) => {
|
const fixPackageName = (title: string) => {
|
||||||
return title.replace("-", "\u2011");
|
return title.replace("-", "\u2011");
|
||||||
};
|
};
|
||||||
|
|
||||||
// Using this instead of css :active because there is a button inside of a button
|
// Using this instead of css :active because there is a button inside of a button
|
||||||
let isActive = false;
|
let isActive = false;
|
||||||
const activate = () => (isActive = true);
|
const activate = () => (isActive = true);
|
||||||
const deactivate = () => (isActive = false);
|
const deactivate = () => (isActive = false);
|
||||||
|
|
||||||
const preventPropagation = (evt: MouseEvent) => evt.stopPropagation();
|
const preventPropagation = (evt: MouseEvent) => evt.stopPropagation();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<section class="package-card border-gray relative h-auto border {layout}" class:active={isActive}>
|
<section class="package-card border-gray relative h-auto border {layout}" class:active={isActive}>
|
||||||
<BgImage class="absolute top-0 left-0 h-full w-full" {layout} {pkg} />
|
<BgImage class="absolute top-0 left-0 h-full w-full" {layout} {pkg} />
|
||||||
|
|
||||||
<a href={link} on:mousedown={activate} on:mouseup={deactivate} on:mouseleave={deactivate}>
|
<a href={link} on:mousedown={activate} on:mouseup={deactivate} on:mouseleave={deactivate}>
|
||||||
<div class="package-card-content absolute h-full w-full flex-col justify-between">
|
<div class="package-card-content absolute h-full w-full flex-col justify-between">
|
||||||
<div class="hint-container">
|
<div class="hint-container">
|
||||||
<div class="hint">
|
<div class="hint">
|
||||||
<div class="line-clamp-1 text-xs">view more details</div>
|
<div class="line-clamp-1 text-xs">view more details</div>
|
||||||
<div class="hint-icon"><i class="icon-upward-arrow" /></div>
|
<div class="hint-icon"><i class="icon-upward-arrow" /></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="content-container absolute bottom-0 w-full {layout}">
|
<div class="content-container absolute bottom-0 w-full {layout}">
|
||||||
<article class="card-thumb-label relative">
|
<article class="card-thumb-label relative">
|
||||||
{#if layout === "bottom"}
|
{#if layout === "bottom"}
|
||||||
<h3 class="text-bold font-mona line-clamp-1 text-2xl font-bold text-white">
|
<h3 class="text-bold font-mona line-clamp-1 text-2xl font-bold text-white">
|
||||||
{fixPackageName(pkg.name)}
|
{fixPackageName(pkg.name)}
|
||||||
</h3>
|
</h3>
|
||||||
<p class="line-clamp-2 h-[32px] text-xs font-thin lowercase">{pkg.desc ?? ""}</p>
|
<p class="line-clamp-2 h-[32px] text-xs font-thin lowercase">{pkg.desc ?? ""}</p>
|
||||||
{:else}
|
{:else}
|
||||||
<h3 class="text-bold font-mona line-clamp-1 mb-4 text-3xl font-bold text-white">
|
<h3 class="text-bold font-mona line-clamp-1 mb-4 text-3xl font-bold text-white">
|
||||||
{fixPackageName(pkg.name)}
|
{fixPackageName(pkg.name)}
|
||||||
</h3>
|
</h3>
|
||||||
<p class="line-clamp-10 h-[160px] text-xs font-thin lowercase">{pkg.desc ?? ""}</p>
|
<p class="line-clamp-10 h-[160px] text-xs font-thin lowercase">{pkg.desc ?? ""}</p>
|
||||||
{/if}
|
{/if}
|
||||||
</article>
|
</article>
|
||||||
<div class="relative mt-3.5 w-full">
|
<div class="relative mt-3.5 w-full">
|
||||||
<div class="install-button {layout}" on:mousedown={preventPropagation}>
|
<div class="install-button {layout}" on:mousedown={preventPropagation}>
|
||||||
{#if pkg.state === PackageStates.INSTALLED}
|
{#if pkg.state === PackageStates.INSTALLED}
|
||||||
<PackageInstalledBadge version={pkg.version} />
|
<PackageInstalledBadge version={pkg.version} />
|
||||||
{:else}
|
{:else}
|
||||||
<PackageInstallButton
|
<PackageInstallButton
|
||||||
{pkg}
|
{pkg}
|
||||||
onClick={(evt) => {
|
onClick={(evt) => {
|
||||||
// prevent default to prevent the link that this button is inside of from being followed
|
// prevent default to prevent the link that this button is inside of from being followed
|
||||||
evt?.preventDefault();
|
evt?.preventDefault();
|
||||||
onClickCTA();
|
onClickCTA();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="relative mt-1.5 h-[10px] leading-[10px]">
|
<div class="relative mt-1.5 h-[10px] leading-[10px]">
|
||||||
{#if pkg.state === "NEEDS_UPDATE"}
|
{#if pkg.state === "NEEDS_UPDATE"}
|
||||||
<span class="text-[10px]">
|
<span class="text-[10px]">
|
||||||
<span class="opacity-70">you have</span>
|
<span class="opacity-70">you have</span>
|
||||||
v{findRecentInstalledVersion(pkg)}
|
v{findRecentInstalledVersion(pkg)}
|
||||||
</span>
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
{#if progessLoading > 0 && progessLoading < 100}
|
{#if progessLoading > 0 && progessLoading < 100}
|
||||||
<div class="absolute left-0 top-0 z-40 h-full w-full bg-black bg-opacity-50">
|
<div class="absolute left-0 top-0 z-40 h-full w-full bg-black bg-opacity-50">
|
||||||
<div class="absolute left-0 right-0 top-1/2 m-auto -mt-12 h-24 w-24">
|
<div class="absolute left-0 right-0 top-1/2 m-auto -mt-12 h-24 w-24">
|
||||||
<ProgressCircle value={progessLoading} />
|
<ProgressCircle value={progessLoading} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
section {
|
section {
|
||||||
transition: all 0.3s;
|
transition: all 0.3s;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 340px;
|
height: 340px;
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
section.active::before {
|
section.active::before {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background-color: rgba(26, 26, 26, 0.7);
|
background-color: rgba(26, 26, 26, 0.7);
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
content: "";
|
content: "";
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
section.package-card:active {
|
section.package-card:active {
|
||||||
border-color: #8000ff;
|
border-color: #8000ff;
|
||||||
box-shadow: 0px 0px 0px 2px rgba(128, 0, 255, 0.5);
|
box-shadow: 0px 0px 0px 2px rgba(128, 0, 255, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
.content-container {
|
.content-container {
|
||||||
height: 50%;
|
height: 50%;
|
||||||
background: linear-gradient(180deg, rgba(26, 26, 26, 0.3) 0%, rgba(26, 26, 26, 0.75) 72.92%);
|
background: linear-gradient(180deg, rgba(26, 26, 26, 0.3) 0%, rgba(26, 26, 26, 0.75) 72.92%);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding: 28px 14px;
|
padding: 28px 14px;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.content-container.bottom {
|
.content-container.bottom {
|
||||||
left: 0px;
|
left: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.content-container.left {
|
.content-container.left {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 60%;
|
width: 60%;
|
||||||
left: 0px;
|
left: 0px;
|
||||||
padding: 28px 28px;
|
padding: 28px 28px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.content-container.right {
|
.content-container.right {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 60%;
|
width: 60%;
|
||||||
right: 0px;
|
right: 0px;
|
||||||
padding: 28px 28px;
|
padding: 28px 28px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hint-container {
|
.hint-container {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0px;
|
top: 0px;
|
||||||
right: 0px;
|
right: 0px;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hint {
|
.hint {
|
||||||
min-width: 240px;
|
min-width: 240px;
|
||||||
padding-left: 30%;
|
padding-left: 30%;
|
||||||
height: 24px;
|
height: 24px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
column-gap: 0.5rem;
|
column-gap: 0.5rem;
|
||||||
background: linear-gradient(270deg, #e1e1e1 66.29%, rgba(225, 225, 225, 0) 100%);
|
background: linear-gradient(270deg, #e1e1e1 66.29%, rgba(225, 225, 225, 0) 100%);
|
||||||
color: #1a1a1a;
|
color: #1a1a1a;
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hint-icon {
|
.hint-icon {
|
||||||
background: #8000ff;
|
background: #8000ff;
|
||||||
height: 24px;
|
height: 24px;
|
||||||
width: 24px;
|
width: 24px;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
color: #e1e1e1;
|
color: #e1e1e1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.package-card.right {
|
.package-card.right {
|
||||||
min-width: 550px;
|
min-width: 550px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.package-card.left {
|
.package-card.left {
|
||||||
min-width: 550px;
|
min-width: 550px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.package-card:hover .hint {
|
.package-card:hover .hint {
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-thumb-label {
|
.card-thumb-label {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-thumb-label p {
|
.card-thumb-label p {
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.install-button {
|
.install-button {
|
||||||
width: 160px;
|
width: 160px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.install-button.bottom {
|
.install-button.bottom {
|
||||||
min-width: 100%;
|
min-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (min-width: 650px) {
|
@media screen and (min-width: 650px) {
|
||||||
.install-button.bottom {
|
.install-button.bottom {
|
||||||
min-width: 60%;
|
min-width: 60%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (min-width: 1000px) {
|
@media screen and (min-width: 1000px) {
|
||||||
.install-button.bottom {
|
.install-button.bottom {
|
||||||
min-width: 50%;
|
min-width: 50%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { Package } from '@tea/ui/types';
|
import type { Package } from "@tea/ui/types";
|
||||||
export let pkg: Package;
|
export let pkg: Package;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<section class="h-64 w-full">
|
<section class="h-64 w-full">
|
||||||
<h1>{pkg.full_name}</h1>
|
<h1>{pkg.full_name}</h1>
|
||||||
</section>
|
</section>
|
||||||
|
|
|
@ -1,105 +1,105 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { PackageStates, type GUIPackage } from "$libs/types";
|
import { PackageStates, type GUIPackage } from "$libs/types";
|
||||||
import Button from "@tea/ui/button/button.svelte";
|
import Button from "@tea/ui/button/button.svelte";
|
||||||
import { t } from "$libs/translations";
|
import { t } from "$libs/translations";
|
||||||
|
|
||||||
export let buttonSize: "small" | "large" = "small";
|
export let buttonSize: "small" | "large" = "small";
|
||||||
|
|
||||||
export let pkg: GUIPackage;
|
export let pkg: GUIPackage;
|
||||||
export let onClick = (evt?: MouseEvent) => {
|
export let onClick = (evt?: MouseEvent) => {
|
||||||
console.log("do nothing");
|
console.log("do nothing");
|
||||||
};
|
};
|
||||||
|
|
||||||
const getColor = (state: PackageStates): "primary" | "secondary" | "black" => {
|
const getColor = (state: PackageStates): "primary" | "secondary" | "black" => {
|
||||||
if (state === PackageStates.INSTALLED) {
|
if (state === PackageStates.INSTALLED) {
|
||||||
return "black";
|
return "black";
|
||||||
}
|
}
|
||||||
if (state === PackageStates.AVAILABLE || state === PackageStates.INSTALLING) {
|
if (state === PackageStates.AVAILABLE || state === PackageStates.INSTALLING) {
|
||||||
return "secondary";
|
return "secondary";
|
||||||
}
|
}
|
||||||
return "primary";
|
return "primary";
|
||||||
};
|
};
|
||||||
|
|
||||||
const isActive = (state: PackageStates): boolean => {
|
const isActive = (state: PackageStates): boolean => {
|
||||||
return state === PackageStates.INSTALLING || state === PackageStates.UPDATING;
|
return state === PackageStates.INSTALLING || state === PackageStates.UPDATING;
|
||||||
};
|
};
|
||||||
|
|
||||||
const badgeClass: Record<PackageStates, string> = {
|
const badgeClass: Record<PackageStates, string> = {
|
||||||
[PackageStates.AVAILABLE]: "install-badge",
|
[PackageStates.AVAILABLE]: "install-badge",
|
||||||
[PackageStates.INSTALLING]: "install-badge",
|
[PackageStates.INSTALLING]: "install-badge",
|
||||||
[PackageStates.NEEDS_UPDATE]: "update-badge",
|
[PackageStates.NEEDS_UPDATE]: "update-badge",
|
||||||
[PackageStates.UPDATING]: "update-badge",
|
[PackageStates.UPDATING]: "update-badge",
|
||||||
[PackageStates.INSTALLED]: "installed-badge",
|
[PackageStates.INSTALLED]: "installed-badge"
|
||||||
};
|
};
|
||||||
|
|
||||||
const hasVersionSelectorDropdown = !!$$slots.selector;
|
const hasVersionSelectorDropdown = !!$$slots.selector;
|
||||||
|
|
||||||
$: ctaLabel = $t(`package.cta-${pkg.state}`);
|
$: ctaLabel = $t(`package.cta-${pkg.state}`);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
class="w-full border p-0 text-xs text-white {buttonSize === 'small' ? 'h-8' : 'h-10'}"
|
class="w-full border p-0 text-xs text-white {buttonSize === 'small' ? 'h-8' : 'h-10'}"
|
||||||
type="plain"
|
type="plain"
|
||||||
color={getColor(pkg.state)}
|
color={getColor(pkg.state)}
|
||||||
active={isActive(pkg.state)}
|
active={isActive(pkg.state)}
|
||||||
{onClick}
|
{onClick}
|
||||||
>
|
>
|
||||||
<div class="version-button h-full">
|
<div class="version-button h-full">
|
||||||
<div class="flex h-full flex-col justify-center p-2">
|
<div class="flex h-full flex-col justify-center p-2">
|
||||||
{#if hasVersionSelectorDropdown}
|
{#if hasVersionSelectorDropdown}
|
||||||
<div class="flex items-center justify-between gap-x-2">
|
<div class="flex items-center justify-between gap-x-2">
|
||||||
<div class="flex items-center gap-x-2">
|
<div class="flex items-center gap-x-2">
|
||||||
<div>{ctaLabel}</div>
|
<div>{ctaLabel}</div>
|
||||||
<div class="version-label {badgeClass[pkg.state]}">{pkg.version}</div>
|
<div class="version-label {badgeClass[pkg.state]}">{pkg.version}</div>
|
||||||
</div>
|
</div>
|
||||||
<i class="icon-downward-arrow flex" />
|
<i class="icon-downward-arrow flex" />
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="flex items-center justify-center gap-x-2">
|
<div class="flex items-center justify-center gap-x-2">
|
||||||
<div>{ctaLabel}</div>
|
<div>{ctaLabel}</div>
|
||||||
<div class="version-label {badgeClass[pkg.state]}">{pkg.version}</div>
|
<div class="version-label {badgeClass[pkg.state]}">{pkg.version}</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<!-- This slot holds the drop down menu and it inside of the button so that the
|
<!-- This slot holds the drop down menu and it inside of the button so that the
|
||||||
hover effect remain on the button while the user is hovering the dropdown items-->
|
hover effect remain on the button while the user is hovering the dropdown items-->
|
||||||
<slot name="selector" />
|
<slot name="selector" />
|
||||||
</div>
|
</div>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.version-label {
|
.version-label {
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
line-height: 12px;
|
line-height: 12px;
|
||||||
padding: 0 4px;
|
padding: 0 4px;
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.install-badge {
|
.install-badge {
|
||||||
background-color: #dcb8ff;
|
background-color: #dcb8ff;
|
||||||
color: #8000ff;
|
color: #8000ff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.update-badge {
|
.update-badge {
|
||||||
background-color: #04957a;
|
background-color: #04957a;
|
||||||
color: #00ffd0;
|
color: #00ffd0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.installed-badge {
|
.installed-badge {
|
||||||
background-color: white;
|
background-color: white;
|
||||||
color: #1a1a1a;
|
color: #1a1a1a;
|
||||||
}
|
}
|
||||||
|
|
||||||
.version-button:hover .install-badge {
|
.version-button:hover .install-badge {
|
||||||
background-color: white;
|
background-color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.version-button:hover .update-badge {
|
.version-button:hover .update-badge {
|
||||||
background-color: #1a1a1a;
|
background-color: #1a1a1a;
|
||||||
}
|
}
|
||||||
|
|
||||||
.version-button:hover .installed-badge {
|
.version-button:hover .installed-badge {
|
||||||
background-color: #1a1a1a;
|
background-color: #1a1a1a;
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,32 +1,32 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
export let version = "1.0.0";
|
export let version = "1.0.0";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="container relative h-full">
|
<div class="container relative h-full">
|
||||||
<div class="content flex items-center justify-center gap-2 p-2">
|
<div class="content flex items-center justify-center gap-2 p-2">
|
||||||
<i class="icon-check-circle-o flex text-sm text-[#00ffd0]" />
|
<i class="icon-check-circle-o flex text-sm text-[#00ffd0]" />
|
||||||
<div class="text-xs">INSTALLED</div>
|
<div class="text-xs">INSTALLED</div>
|
||||||
<div class="rounded-sm bg-white px-1 text-[10px] leading-[12px] text-black">
|
<div class="rounded-sm bg-white px-1 text-[10px] leading-[12px] text-black">
|
||||||
v{version}
|
v{version}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.content {
|
.content {
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.container::before {
|
.container::before {
|
||||||
content: "";
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
mix-blend-mode: overlay;
|
mix-blend-mode: overlay;
|
||||||
top: 0;
|
top: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
background: #dedede;
|
background: #dedede;
|
||||||
z-index: 0;
|
z-index: 0;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,147 +1,147 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { PackageStates, type GUIPackage } from "$libs/types";
|
import { PackageStates, type GUIPackage } from "$libs/types";
|
||||||
import clickOutside from "@tea/ui/lib/clickOutside";
|
import clickOutside from "@tea/ui/lib/clickOutside";
|
||||||
import PackageStateButton from "./package-install-button.svelte";
|
import PackageStateButton from "./package-install-button.svelte";
|
||||||
|
|
||||||
export let buttonSize: "small" | "large" = "small";
|
export let buttonSize: "small" | "large" = "small";
|
||||||
export let pkg: GUIPackage;
|
export let pkg: GUIPackage;
|
||||||
export let availableVersions: string[] = [];
|
export let availableVersions: string[] = [];
|
||||||
|
|
||||||
export let onClick = async (_version: string) => {
|
export let onClick = async (_version: string) => {
|
||||||
console.log("do nothing");
|
console.log("do nothing");
|
||||||
};
|
};
|
||||||
|
|
||||||
$: isOpened = false;
|
$: isOpened = false;
|
||||||
|
|
||||||
const toggleOpen = (evt?: MouseEvent) => {
|
const toggleOpen = (evt?: MouseEvent) => {
|
||||||
evt?.preventDefault();
|
evt?.preventDefault();
|
||||||
|
|
||||||
if ([PackageStates.INSTALLING, PackageStates.UPDATING].includes(pkg.state)) {
|
if ([PackageStates.INSTALLING, PackageStates.UPDATING].includes(pkg.state)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
isOpened = !isOpened;
|
isOpened = !isOpened;
|
||||||
};
|
};
|
||||||
|
|
||||||
const isInstalled = (version: string) => pkg.installed_versions?.includes(version);
|
const isInstalled = (version: string) => pkg.installed_versions?.includes(version);
|
||||||
|
|
||||||
$: installedVersions = pkg.installed_versions || [];
|
$: installedVersions = pkg.installed_versions || [];
|
||||||
|
|
||||||
const handleClick = (evt: MouseEvent, version: string) => {
|
const handleClick = (evt: MouseEvent, version: string) => {
|
||||||
if (isInstalled(version)) {
|
if (isInstalled(version)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
isOpened = false;
|
isOpened = false;
|
||||||
if (version) {
|
if (version) {
|
||||||
onClick(version);
|
onClick(version);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClickOutside = () => (isOpened = false);
|
const handleClickOutside = () => (isOpened = false);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="dropdown z-10" use:clickOutside on:click_outside={handleClickOutside}>
|
<div class="dropdown z-10" use:clickOutside on:click_outside={handleClickOutside}>
|
||||||
<PackageStateButton {buttonSize} {pkg} onClick={toggleOpen}>
|
<PackageStateButton {buttonSize} {pkg} onClick={toggleOpen}>
|
||||||
<div slot="selector" class="pt-2">
|
<div slot="selector" class="pt-2">
|
||||||
<div class="version-list" class:visible={isOpened}>
|
<div class="version-list" class:visible={isOpened}>
|
||||||
{#each availableVersions as version, idx}
|
{#each availableVersions as version, idx}
|
||||||
{#if idx !== 0}<hr class="divider" />{/if}
|
{#if idx !== 0}<hr class="divider" />{/if}
|
||||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
<div
|
<div
|
||||||
class="version-item flex items-center justify-start gap-x-1 text-xs"
|
class="version-item flex items-center justify-start gap-x-1 text-xs"
|
||||||
class:installable-version={!installedVersions.includes(version)}
|
class:installable-version={!installedVersions.includes(version)}
|
||||||
on:click={(evt) => handleClick(evt, version)}
|
on:click={(evt) => handleClick(evt, version)}
|
||||||
>
|
>
|
||||||
<div class:installed-text={installedVersions.includes(version)}>v{version}</div>
|
<div class:installed-text={installedVersions.includes(version)}>v{version}</div>
|
||||||
{#if idx === 0}
|
{#if idx === 0}
|
||||||
<div class="latest-version">(latest)</div>
|
<div class="latest-version">(latest)</div>
|
||||||
{/if}
|
{/if}
|
||||||
{#if installedVersions.includes(version)}
|
{#if installedVersions.includes(version)}
|
||||||
<div class="flex grow justify-end">
|
<div class="flex grow justify-end">
|
||||||
<i class="installed-text icon-check-circle flex" />
|
<i class="installed-text icon-check-circle flex" />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</PackageStateButton>
|
</PackageStateButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.version-list {
|
.version-list {
|
||||||
display: none;
|
display: none;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
color: white;
|
color: white;
|
||||||
background-color: #1a1a1a;
|
background-color: #1a1a1a;
|
||||||
border: 0.5px solid #949494;
|
border: 0.5px solid #949494;
|
||||||
box-shadow: 0px 0px 8px rgba(0, 0, 0, 0.5);
|
box-shadow: 0px 0px 8px rgba(0, 0, 0, 0.5);
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
max-height: 160px;
|
max-height: 160px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.version-item {
|
.version-item {
|
||||||
margin: 4px 6px;
|
margin: 4px 6px;
|
||||||
padding: 4px 6px;
|
padding: 4px 6px;
|
||||||
height: auto;
|
height: auto;
|
||||||
width: auto;
|
width: auto;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.installable-version {
|
.installable-version {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.installable-version:hover {
|
.installable-version:hover {
|
||||||
outline: 1px solid #949494;
|
outline: 1px solid #949494;
|
||||||
background-color: rgba(148, 148, 148, 0.35);
|
background-color: rgba(148, 148, 148, 0.35);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropdown {
|
.dropdown {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.divider {
|
.divider {
|
||||||
border: 1px solid #272626;
|
border: 1px solid #272626;
|
||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.installed-text {
|
.installed-text {
|
||||||
color: #00ffd0;
|
color: #00ffd0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.latest-version {
|
.latest-version {
|
||||||
color: #af5fff;
|
color: #af5fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.visible {
|
.visible {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* width */
|
/* width */
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
width: 6px;
|
width: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Track */
|
/* Track */
|
||||||
::-webkit-scrollbar-track {
|
::-webkit-scrollbar-track {
|
||||||
background: #272626;
|
background: #272626;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Handle */
|
/* Handle */
|
||||||
::-webkit-scrollbar-thumb {
|
::-webkit-scrollbar-thumb {
|
||||||
background: #949494;
|
background: #949494;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Handle on hover */
|
/* Handle on hover */
|
||||||
::-webkit-scrollbar-thumb:hover {
|
::-webkit-scrollbar-thumb:hover {
|
||||||
background: white;
|
background: white;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,56 +1,56 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import '$appcss';
|
import "$appcss";
|
||||||
import { afterUpdate } from 'svelte';
|
import { afterUpdate } from "svelte";
|
||||||
import ReviewCard from '@tea/ui/review-card/review-card.svelte';
|
import ReviewCard from "@tea/ui/review-card/review-card.svelte";
|
||||||
|
|
||||||
import type { Review } from '@tea/ui/types';
|
import type { Review } from "@tea/ui/types";
|
||||||
export let reviews: Review[];
|
export let reviews: Review[];
|
||||||
|
|
||||||
export let showLimit = 9;
|
export let showLimit = 9;
|
||||||
let showMore = false;
|
let showMore = false;
|
||||||
|
|
||||||
const getColReviews = (n: number) => {
|
const getColReviews = (n: number) => {
|
||||||
const showReviews = reviews.filter((_item, i) => (i - n) % 3 === 0);
|
const showReviews = reviews.filter((_item, i) => (i - n) % 3 === 0);
|
||||||
return showMore ? showReviews : showReviews.slice(0, showLimit / 3);
|
return showMore ? showReviews : showReviews.slice(0, showLimit / 3);
|
||||||
};
|
};
|
||||||
|
|
||||||
let col1: Review[] = [];
|
let col1: Review[] = [];
|
||||||
let col2: Review[] = [];
|
let col2: Review[] = [];
|
||||||
let col3: Review[] = [];
|
let col3: Review[] = [];
|
||||||
|
|
||||||
afterUpdate(() => {
|
afterUpdate(() => {
|
||||||
col1 = getColReviews(0);
|
col1 = getColReviews(0);
|
||||||
col2 = getColReviews(1);
|
col2 = getColReviews(1);
|
||||||
col3 = getColReviews(2);
|
col3 = getColReviews(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO: problem with reviews with differing heights
|
// TODO: problem with reviews with differing heights
|
||||||
// ideally they should work like metro-ui to not have extreme height diff between columns
|
// ideally they should work like metro-ui to not have extreme height diff between columns
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<header class="border-gray text-primary border bg-black p-4">REVIEWS ({reviews.length})</header>
|
<header class="border-gray text-primary border bg-black p-4">REVIEWS ({reviews.length})</header>
|
||||||
<section class="flex flex-row flex-wrap bg-black">
|
<section class="flex flex-row flex-wrap bg-black">
|
||||||
<div class="border-gray w-1/3 border-0 border-l-2 border-b-2 p-4">
|
<div class="border-gray w-1/3 border-0 border-l-2 border-b-2 p-4">
|
||||||
{#each col1 as review}
|
{#each col1 as review}
|
||||||
<ReviewCard {review} />
|
<ReviewCard {review} />
|
||||||
<div class="mt-4" />
|
<div class="mt-4" />
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
<div class="border-gray w-1/3 border-0 border-l-2 border-b-2 p-4">
|
<div class="border-gray w-1/3 border-0 border-l-2 border-b-2 p-4">
|
||||||
{#each col2 as review}
|
{#each col2 as review}
|
||||||
<ReviewCard {review} />
|
<ReviewCard {review} />
|
||||||
<div class="mt-4" />
|
<div class="mt-4" />
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
<div class="border-gray w-1/3 border-0 border-x-2 border-b-2 p-4">
|
<div class="border-gray w-1/3 border-0 border-x-2 border-b-2 p-4">
|
||||||
{#each col3 as review}
|
{#each col3 as review}
|
||||||
<ReviewCard {review} />
|
<ReviewCard {review} />
|
||||||
<div class="mt-4" />
|
<div class="mt-4" />
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
{#if showLimit <= reviews.length && showMore === false}
|
{#if showLimit <= reviews.length && showMore === false}
|
||||||
<footer class="border-gray border bg-black p-4">
|
<footer class="border-gray border bg-black p-4">
|
||||||
<button on:click={() => (showMore = true)}>SHOW MORE</button>
|
<button on:click={() => (showMore = true)}>SHOW MORE</button>
|
||||||
</footer>
|
</footer>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -1,14 +1,16 @@
|
||||||
<script>
|
<script>
|
||||||
import Button from "@tea/ui/button/button.svelte";
|
import Button from "@tea/ui/button/button.svelte";
|
||||||
import { goto } from "$app/navigation";
|
import { goto } from "$app/navigation";
|
||||||
import { SideMenuOptions } from "$libs/types";
|
import { SideMenuOptions } from "$libs/types";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex w-full h-3/4 justify-center items-center flex-col">
|
<div class="flex h-3/4 w-full flex-col items-center justify-center">
|
||||||
<div class="text-2xl text-[#e1e1e1] bg-[#252424] px-9 py-3">
|
<div class="bg-[#252424] px-9 py-3 text-2xl text-[#e1e1e1]">
|
||||||
You don’t have anything installed
|
You don’t have anything installed
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-6 h-6 w-40">
|
<div class="mt-6 h-6 w-40">
|
||||||
<Button type="plain" color="blue" onClick={() => goto(`/?tab=${SideMenuOptions.discover}`)}>DISCOVER</Button>
|
<Button type="plain" color="blue" onClick={() => goto(`/?tab=${SideMenuOptions.discover}`)}
|
||||||
|
>DISCOVER</Button
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
<div class="flex w-full h-3/4 justify-center items-center">
|
<div class="flex h-3/4 w-full items-center justify-center">
|
||||||
<div class="text-2xl text-[#e1e1e1] bg-[#252424] px-9 py-3">
|
<div class="bg-[#252424] px-9 py-3 text-2xl text-[#e1e1e1]">You’re all up to date 👍</div>
|
||||||
You’re all up to date 👍
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,33 +1,33 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import "$appcss";
|
import "$appcss";
|
||||||
|
|
||||||
import { PackageStates, type GUIPackage } from "$libs/types";
|
import { PackageStates, type GUIPackage } from "$libs/types";
|
||||||
import { packagesStore } from "$libs/stores";
|
import { packagesStore } from "$libs/stores";
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import PackageCard from "$components/package-card/package-card.svelte";
|
import PackageCard from "$components/package-card/package-card.svelte";
|
||||||
|
|
||||||
export let tab = "all";
|
export let tab = "all";
|
||||||
export let pkg: GUIPackage;
|
export let pkg: GUIPackage;
|
||||||
export let layout: "bottom" | "left" | "right" = "bottom";
|
export let layout: "bottom" | "left" | "right" = "bottom";
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
packagesStore.fetchPackageBottles(pkg.full_name);
|
packagesStore.fetchPackageBottles(pkg.full_name);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<PackageCard
|
<PackageCard
|
||||||
{pkg}
|
{pkg}
|
||||||
{layout}
|
{layout}
|
||||||
link="/packages/{pkg.slug}?tab={tab}"
|
link="/packages/{pkg.slug}?tab={tab}"
|
||||||
progessLoading={pkg.install_progress_percentage}
|
progessLoading={pkg.install_progress_percentage}
|
||||||
onClickCTA={async () => {
|
onClickCTA={async () => {
|
||||||
if (
|
if (
|
||||||
[PackageStates.INSTALLED, PackageStates.INSTALLING, PackageStates.UPDATING].includes(
|
[PackageStates.INSTALLED, PackageStates.INSTALLING, PackageStates.UPDATING].includes(
|
||||||
pkg.state
|
pkg.state
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
packagesStore.installPkg(pkg);
|
packagesStore.installPkg(pkg);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,159 +1,159 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import "$appcss";
|
import "$appcss";
|
||||||
import { watchResize } from "svelte-watch-resize";
|
import { watchResize } from "svelte-watch-resize";
|
||||||
import InfiniteScroll from "svelte-infinite-scroll";
|
import InfiniteScroll from "svelte-infinite-scroll";
|
||||||
// import { t } from '$libs/translations';
|
// import { t } from '$libs/translations';
|
||||||
import type { GUIPackage } from "$libs/types";
|
import type { GUIPackage } from "$libs/types";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import { PackageStates, SideMenuOptions } from "$libs/types";
|
import { PackageStates, SideMenuOptions } from "$libs/types";
|
||||||
import Preloader from "@tea/ui/Preloader/Preloader.svelte";
|
import Preloader from "@tea/ui/Preloader/Preloader.svelte";
|
||||||
import Package from "./package.svelte";
|
import Package from "./package.svelte";
|
||||||
import NoInstalls from "./no-installs.svelte";
|
import NoInstalls from "./no-installs.svelte";
|
||||||
import NoUpdates from "./no-updates.svelte";
|
import NoUpdates from "./no-updates.svelte";
|
||||||
import { packagesStore } from "$libs/stores";
|
import { packagesStore } from "$libs/stores";
|
||||||
|
|
||||||
const { packageList: allPackages } = packagesStore;
|
const { packageList: allPackages } = packagesStore;
|
||||||
export let packageFilter: SideMenuOptions = SideMenuOptions.all;
|
export let packageFilter: SideMenuOptions = SideMenuOptions.all;
|
||||||
|
|
||||||
export let sortBy: "popularity" | "most recent" = "most recent";
|
export let sortBy: "popularity" | "most recent" = "most recent";
|
||||||
export let sortDirection: "asc" | "desc" = "desc";
|
export let sortDirection: "asc" | "desc" = "desc";
|
||||||
|
|
||||||
export let scrollY = 0;
|
export let scrollY = 0;
|
||||||
|
|
||||||
let loadMore = 9;
|
let loadMore = 9;
|
||||||
let limit = loadMore + 9;
|
let limit = loadMore + 9;
|
||||||
|
|
||||||
// TODO: figure out a better type strategy here so that this breaks if SideMenuOptions is updated
|
// TODO: figure out a better type strategy here so that this breaks if SideMenuOptions is updated
|
||||||
const pkgFilters: { [key: string]: (pkg: GUIPackage) => boolean } = {
|
const pkgFilters: { [key: string]: (pkg: GUIPackage) => boolean } = {
|
||||||
[SideMenuOptions.all]: (_pkg: GUIPackage) => true,
|
[SideMenuOptions.all]: (_pkg: GUIPackage) => true,
|
||||||
[SideMenuOptions.installed]: (pkg: GUIPackage) => {
|
[SideMenuOptions.installed]: (pkg: GUIPackage) => {
|
||||||
return [
|
return [
|
||||||
PackageStates.INSTALLED,
|
PackageStates.INSTALLED,
|
||||||
PackageStates.INSTALLING,
|
PackageStates.INSTALLING,
|
||||||
PackageStates.NEEDS_UPDATE,
|
PackageStates.NEEDS_UPDATE,
|
||||||
PackageStates.UPDATING
|
PackageStates.UPDATING
|
||||||
].includes(pkg.state);
|
].includes(pkg.state);
|
||||||
},
|
},
|
||||||
[SideMenuOptions.installed_updates_available]: (pkg: GUIPackage) => {
|
[SideMenuOptions.installed_updates_available]: (pkg: GUIPackage) => {
|
||||||
return [PackageStates.UPDATING, PackageStates.NEEDS_UPDATE].includes(pkg.state);
|
return [PackageStates.UPDATING, PackageStates.NEEDS_UPDATE].includes(pkg.state);
|
||||||
},
|
},
|
||||||
[SideMenuOptions.recently_updated]: (pkg: GUIPackage) => {
|
[SideMenuOptions.recently_updated]: (pkg: GUIPackage) => {
|
||||||
return moment(pkg.last_modified).isAfter(moment().subtract(30, "days"));
|
return moment(pkg.last_modified).isAfter(moment().subtract(30, "days"));
|
||||||
},
|
},
|
||||||
[SideMenuOptions.new_packages]: (pkg: GUIPackage) => {
|
[SideMenuOptions.new_packages]: (pkg: GUIPackage) => {
|
||||||
return moment(pkg.created).isAfter(moment().subtract(30, "days"));
|
return moment(pkg.created).isAfter(moment().subtract(30, "days"));
|
||||||
},
|
},
|
||||||
[SideMenuOptions.popular]: (pkg: GUIPackage) =>
|
[SideMenuOptions.popular]: (pkg: GUIPackage) =>
|
||||||
pkg.categories.includes(SideMenuOptions.popular),
|
pkg.categories.includes(SideMenuOptions.popular),
|
||||||
[SideMenuOptions.featured]: (pkg: GUIPackage) =>
|
[SideMenuOptions.featured]: (pkg: GUIPackage) =>
|
||||||
pkg.categories.includes(SideMenuOptions.featured),
|
pkg.categories.includes(SideMenuOptions.featured),
|
||||||
[SideMenuOptions.essentials]: (pkg: GUIPackage) =>
|
[SideMenuOptions.essentials]: (pkg: GUIPackage) =>
|
||||||
pkg.categories.includes(SideMenuOptions.essentials),
|
pkg.categories.includes(SideMenuOptions.essentials),
|
||||||
[SideMenuOptions.starstruck]: (pkg: GUIPackage) =>
|
[SideMenuOptions.starstruck]: (pkg: GUIPackage) =>
|
||||||
pkg.categories.includes(SideMenuOptions.starstruck),
|
pkg.categories.includes(SideMenuOptions.starstruck),
|
||||||
[SideMenuOptions.made_by_tea]: (pkg: GUIPackage) => pkg.full_name.includes("tea.xyz")
|
[SideMenuOptions.made_by_tea]: (pkg: GUIPackage) => pkg.full_name.includes("tea.xyz")
|
||||||
};
|
};
|
||||||
|
|
||||||
const onScroll = (e: Event) => {
|
const onScroll = (e: Event) => {
|
||||||
const target = e.target as HTMLInputElement;
|
const target = e.target as HTMLInputElement;
|
||||||
scrollY = target.scrollTop || 0;
|
scrollY = target.scrollTop || 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
$: packages = $allPackages.filter(pkgFilters[packageFilter] || pkgFilters.all).sort((a, b) => {
|
$: packages = $allPackages.filter(pkgFilters[packageFilter] || pkgFilters.all).sort((a, b) => {
|
||||||
if (sortBy === "popularity") {
|
if (sortBy === "popularity") {
|
||||||
const aPop = +a.dl_count + a.installs;
|
const aPop = +a.dl_count + a.installs;
|
||||||
const bPop = +b.dl_count + b.installs;
|
const bPop = +b.dl_count + b.installs;
|
||||||
return sortDirection === "asc" ? aPop - bPop : bPop - aPop;
|
return sortDirection === "asc" ? aPop - bPop : bPop - aPop;
|
||||||
} else {
|
} else {
|
||||||
// most recent
|
// most recent
|
||||||
const aDate = new Date(a.last_modified);
|
const aDate = new Date(a.last_modified);
|
||||||
const bDate = new Date(b.last_modified);
|
const bDate = new Date(b.last_modified);
|
||||||
return sortDirection === "asc" ? +aDate - +bDate : +bDate - +aDate;
|
return sortDirection === "asc" ? +aDate - +bDate : +bDate - +aDate;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const onResize = (node: HTMLElement) => {
|
const onResize = (node: HTMLElement) => {
|
||||||
const assumedCardHeight = 250;
|
const assumedCardHeight = 250;
|
||||||
const cardRows = Math.floor(packages.length / 3);
|
const cardRows = Math.floor(packages.length / 3);
|
||||||
const minCardRows = Math.floor(node.scrollHeight / assumedCardHeight);
|
const minCardRows = Math.floor(node.scrollHeight / assumedCardHeight);
|
||||||
if (cardRows < minCardRows) {
|
if (cardRows < minCardRows) {
|
||||||
const addLimit = 3 * (minCardRows - cardRows);
|
const addLimit = 3 * (minCardRows - cardRows);
|
||||||
limit += addLimit;
|
limit += addLimit;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="relative h-full w-full">
|
<div class="relative h-full w-full">
|
||||||
<ul class="flex flex-wrap content-start bg-black" use:watchResize={onResize} on:scroll={onScroll}>
|
<ul class="flex flex-wrap content-start bg-black" use:watchResize={onResize} on:scroll={onScroll}>
|
||||||
{#if packages.length > 0}
|
{#if packages.length > 0}
|
||||||
{#each packages as pkg, index}
|
{#each packages as pkg, index}
|
||||||
{#if index < limit}
|
{#if index < limit}
|
||||||
<div class="card z-1 p-1" class:animate-puls={pkg.state === PackageStates.INSTALLING}>
|
<div class="card z-1 p-1" class:animate-puls={pkg.state === PackageStates.INSTALLING}>
|
||||||
<Package tab={packageFilter} {pkg} layout="bottom"/>
|
<Package tab={packageFilter} {pkg} layout="bottom" />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
{:else if packageFilter === SideMenuOptions.installed}
|
{:else if packageFilter === SideMenuOptions.installed}
|
||||||
<NoInstalls />
|
<NoInstalls />
|
||||||
{:else if packageFilter === SideMenuOptions.installed_updates_available}
|
{:else if packageFilter === SideMenuOptions.installed_updates_available}
|
||||||
<NoUpdates />
|
<NoUpdates />
|
||||||
{:else}
|
{:else}
|
||||||
{#each Array(9) as _}
|
{#each Array(9) as _}
|
||||||
<section class="card p-1 h-{238}">
|
<section class="card p-1 h-{238}">
|
||||||
<div class="border-gray h-full w-full border">
|
<div class="border-gray h-full w-full border">
|
||||||
<Preloader />
|
<Preloader />
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
{/each}
|
{/each}
|
||||||
{/if}
|
{/if}
|
||||||
<InfiniteScroll threshold={100} on:loadMore={() => (limit += loadMore)} />
|
<InfiniteScroll threshold={100} on:loadMore={() => (limit += loadMore)} />
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
ul {
|
ul {
|
||||||
margin-top: 0px;
|
margin-top: 0px;
|
||||||
padding-top: 80px;
|
padding-top: 80px;
|
||||||
height: calc(100vh - 49px);
|
height: calc(100vh - 49px);
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
padding-right: 4px;
|
padding-right: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* width */
|
/* width */
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
width: 6px;
|
width: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Track */
|
/* Track */
|
||||||
::-webkit-scrollbar-track {
|
::-webkit-scrollbar-track {
|
||||||
background: #272626;
|
background: #272626;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Handle */
|
/* Handle */
|
||||||
::-webkit-scrollbar-thumb {
|
::-webkit-scrollbar-thumb {
|
||||||
background: #949494;
|
background: #949494;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Handle on hover */
|
/* Handle on hover */
|
||||||
::-webkit-scrollbar-thumb:hover {
|
::-webkit-scrollbar-thumb:hover {
|
||||||
background: white;
|
background: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card {
|
.card {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (min-width: 650px) {
|
@media screen and (min-width: 650px) {
|
||||||
.card {
|
.card {
|
||||||
width: 50%;
|
width: 50%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (min-width: 1000px) {
|
@media screen and (min-width: 1000px) {
|
||||||
.card {
|
.card {
|
||||||
width: 33.333333%;
|
width: 33.333333%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
export let coverUrl = '';
|
export let coverUrl = "";
|
||||||
let clazz = '';
|
let clazz = "";
|
||||||
export { clazz as class };
|
export { clazz as class };
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<figure class="relative mb-8 h-32 w-full uppercase {clazz}">
|
<figure class="relative mb-8 h-32 w-full uppercase {clazz}">
|
||||||
{#if coverUrl}
|
{#if coverUrl}
|
||||||
<img src={coverUrl} class="absolute z-0 h-32 w-full object-cover" alt="cover" />
|
<img src={coverUrl} class="absolute z-0 h-32 w-full object-cover" alt="cover" />
|
||||||
{/if}
|
{/if}
|
||||||
<div class="text-primary absolute bottom-0 text-6xl leading-[32px]">
|
<div class="text-primary absolute bottom-0 text-6xl leading-[32px]">
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</figure>
|
</figure>
|
||||||
|
|
|
@ -1,24 +1,24 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
export let label = '';
|
export let label = "";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<section class="bg-gray p-8">
|
<section class="bg-gray p-8">
|
||||||
<header>{label}</header>
|
<header>{label}</header>
|
||||||
<slot />
|
<slot />
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
section {
|
section {
|
||||||
position: relative;
|
position: relative;
|
||||||
min-height: 240px;
|
min-height: 240px;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-width: 100%;
|
min-width: 100%;
|
||||||
/* background-color: #ccc; */
|
/* background-color: #ccc; */
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
header {
|
header {
|
||||||
color: rgb(50, 48, 48);
|
color: rgb(50, 48, 48);
|
||||||
font-size: 3em;
|
font-size: 3em;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,35 +1,35 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import '$appcss';
|
import "$appcss";
|
||||||
import { shellOpenExternal } from '@native';
|
import { shellOpenExternal } from "@native";
|
||||||
const openGithub = () => {
|
const openGithub = () => {
|
||||||
shellOpenExternal("https://github.com/teaxyz");
|
shellOpenExternal("https://github.com/teaxyz");
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="card social-box" style="width: 100%; float:right;">
|
<div class="card social-box" style="width: 100%; float:right;">
|
||||||
<header class="border-gray text-primary border-b pt-7 pb-7 pl-5">PRE-FLIGHT</header>
|
<header class="border-gray text-primary border-b pt-7 pb-7 pl-5">PRE-FLIGHT</header>
|
||||||
<div class="listbox-item border-gray border-b p-6">
|
<div class="listbox-item border-gray border-b p-6">
|
||||||
<a href="/cli">
|
<a href="/cli">
|
||||||
<p>Install Tea</p>
|
<p>Install Tea</p>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="listbox-item border-gray border-b p-6">
|
<div class="listbox-item border-gray border-b p-6">
|
||||||
<div>
|
<div>
|
||||||
<p>Authenticate</p>
|
<p>Authenticate</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="listbox-item p-6">
|
<div class="listbox-item p-6">
|
||||||
<button on:click={openGithub}>Give tea a star</button>
|
<button on:click={openGithub}>Give tea a star</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.card {
|
.card {
|
||||||
border: 2px solid #949494;
|
border: 2px solid #949494;
|
||||||
background-color: #1a1a1a;
|
background-color: #1a1a1a;
|
||||||
}
|
}
|
||||||
|
|
||||||
.listbox-item {
|
.listbox-item {
|
||||||
height: 75px;
|
height: 75px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,33 +1,33 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import '$appcss';
|
import "$appcss";
|
||||||
import { authStore } from '$libs/stores';
|
import { authStore } from "$libs/stores";
|
||||||
|
|
||||||
const { user } = authStore;
|
const { user } = authStore;
|
||||||
// const authPage = `http://localhost:3000/v1/auth/user?device_id=${authStore.deviceId}`; // https://api.tea.xyz/v1/auth/user?device_id=device_id
|
// const authPage = `http://localhost:3000/v1/auth/user?device_id=${authStore.deviceId}`; // https://api.tea.xyz/v1/auth/user?device_id=device_id
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if $user}
|
{#if $user}
|
||||||
<section class="border-gray border-2 bg-black p-2">
|
<section class="border-gray border-2 bg-black p-2">
|
||||||
<div class="profile_banner border-gray container flex border bg-black">
|
<div class="profile_banner border-gray container flex border bg-black">
|
||||||
<img class="w-1/5" src={$user.avatar_url || '/images/bored-ape.png'} alt="profile" />
|
<img class="w-1/5" src={$user.avatar_url || "/images/bored-ape.png"} alt="profile" />
|
||||||
<div class="flex w-4/5 items-center p-5">
|
<div class="flex w-4/5 items-center p-5">
|
||||||
<div class="w-1/2 pl-5">
|
<div class="w-1/2 pl-5">
|
||||||
<p class="text-gray uppercase">Authenticated with GitHub</p>
|
<p class="text-gray uppercase">Authenticated with GitHub</p>
|
||||||
<p />
|
<p />
|
||||||
<p class="text-primary text-4xl">@{$user.login}</p>
|
<p class="text-primary text-4xl">@{$user.login}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="border-gray h-full border-l" />
|
<div class="border-gray h-full border-l" />
|
||||||
<div class="w-1/2 pl-10">
|
<div class="w-1/2 pl-10">
|
||||||
<p class="text-gray uppercase leading-loose">
|
<p class="text-gray uppercase leading-loose">
|
||||||
Country: <span>$user?.country}</span><br />Wallet:
|
Country: <span>$user?.country}</span><br />Wallet:
|
||||||
{#if $user.wallet}
|
{#if $user.wallet}
|
||||||
<span>{$user.wallet}</span>
|
<span>{$user.wallet}</span>
|
||||||
{:else}
|
{:else}
|
||||||
<a class="text-green underline" href="/">Connect Now</a>
|
<a class="text-green underline" href="/">Connect Now</a>
|
||||||
{/if}
|
{/if}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -20,6 +20,18 @@
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<svg viewBox="0 0 100 100">
|
||||||
|
<path d="M50,5A45 45 0 1 1 49.9999 5" />
|
||||||
|
<path d={progressPath()} />
|
||||||
|
</svg>
|
||||||
|
<div>
|
||||||
|
<slot>
|
||||||
|
<span>{value}</span>
|
||||||
|
</slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
svg {
|
svg {
|
||||||
fill: var(--progress-fill, transparent);
|
fill: var(--progress-fill, transparent);
|
||||||
|
@ -48,14 +60,3 @@
|
||||||
transform: translate(-50%, -50%);
|
transform: translate(-50%, -50%);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<div>
|
|
||||||
<svg viewBox="0 0 100 100">
|
|
||||||
<path d="M50,5A45 45 0 1 1 49.9999 5" />
|
|
||||||
<path d="{progressPath()}" />
|
|
||||||
</svg>
|
|
||||||
<div>
|
|
||||||
<slot>
|
|
||||||
<span>{value}</span>
|
|
||||||
</slot>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
|
@ -1,139 +1,138 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import '$appcss';
|
import "$appcss";
|
||||||
import { t } from '$libs/translations';
|
import { t } from "$libs/translations";
|
||||||
|
|
||||||
|
type SortOption = "popularity" | "most recent";
|
||||||
|
export let onSort: (opt: SortOption, dir: "asc" | "desc") => void;
|
||||||
|
|
||||||
type SortOption = "popularity" | "most recent";
|
let sortBy: SortOption = "popularity";
|
||||||
export let onSort: (opt: SortOption, dir: 'asc' | 'desc') => void;
|
let sortDirection: "asc" | "desc" = "desc";
|
||||||
|
|
||||||
let sortBy: SortOption = "popularity";
|
const sortOptions: SortOption[] = ["popularity", "most recent"];
|
||||||
let sortDirection: 'asc' | 'desc' = 'desc';
|
|
||||||
|
|
||||||
const sortOptions: SortOption[] = ["popularity", "most recent"];
|
const optionLabels = {
|
||||||
|
[sortOptions[0]]: $t("sorting.popularity"),
|
||||||
|
[sortOptions[1]]: $t("sorting.most-recent")
|
||||||
|
};
|
||||||
|
|
||||||
const optionLabels = {
|
const setSortBy = (opt: SortOption) => {
|
||||||
[sortOptions[0]]: $t("sorting.popularity"),
|
sortBy = opt;
|
||||||
[sortOptions[1]]: $t("sorting.most-recent")
|
if (onSort) {
|
||||||
}
|
onSort(sortBy, sortDirection);
|
||||||
|
}
|
||||||
const setSortBy = (opt: SortOption) => {
|
};
|
||||||
sortBy = opt;
|
const setSortDir = (opt: SortOption, dir: "asc" | "desc") => {
|
||||||
if (onSort) {
|
sortDirection = dir;
|
||||||
onSort(sortBy, sortDirection);
|
setSortBy(opt);
|
||||||
}
|
};
|
||||||
};
|
|
||||||
const setSortDir = (opt: SortOption, dir: 'asc' | 'desc') => {
|
|
||||||
sortDirection = dir;
|
|
||||||
setSortBy(opt);
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<section class="sorting-container bg-black text-gray">
|
<section class="sorting-container text-gray bg-black">
|
||||||
<div class="dropdown">
|
<div class="dropdown">
|
||||||
<div class="dropdown-title">{$t("sorting.label")}</div>
|
<div class="dropdown-title">{$t("sorting.label")}</div>
|
||||||
<ul class="dropdown-content column flex">
|
<ul class="dropdown-content column flex">
|
||||||
{#each sortOptions as option}
|
{#each sortOptions as option}
|
||||||
<li class="flex items-center">
|
<li class="flex items-center">
|
||||||
<button
|
<button
|
||||||
class="sort-btn"
|
class="sort-btn"
|
||||||
class:active={sortBy === option}
|
class:active={sortBy === option}
|
||||||
on:click={() => setSortBy(option)}
|
on:click={() => setSortBy(option)}
|
||||||
>
|
>
|
||||||
{optionLabels[option]}
|
{optionLabels[option]}
|
||||||
</button>
|
</button>
|
||||||
<div class="direction-arrows">
|
<div class="direction-arrows">
|
||||||
<button
|
<button
|
||||||
on:click={() => setSortDir(option, 'asc')}
|
on:click={() => setSortDir(option, "asc")}
|
||||||
class={sortBy === option && sortDirection === 'asc' ? 'active' : ''}>↑</button
|
class={sortBy === option && sortDirection === "asc" ? "active" : ""}>↑</button
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
on:click={() => setSortDir(option, 'desc')}
|
on:click={() => setSortDir(option, "desc")}
|
||||||
class={sortBy === option && sortDirection === 'desc' ? 'active' : ''}>↓</button
|
class={sortBy === option && sortDirection === "desc" ? "active" : ""}>↓</button
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
{/each}
|
{/each}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.direction-arrows {
|
.direction-arrows {
|
||||||
float: right;
|
float: right;
|
||||||
}
|
}
|
||||||
.direction-arrows button {
|
.direction-arrows button {
|
||||||
opacity: 0.3;
|
opacity: 0.3;
|
||||||
}
|
}
|
||||||
.direction-arrows button.active {
|
.direction-arrows button.active {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sorting-container {
|
.sorting-container {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
max-width: 240px;
|
max-width: 240px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
min-height: 30px;
|
min-height: 30px;
|
||||||
transition: 0.1s linear;
|
transition: 0.1s linear;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropdown {
|
.dropdown {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: auto;
|
height: auto;
|
||||||
position: relative;
|
position: relative;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropdown-content {
|
.dropdown-content {
|
||||||
display: none;
|
display: none;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
background-color: #1a1a1a;
|
background-color: #1a1a1a;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
box-shadow: 0px 8px 16px 0px rgba(0, 0, 0, 0.2);
|
box-shadow: 0px 8px 16px 0px rgba(0, 0, 0, 0.2);
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
color: white;
|
color: white;
|
||||||
list-style: none;
|
list-style: none;
|
||||||
padding: 0px;
|
padding: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropdown-content li {
|
.dropdown-content li {
|
||||||
position: relative;
|
position: relative;
|
||||||
padding: 0px 10px;
|
padding: 0px 10px;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
line-height: 36px;
|
line-height: 36px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropdown-content li .sort-btn {
|
.dropdown-content li .sort-btn {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: calc(100% - 40px);
|
width: calc(100% - 40px);
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropdown-content li .sort-btn.active {
|
.dropdown-content li .sort-btn.active {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropdown-content li .direction-arrows {
|
.dropdown-content li .direction-arrows {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 10px;
|
right: 10px;
|
||||||
top: 0px;
|
top: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropdown-content li:hover {
|
.dropdown-content li:hover {
|
||||||
background: #00ffd0;
|
background: #00ffd0;
|
||||||
color: black;
|
color: black;
|
||||||
}
|
}
|
||||||
.dropdown:hover .dropdown-content {
|
.dropdown:hover .dropdown-content {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropdown-title {
|
.dropdown-title {
|
||||||
height: 40px;
|
height: 40px;
|
||||||
line-height: 40px;
|
line-height: 40px;
|
||||||
padding-left: 10px;
|
padding-left: 10px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -3,13 +3,17 @@
|
||||||
import { shellOpenExternal } from "@native";
|
import { shellOpenExternal } from "@native";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex flex-col justify-center items-center h-full">
|
<div class="flex h-full flex-col items-center justify-center">
|
||||||
<div class="text-2xl">Hmm, we don't have that one yet...</div>
|
<div class="text-2xl">Hmm, we don't have that one yet...</div>
|
||||||
<div class="text-xl mt-4">But we'd love for you to submit it to the pantry!</div>
|
<div class="mt-4 text-xl">But we'd love for you to submit it to the pantry!</div>
|
||||||
<div class="mt-10">
|
<div class="mt-10">
|
||||||
<Button type="plain" color="blue" onClick={() => shellOpenExternal("https://github.com/teaxyz/pantry")}>
|
<Button
|
||||||
<span class="text-sm px-12">VISIT PANTRY</span>
|
type="plain"
|
||||||
|
color="blue"
|
||||||
|
onClick={() => shellOpenExternal("https://github.com/teaxyz/pantry")}
|
||||||
|
>
|
||||||
|
<span class="px-12 text-sm">VISIT PANTRY</span>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-gray text-xs mt-2">Redirects to github</div>
|
<div class="text-gray mt-2 text-xs">Redirects to github</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,62 +1,62 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { GUIPackage } from "$libs/types";
|
import type { GUIPackage } from "$libs/types";
|
||||||
import { packagesStore } from "$libs/stores";
|
import { packagesStore } from "$libs/stores";
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
|
|
||||||
import ImgLoader from "@tea/ui/img-loader/img-loader.svelte";
|
import ImgLoader from "@tea/ui/img-loader/img-loader.svelte";
|
||||||
import { goto } from "$app/navigation";
|
import { goto } from "$app/navigation";
|
||||||
import PackageInstallButton from "$components/package-install-button/package-install-button.svelte";
|
import PackageInstallButton from "$components/package-install-button/package-install-button.svelte";
|
||||||
export let pkg: GUIPackage; // Fuse package search result probably not updated
|
export let pkg: GUIPackage; // Fuse package search result probably not updated
|
||||||
export let onClick: () => Promise<void>;
|
export let onClick: () => Promise<void>;
|
||||||
export let onClose: () => void;
|
export let onClose: () => void;
|
||||||
|
|
||||||
const { packageList } = packagesStore;
|
const { packageList } = packagesStore;
|
||||||
|
|
||||||
$: updatedPkg = $packageList.find((p) => p.full_name === pkg.full_name);
|
$: updatedPkg = $packageList.find((p) => p.full_name === pkg.full_name);
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
packagesStore.fetchPackageBottles(pkg.full_name);
|
packagesStore.fetchPackageBottles(pkg.full_name);
|
||||||
});
|
});
|
||||||
|
|
||||||
const gotoPackagePage = () => {
|
const gotoPackagePage = () => {
|
||||||
goto(`/packages/${pkg.slug}?tab=all`);
|
goto(`/packages/${pkg.slug}?tab=all`);
|
||||||
onClose();
|
onClose();
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
<figure class="border-gray flex flex-row gap-2 border p-2">
|
<figure class="border-gray flex flex-row gap-2 border p-2">
|
||||||
<ImgLoader
|
<ImgLoader
|
||||||
on:click={() => gotoPackagePage()}
|
on:click={() => gotoPackagePage()}
|
||||||
class="pkg-image h-16 w-16 object-cover"
|
class="pkg-image h-16 w-16 object-cover"
|
||||||
src={!pkg.thumb_image_url.includes("https://tea.xyz")
|
src={!pkg.thumb_image_url.includes("https://tea.xyz")
|
||||||
? "/images/default-thumb.jpg"
|
? "/images/default-thumb.jpg"
|
||||||
: pkg.thumb_image_url}
|
: pkg.thumb_image_url}
|
||||||
alt={pkg.name}
|
alt={pkg.name}
|
||||||
/>
|
/>
|
||||||
<header class="flex-grow" on:click={() => gotoPackagePage()}>
|
<header class="flex-grow" on:click={() => gotoPackagePage()}>
|
||||||
<h1>{pkg.full_name}</h1>
|
<h1>{pkg.full_name}</h1>
|
||||||
<p class="line-clamp-2 text-xs">{pkg.desc}</p>
|
<p class="line-clamp-2 text-xs">{pkg.desc}</p>
|
||||||
</header>
|
</header>
|
||||||
<aside>
|
<aside>
|
||||||
<div>
|
<div>
|
||||||
{#if updatedPkg}
|
{#if updatedPkg}
|
||||||
<PackageInstallButton pkg={updatedPkg} {onClick} />
|
<PackageInstallButton pkg={updatedPkg} {onClick} />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
</figure>
|
</figure>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
figure:hover {
|
figure:hover {
|
||||||
background-color: #252525;
|
background-color: #252525;
|
||||||
}
|
}
|
||||||
|
|
||||||
aside {
|
aside {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
min-width: 140px;
|
min-width: 140px;
|
||||||
margin-right: 22px;
|
margin-right: 22px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,111 +1,111 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { packagesStore, searchStore } from "$libs/stores";
|
import { packagesStore, searchStore } from "$libs/stores";
|
||||||
import type { GUIPackage } from "$libs/types";
|
import type { GUIPackage } from "$libs/types";
|
||||||
import SearchInput from "@tea/ui/search-input/search-input.svelte";
|
import SearchInput from "@tea/ui/search-input/search-input.svelte";
|
||||||
import { t } from "$libs/translations";
|
import { t } from "$libs/translations";
|
||||||
import Preloader from "@tea/ui/Preloader/Preloader.svelte";
|
import Preloader from "@tea/ui/Preloader/Preloader.svelte";
|
||||||
import Package from "$components/packages/package.svelte";
|
import Package from "$components/packages/package.svelte";
|
||||||
import { PackageStates } from "$libs/types";
|
import { PackageStates } from "$libs/types";
|
||||||
import PackageResult from "./package-search-result.svelte";
|
import PackageResult from "./package-search-result.svelte";
|
||||||
import Mousetrap from "mousetrap";
|
import Mousetrap from "mousetrap";
|
||||||
// import Posts from '@tea/ui/posts/posts.svelte';
|
// import Posts from '@tea/ui/posts/posts.svelte';
|
||||||
|
|
||||||
import { installPackage } from "@native";
|
import { installPackage } from "@native";
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import NoSearchResults from "./no-search-results.svelte";
|
import NoSearchResults from "./no-search-results.svelte";
|
||||||
|
|
||||||
const { searching, packagesSearch } = searchStore;
|
const { searching, packagesSearch } = searchStore;
|
||||||
// import type { AirtablePost } from '@tea/ui/types';
|
// import type { AirtablePost } from '@tea/ui/types';
|
||||||
let term: string;
|
let term: string;
|
||||||
// let articles: AirtablePost[] = []; // news, blogs, etc
|
// let articles: AirtablePost[] = []; // news, blogs, etc
|
||||||
// let workshops: AirtablePost[] = []; // workshops, course
|
// let workshops: AirtablePost[] = []; // workshops, course
|
||||||
let loading = true;
|
let loading = true;
|
||||||
|
|
||||||
// searchStore.packagesSearch.subscribe((pkgs) => {
|
// searchStore.packagesSearch.subscribe((pkgs) => {
|
||||||
// packages = pkgs;
|
// packages = pkgs;
|
||||||
// });
|
// });
|
||||||
// searchStore.postsSearch.subscribe((posts) => {
|
// searchStore.postsSearch.subscribe((posts) => {
|
||||||
// let partialArticles: AirtablePost[] = [];
|
// let partialArticles: AirtablePost[] = [];
|
||||||
// let partialWorkshops: AirtablePost[] = [];
|
// let partialWorkshops: AirtablePost[] = [];
|
||||||
// for (let post of posts) {
|
// for (let post of posts) {
|
||||||
// if (post.tags.includes('news')) {
|
// if (post.tags.includes('news')) {
|
||||||
// partialArticles.push(post);
|
// partialArticles.push(post);
|
||||||
// }
|
// }
|
||||||
// if (post.tags.includes('course') || post.tags.includes('featured_course')) {
|
// if (post.tags.includes('course') || post.tags.includes('featured_course')) {
|
||||||
// partialWorkshops.push(post);
|
// partialWorkshops.push(post);
|
||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// articles = partialArticles;
|
// articles = partialArticles;
|
||||||
// workshops = partialWorkshops;
|
// workshops = partialWorkshops;
|
||||||
// });
|
// });
|
||||||
|
|
||||||
searchStore.searching.subscribe((v) => (loading = v));
|
searchStore.searching.subscribe((v) => (loading = v));
|
||||||
|
|
||||||
const onClose = () => {
|
const onClose = () => {
|
||||||
term = "";
|
term = "";
|
||||||
searchStore.searching.set(false);
|
searchStore.searching.set(false);
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if $searching === true}
|
{#if $searching === true}
|
||||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
<div id="bg-close" class="z-40" on:click={onClose} />
|
<div id="bg-close" class="z-40" on:click={onClose} />
|
||||||
<section class="z-50">
|
<section class="z-50">
|
||||||
<header class="border-gray flex border border-x-0 border-t-0 bg-black">
|
<header class="border-gray flex border border-x-0 border-t-0 bg-black">
|
||||||
<div class="relative w-full">
|
<div class="relative w-full">
|
||||||
<SearchInput
|
<SearchInput
|
||||||
class="h-9 w-full rounded-sm"
|
class="h-9 w-full rounded-sm"
|
||||||
size="small"
|
size="small"
|
||||||
autofocus={true}
|
autofocus={true}
|
||||||
placeholder={$t("store-search-placeholder")}
|
placeholder={$t("store-search-placeholder")}
|
||||||
onSearch={(search) => {
|
onSearch={(search) => {
|
||||||
term = search;
|
term = search;
|
||||||
searchStore.search(search);
|
searchStore.search(search);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<div class="absolute top-1 right-4 flex items-center gap-1 pt-[1px] opacity-50">
|
<div class="absolute top-1 right-4 flex items-center gap-1 pt-[1px] opacity-50">
|
||||||
<span class="mr-1 text-xs">clear</span>
|
<span class="mr-1 text-xs">clear</span>
|
||||||
<kbd class=" bg-gray flex items-center rounded-sm px-2 pt-[1px] text-white">
|
<kbd class=" bg-gray flex items-center rounded-sm px-2 pt-[1px] text-white">
|
||||||
<!-- using apple system ui font as our default renders the symbols wonky -->
|
<!-- using apple system ui font as our default renders the symbols wonky -->
|
||||||
<span class="" style="font-family: system-ui, -apple-system, sans-serif">⌘⇧⌫</span>
|
<span class="" style="font-family: system-ui, -apple-system, sans-serif">⌘⇧⌫</span>
|
||||||
</kbd>
|
</kbd>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button class="mr-2" on:click={onClose}>✕</button>
|
<button class="mr-2" on:click={onClose}>✕</button>
|
||||||
</header>
|
</header>
|
||||||
{#if term}
|
{#if term}
|
||||||
<div class="z-20 bg-black">
|
<div class="z-20 bg-black">
|
||||||
{#if $packagesSearch.length > 0}
|
{#if $packagesSearch.length > 0}
|
||||||
<header class="text-gray p-4 text-lg">
|
<header class="text-gray p-4 text-lg">
|
||||||
packages ({$packagesSearch.length})
|
packages ({$packagesSearch.length})
|
||||||
</header>
|
</header>
|
||||||
<ul class="flex flex-col gap-2 p-2">
|
<ul class="flex flex-col gap-2 p-2">
|
||||||
{#each $packagesSearch as pkg}
|
{#each $packagesSearch as pkg}
|
||||||
<div class={pkg.state === PackageStates.INSTALLING ? "animate-pulse" : ""}>
|
<div class={pkg.state === PackageStates.INSTALLING ? "animate-pulse" : ""}>
|
||||||
<PackageResult
|
<PackageResult
|
||||||
{pkg}
|
{pkg}
|
||||||
{onClose}
|
{onClose}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
if (
|
if (
|
||||||
[
|
[
|
||||||
PackageStates.INSTALLED,
|
PackageStates.INSTALLED,
|
||||||
PackageStates.INSTALLING,
|
PackageStates.INSTALLING,
|
||||||
PackageStates.UPDATING
|
PackageStates.UPDATING
|
||||||
].includes(pkg.state)
|
].includes(pkg.state)
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
packagesStore.installPkg(pkg);
|
packagesStore.installPkg(pkg);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</ul>
|
</ul>
|
||||||
{:else}
|
{:else}
|
||||||
<NoSearchResults />
|
<NoSearchResults />
|
||||||
{/if}
|
{/if}
|
||||||
<!-- <header class="text-primary p-4 text-lg">
|
<!-- <header class="text-primary p-4 text-lg">
|
||||||
Top Article Results ({articles.length})
|
Top Article Results ({articles.length})
|
||||||
</header>
|
</header>
|
||||||
{#if articles.length}
|
{#if articles.length}
|
||||||
|
@ -125,67 +125,67 @@
|
||||||
<Preloader />
|
<Preloader />
|
||||||
</section>
|
</section>
|
||||||
{/if} -->
|
{/if} -->
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="flex h-full w-full flex-col justify-center bg-black">
|
<div class="flex h-full w-full flex-col justify-center bg-black">
|
||||||
<p class="text-gray text-center">start typing to search</p>
|
<p class="text-gray text-center">start typing to search</p>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</section>
|
</section>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
#bg-close {
|
#bg-close {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
width: calc(100vw - 2px);
|
width: calc(100vw - 2px);
|
||||||
height: calc(100vh - 2px);
|
height: calc(100vh - 2px);
|
||||||
top: 1px;
|
top: 1px;
|
||||||
left: 1px;
|
left: 1px;
|
||||||
background: rgba(0, 0, 0, 0.7);
|
background: rgba(0, 0, 0, 0.7);
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
}
|
}
|
||||||
section {
|
section {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 50px;
|
top: 50px;
|
||||||
left: 50px;
|
left: 50px;
|
||||||
right: 50px;
|
right: 50px;
|
||||||
bottom: 50px;
|
bottom: 50px;
|
||||||
background: rgba(0, 0, 0, 0.7);
|
background: rgba(0, 0, 0, 0.7);
|
||||||
transition: opacity 0.3s ease-in-out;
|
transition: opacity 0.3s ease-in-out;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
height: auto;
|
height: auto;
|
||||||
border: gray 1px solid;
|
border: gray 1px solid;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
section > div {
|
section > div {
|
||||||
position: relative;
|
position: relative;
|
||||||
margin-top: 2px;
|
margin-top: 2px;
|
||||||
height: calc(100% - 40px);
|
height: calc(100% - 40px);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
transition: height 0.6s ease-in-out;
|
transition: height 0.6s ease-in-out;
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* width */
|
/* width */
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
width: 6px;
|
width: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Track */
|
/* Track */
|
||||||
::-webkit-scrollbar-track {
|
::-webkit-scrollbar-track {
|
||||||
background: #272626;
|
background: #272626;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Handle */
|
/* Handle */
|
||||||
::-webkit-scrollbar-thumb {
|
::-webkit-scrollbar-thumb {
|
||||||
background: #949494;
|
background: #949494;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Handle on hover */
|
/* Handle on hover */
|
||||||
::-webkit-scrollbar-thumb:hover {
|
::-webkit-scrollbar-thumb:hover {
|
||||||
background: white;
|
background: white;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,16 +1,16 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { t, locales, locale } from "$libs/translations";
|
import { t, locales, locale } from "$libs/translations";
|
||||||
import SelectExpand from "@tea/ui/select-expand/select-expand.svelte";
|
import SelectExpand from "@tea/ui/select-expand/select-expand.svelte";
|
||||||
|
|
||||||
const label = "language";
|
const label = "language";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<SelectExpand
|
<SelectExpand
|
||||||
{label}
|
{label}
|
||||||
bind:value={$locale}
|
bind:value={$locale}
|
||||||
options={$locales.map((value) => ({
|
options={$locales.map((value) => ({
|
||||||
label: $t(`lang.${value}`),
|
label: $t(`lang.${value}`),
|
||||||
value,
|
value,
|
||||||
selected: value === $locale
|
selected: value === $locale
|
||||||
}))}
|
}))}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,83 +1,83 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { shellOpenExternal, submitLogs } from "@native";
|
import { shellOpenExternal, submitLogs } from "@native";
|
||||||
import mouseLeaveDelay from "@tea/ui/lib/mouse-leave-delay";
|
import mouseLeaveDelay from "@tea/ui/lib/mouse-leave-delay";
|
||||||
import UpdateButton from "./update-button.svelte";
|
import UpdateButton from "./update-button.svelte";
|
||||||
import { appUpdateStore } from "$libs/stores";
|
import { appUpdateStore } from "$libs/stores";
|
||||||
const { updateStatus } = appUpdateStore;
|
const { updateStatus } = appUpdateStore;
|
||||||
|
|
||||||
const hidePopup = () => {
|
const hidePopup = () => {
|
||||||
isOpen = false;
|
isOpen = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
const preventDoubleClick = (evt: MouseEvent) => evt.stopPropagation();
|
const preventDoubleClick = (evt: MouseEvent) => evt.stopPropagation();
|
||||||
|
|
||||||
$: isOpen = false;
|
$: isOpen = false;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="relative"
|
class="relative"
|
||||||
use:mouseLeaveDelay={2000}
|
use:mouseLeaveDelay={2000}
|
||||||
on:leave_delay={() => hidePopup()}
|
on:leave_delay={() => hidePopup()}
|
||||||
on:dblclick={preventDoubleClick}
|
on:dblclick={preventDoubleClick}
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
class="border-gray group flex h-[28px] w-[28px] items-center justify-center rounded-sm border hover:bg-[#e1e1e1]"
|
class="border-gray group flex h-[28px] w-[28px] items-center justify-center rounded-sm border hover:bg-[#e1e1e1]"
|
||||||
class:circle-badge={$updateStatus.status === "available" || $updateStatus.status === "ready"}
|
class:circle-badge={$updateStatus.status === "available" || $updateStatus.status === "ready"}
|
||||||
on:click={() => (isOpen = !isOpen)}
|
on:click={() => (isOpen = !isOpen)}
|
||||||
title="settings"
|
title="settings"
|
||||||
>
|
>
|
||||||
<div class="icon-gear text-l text-gray flex group-hover:text-black" />
|
<div class="icon-gear text-l text-gray flex group-hover:text-black" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<nav
|
<nav
|
||||||
class="menu border-gray absolute w-full border bg-black p-2 text-xs transition-all"
|
class="menu border-gray absolute w-full border bg-black p-2 text-xs transition-all"
|
||||||
class:invisible={!isOpen}
|
class:invisible={!isOpen}
|
||||||
class:visible={isOpen}
|
class:visible={isOpen}
|
||||||
>
|
>
|
||||||
<!-- TODO: what is this supposed to do? -->
|
<!-- TODO: what is this supposed to do? -->
|
||||||
<!-- <button
|
<!-- <button
|
||||||
class="hover:bg-gray outline-gray focus:bg-secondary h-8 w-full p-1 text-left outline-1 transition-all hover:bg-opacity-25 hover:outline"
|
class="hover:bg-gray outline-gray focus:bg-secondary h-8 w-full p-1 text-left outline-1 transition-all hover:bg-opacity-25 hover:outline"
|
||||||
>
|
>
|
||||||
language
|
language
|
||||||
</button>
|
</button>
|
||||||
<hr /> -->
|
<hr /> -->
|
||||||
<button
|
<button
|
||||||
class="hover:bg-gray outline-gray h-7 w-full p-1 text-left outline-1 hover:bg-opacity-25 hover:outline"
|
class="hover:bg-gray outline-gray h-7 w-full p-1 text-left outline-1 hover:bg-opacity-25 hover:outline"
|
||||||
on:click={() => shellOpenExternal("https://docs.tea.xyz")}
|
on:click={() => shellOpenExternal("https://docs.tea.xyz")}
|
||||||
>
|
>
|
||||||
docs
|
docs
|
||||||
</button>
|
</button>
|
||||||
<hr />
|
<hr />
|
||||||
<UpdateButton />
|
<UpdateButton />
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
hr {
|
hr {
|
||||||
border: 1px solid #272626;
|
border: 1px solid #272626;
|
||||||
margin: 1px 0;
|
margin: 1px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu {
|
.menu {
|
||||||
top: calc(100% + 4px);
|
top: calc(100% + 4px);
|
||||||
left: calc(50% - 80px);
|
left: calc(50% - 80px);
|
||||||
width: 160px;
|
width: 160px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.circle-badge::after {
|
.circle-badge::after {
|
||||||
content: "1";
|
content: "1";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 14px;
|
width: 14px;
|
||||||
height: 14px;
|
height: 14px;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
color: white;
|
color: white;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: #ff4100;
|
background: #ff4100;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
top: -7px;
|
top: -7px;
|
||||||
right: -7px;
|
right: -7px;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,50 +1,50 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Spinner from "@tea/ui/spinner/spinner.svelte";
|
import Spinner from "@tea/ui/spinner/spinner.svelte";
|
||||||
import { relaunch } from "@native";
|
import { relaunch } from "@native";
|
||||||
import { appUpdateStore } from "$libs/stores";
|
import { appUpdateStore } from "$libs/stores";
|
||||||
|
|
||||||
const { updateStatus } = appUpdateStore;
|
const { updateStatus } = appUpdateStore;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if $updateStatus.status === "up-to-date"}
|
{#if $updateStatus.status === "up-to-date"}
|
||||||
<div
|
<div
|
||||||
class="hover:bg-gray outline-gray flex h-7 w-full items-center justify-between p-1 text-left outline-1 hover:bg-opacity-25 hover:outline"
|
class="hover:bg-gray outline-gray flex h-7 w-full items-center justify-between p-1 text-left outline-1 hover:bg-opacity-25 hover:outline"
|
||||||
>
|
>
|
||||||
<div>up to date</div>
|
<div>up to date</div>
|
||||||
<i class="installed-text icon-check-circle-o flex text-[#00ffd0]" />
|
<i class="installed-text icon-check-circle-o flex text-[#00ffd0]" />
|
||||||
</div>
|
</div>
|
||||||
{:else if $updateStatus.status === "available"}
|
{:else if $updateStatus.status === "available"}
|
||||||
<div
|
<div
|
||||||
class="hover:bg-gray outline-gray flex h-7 w-full items-center justify-between p-1 text-left outline-1 hover:bg-opacity-25 hover:outline"
|
class="hover:bg-gray outline-gray flex h-7 w-full items-center justify-between p-1 text-left outline-1 hover:bg-opacity-25 hover:outline"
|
||||||
>
|
>
|
||||||
<div>fetching update</div>
|
<div>fetching update</div>
|
||||||
<Spinner />
|
<Spinner />
|
||||||
</div>
|
</div>
|
||||||
{:else if $updateStatus.status === "ready"}
|
{:else if $updateStatus.status === "ready"}
|
||||||
<button
|
<button
|
||||||
class="hover:bg-gray outline-gray flex h-7 w-full items-center justify-between p-1 text-left outline-1 hover:bg-opacity-25 hover:outline"
|
class="hover:bg-gray outline-gray flex h-7 w-full items-center justify-between p-1 text-left outline-1 hover:bg-opacity-25 hover:outline"
|
||||||
on:click={relaunch}
|
on:click={relaunch}
|
||||||
>
|
>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<div class="circle-badge mr-2">1</div>
|
<div class="circle-badge mr-2">1</div>
|
||||||
<div>update</div>
|
<div>update</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="rounded-sm bg-white px-2 text-[10px] leading-[12px] text-black">
|
<div class="rounded-sm bg-white px-2 text-[10px] leading-[12px] text-black">
|
||||||
v{$updateStatus.version}
|
v{$updateStatus.version}
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.circle-badge {
|
.circle-badge {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
color: white;
|
color: white;
|
||||||
background: #ff4100;
|
background: #ff4100;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
width: 14px;
|
width: 14px;
|
||||||
height: 14px;
|
height: 14px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,31 +1,31 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import "$appcss";
|
import "$appcss";
|
||||||
export let icon: string;
|
export let icon: string;
|
||||||
export let label: string;
|
export let label: string;
|
||||||
|
|
||||||
export let active = false;
|
export let active = false;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
on:click
|
on:click
|
||||||
class="outline-gray hover:bg-gray box-border flex w-full items-center gap-2 rounded-sm px-1 text-left align-middle text-xs outline-1 transition-all hover:bg-opacity-25 hover:outline"
|
class="outline-gray hover:bg-gray box-border flex w-full items-center gap-2 rounded-sm px-1 text-left align-middle text-xs outline-1 transition-all hover:bg-opacity-25 hover:outline"
|
||||||
class:active
|
class:active
|
||||||
>
|
>
|
||||||
<i class="icon-{icon} mt-1" />
|
<i class="icon-{icon} mt-1" />
|
||||||
<div class="text-sm font-thin">
|
<div class="text-sm font-thin">
|
||||||
{label}
|
{label}
|
||||||
</div>
|
</div>
|
||||||
<slot />
|
<slot />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
button {
|
button {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
height: 37px;
|
height: 37px;
|
||||||
}
|
}
|
||||||
|
|
||||||
button.active {
|
button.active {
|
||||||
background: rgba(148, 148, 148, 0.5);
|
background: rgba(148, 148, 148, 0.5);
|
||||||
border: rgba(148, 148, 148, 1) 1px solid;
|
border: rgba(148, 148, 148, 1) 1px solid;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,105 +1,105 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import "$appcss";
|
import "$appcss";
|
||||||
import { PackageStates, SideMenuOptions } from "$libs/types";
|
import { PackageStates, SideMenuOptions } from "$libs/types";
|
||||||
import { packagesStore } from "$libs/stores";
|
import { packagesStore } from "$libs/stores";
|
||||||
import MenuButton from "./menu-button.svelte";
|
import MenuButton from "./menu-button.svelte";
|
||||||
import { t } from "$libs/translations";
|
import { t } from "$libs/translations";
|
||||||
import { goto } from "$app/navigation";
|
import { goto } from "$app/navigation";
|
||||||
const { packageList } = packagesStore;
|
const { packageList } = packagesStore;
|
||||||
|
|
||||||
export let activeOption: SideMenuOptions;
|
export let activeOption: SideMenuOptions;
|
||||||
|
|
||||||
$: needsUpdateCount = $packageList.filter((p) => p.state === PackageStates.NEEDS_UPDATE).length;
|
$: needsUpdateCount = $packageList.filter((p) => p.state === PackageStates.NEEDS_UPDATE).length;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<aside class="border-gray border border-t-0 border-b-0 border-l-0 p-2">
|
<aside class="border-gray border border-t-0 border-b-0 border-l-0 p-2">
|
||||||
<ul class="flex flex-col gap-1 pt-4 pl-1">
|
<ul class="flex flex-col gap-1 pt-4 pl-1">
|
||||||
<MenuButton
|
<MenuButton
|
||||||
label={$t("tags.discover").toLowerCase()}
|
label={$t("tags.discover").toLowerCase()}
|
||||||
icon="map"
|
icon="map"
|
||||||
active={activeOption === SideMenuOptions.discover}
|
active={activeOption === SideMenuOptions.discover}
|
||||||
on:click={() => goto(`/?tab=${SideMenuOptions.discover}`)}
|
on:click={() => goto(`/?tab=${SideMenuOptions.discover}`)}
|
||||||
/>
|
/>
|
||||||
<hr />
|
<hr />
|
||||||
<MenuButton
|
<MenuButton
|
||||||
label={$t("side-menu-title.all").toLowerCase()}
|
label={$t("side-menu-title.all").toLowerCase()}
|
||||||
icon="grid"
|
icon="grid"
|
||||||
active={activeOption === SideMenuOptions.all}
|
active={activeOption === SideMenuOptions.all}
|
||||||
on:click={() => goto(`/?tab=${SideMenuOptions.all}`)}
|
on:click={() => goto(`/?tab=${SideMenuOptions.all}`)}
|
||||||
/>
|
/>
|
||||||
<hr />
|
<hr />
|
||||||
<MenuButton
|
<MenuButton
|
||||||
label="installed"
|
label="installed"
|
||||||
icon="tea-checkmark"
|
icon="tea-checkmark"
|
||||||
active={activeOption === SideMenuOptions.installed}
|
active={activeOption === SideMenuOptions.installed}
|
||||||
on:click={() => goto(`/?tab=${SideMenuOptions.installed}`)}
|
on:click={() => goto(`/?tab=${SideMenuOptions.installed}`)}
|
||||||
/>
|
/>
|
||||||
<hr />
|
<hr />
|
||||||
<MenuButton
|
<MenuButton
|
||||||
label={$t("tags.installed_updates_available").toLowerCase()}
|
label={$t("tags.installed_updates_available").toLowerCase()}
|
||||||
icon="update"
|
icon="update"
|
||||||
active={activeOption === SideMenuOptions.installed_updates_available}
|
active={activeOption === SideMenuOptions.installed_updates_available}
|
||||||
on:click={() => goto(`/?tab=${SideMenuOptions.installed_updates_available}`)}
|
on:click={() => goto(`/?tab=${SideMenuOptions.installed_updates_available}`)}
|
||||||
>
|
>
|
||||||
{#if needsUpdateCount > 0}
|
{#if needsUpdateCount > 0}
|
||||||
<div class="update-count-badge">{needsUpdateCount}</div>
|
<div class="update-count-badge">{needsUpdateCount}</div>
|
||||||
{/if}
|
{/if}
|
||||||
</MenuButton>
|
</MenuButton>
|
||||||
<hr />
|
<hr />
|
||||||
<MenuButton
|
<MenuButton
|
||||||
label={$t("tags.new_packages").toLowerCase()}
|
label={$t("tags.new_packages").toLowerCase()}
|
||||||
icon="birthday-cake"
|
icon="birthday-cake"
|
||||||
active={activeOption === SideMenuOptions.new_packages}
|
active={activeOption === SideMenuOptions.new_packages}
|
||||||
on:click={() => goto(`/?tab=${SideMenuOptions.new_packages}`)}
|
on:click={() => goto(`/?tab=${SideMenuOptions.new_packages}`)}
|
||||||
/>
|
/>
|
||||||
<!-- <hr />
|
<!-- <hr />
|
||||||
<MenuButton
|
<MenuButton
|
||||||
label={$t("tags.popular").toLowerCase()}
|
label={$t("tags.popular").toLowerCase()}
|
||||||
icon="bar-chart"
|
icon="bar-chart"
|
||||||
active={activeOption === SideMenuOptions.popular}
|
active={activeOption === SideMenuOptions.popular}
|
||||||
on:click={() => goto(`/?tab=${SideMenuOptions.popular}`)}
|
on:click={() => goto(`/?tab=${SideMenuOptions.popular}`)}
|
||||||
/> -->
|
/> -->
|
||||||
<hr />
|
<hr />
|
||||||
<MenuButton
|
<MenuButton
|
||||||
label={$t("tags.recently_updated").toLowerCase()}
|
label={$t("tags.recently_updated").toLowerCase()}
|
||||||
icon="back-in-time"
|
icon="back-in-time"
|
||||||
active={activeOption === SideMenuOptions.recently_updated}
|
active={activeOption === SideMenuOptions.recently_updated}
|
||||||
on:click={() => goto(`/?tab=${SideMenuOptions.recently_updated}`)}
|
on:click={() => goto(`/?tab=${SideMenuOptions.recently_updated}`)}
|
||||||
/>
|
/>
|
||||||
<hr />
|
<hr />
|
||||||
<MenuButton
|
<MenuButton
|
||||||
label={$t("tags.made_by_tea").toLowerCase()}
|
label={$t("tags.made_by_tea").toLowerCase()}
|
||||||
icon="tea-logo-iconasset-1"
|
icon="tea-logo-iconasset-1"
|
||||||
active={activeOption === SideMenuOptions.made_by_tea}
|
active={activeOption === SideMenuOptions.made_by_tea}
|
||||||
on:click={() => goto(`/?tab=${SideMenuOptions.made_by_tea}`)}
|
on:click={() => goto(`/?tab=${SideMenuOptions.made_by_tea}`)}
|
||||||
/>
|
/>
|
||||||
</ul>
|
</ul>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
aside {
|
aside {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0px;
|
left: 0px;
|
||||||
top: 0px;
|
top: 0px;
|
||||||
height: calc(100vh - 48px); /* win.height - title-bar.height */
|
height: calc(100vh - 48px); /* win.height - title-bar.height */
|
||||||
width: 210px;
|
width: 210px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
hr {
|
hr {
|
||||||
border-top: 1px solid #272626;
|
border-top: 1px solid #272626;
|
||||||
}
|
}
|
||||||
|
|
||||||
.update-count-badge {
|
.update-count-badge {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
color: white;
|
color: white;
|
||||||
background: #ff4100;
|
background: #ff4100;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
width: 22px;
|
width: 22px;
|
||||||
height: 22px;
|
height: 22px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
margin-left: 4px;
|
margin-left: 4px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,40 +1,40 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import "$appcss";
|
import "$appcss";
|
||||||
import type { GUIPackage } from "$libs/types";
|
import type { GUIPackage } from "$libs/types";
|
||||||
import type { Package } from "@tea/ui/types";
|
import type { Package } from "@tea/ui/types";
|
||||||
import { PackageStates } from "$libs/types";
|
import { PackageStates } from "$libs/types";
|
||||||
import Preloader from "@tea/ui/Preloader/Preloader.svelte";
|
import Preloader from "@tea/ui/Preloader/Preloader.svelte";
|
||||||
import PackageCard from "$components/packages/package.svelte";
|
import PackageCard from "$components/packages/package.svelte";
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import { packagesStore } from "$libs/stores";
|
import { packagesStore } from "$libs/stores";
|
||||||
|
|
||||||
export let pkg: Package;
|
export let pkg: Package;
|
||||||
|
|
||||||
let packages: GUIPackage[] = [];
|
let packages: GUIPackage[] = [];
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
if (!packages.length) {
|
if (!packages.length) {
|
||||||
const matches = await packagesStore.search(pkg.desc, 4);
|
const matches = await packagesStore.search(pkg.desc, 4);
|
||||||
packages = matches.filter((mp) => mp.full_name !== pkg.full_name).slice(0, 3);
|
packages = matches.filter((mp) => mp.full_name !== pkg.full_name).slice(0, 3);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<header class="border-gray text-primary flex items-center justify-between border bg-black p-4">
|
<header class="border-gray text-primary flex items-center justify-between border bg-black p-4">
|
||||||
<span>MORE LIKE THIS</span>
|
<span>MORE LIKE THIS</span>
|
||||||
</header>
|
</header>
|
||||||
<ul class="grid grid-cols-3 bg-black">
|
<ul class="grid grid-cols-3 bg-black">
|
||||||
{#if packages.length > 0}
|
{#if packages.length > 0}
|
||||||
{#each packages as pkg}
|
{#each packages as pkg}
|
||||||
<div class={pkg.state === PackageStates.INSTALLING ? "animate-pulse" : ""}>
|
<div class={pkg.state === PackageStates.INSTALLING ? "animate-pulse" : ""}>
|
||||||
<PackageCard {pkg} />
|
<PackageCard {pkg} />
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
{:else}
|
{:else}
|
||||||
{#each Array(9) as _}
|
{#each Array(9) as _}
|
||||||
<section class="h-50 border-gray border p-4">
|
<section class="h-50 border-gray border p-4">
|
||||||
<Preloader />
|
<Preloader />
|
||||||
</section>
|
</section>
|
||||||
{/each}
|
{/each}
|
||||||
{/if}
|
{/if}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
|
@ -1,84 +1,84 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { authStore, navStore } from "$libs/stores";
|
import { authStore, navStore } from "$libs/stores";
|
||||||
import { getSession } from "@native";
|
import { getSession } from "@native";
|
||||||
import { baseUrl } from "$libs/v1-client";
|
import { baseUrl } from "$libs/v1-client";
|
||||||
import { shellOpenExternal } from "@native";
|
import { shellOpenExternal } from "@native";
|
||||||
import mouseLeaveDelay from "@tea/ui/lib/mouse-leave-delay";
|
import mouseLeaveDelay from "@tea/ui/lib/mouse-leave-delay";
|
||||||
const { user } = authStore;
|
const { user } = authStore;
|
||||||
|
|
||||||
$: authenticating = false;
|
$: authenticating = false;
|
||||||
|
|
||||||
$: isLogoutOpen = false;
|
$: isLogoutOpen = false;
|
||||||
|
|
||||||
const openGithub = async () => {
|
const openGithub = async () => {
|
||||||
if (!authenticating) {
|
if (!authenticating) {
|
||||||
authenticating = true;
|
authenticating = true;
|
||||||
try {
|
try {
|
||||||
const session = await getSession();
|
const session = await getSession();
|
||||||
|
|
||||||
if (session && session.device_id) {
|
if (session && session.device_id) {
|
||||||
shellOpenExternal(`${baseUrl}/auth/user?device_id=${session.device_id}`);
|
shellOpenExternal(`${baseUrl}/auth/user?device_id=${session.device_id}`);
|
||||||
authStore.pollSession();
|
authStore.pollSession();
|
||||||
} else {
|
} else {
|
||||||
throw new Error("possible no internet connection");
|
throw new Error("possible no internet connection");
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
} finally {
|
} finally {
|
||||||
authenticating = false;
|
authenticating = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
isLogoutOpen = false;
|
isLogoutOpen = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
const logout = () => authStore.clearSession();
|
const logout = () => authStore.clearSession();
|
||||||
|
|
||||||
const preventDoubleClick = (evt: MouseEvent) => evt.stopPropagation();
|
const preventDoubleClick = (evt: MouseEvent) => evt.stopPropagation();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if $user}
|
{#if $user}
|
||||||
<div class="relative" use:mouseLeaveDelay={2000} on:leave_delay={() => (isLogoutOpen = false)}>
|
<div class="relative" use:mouseLeaveDelay={2000} on:leave_delay={() => (isLogoutOpen = false)}>
|
||||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
<section
|
<section
|
||||||
class="border-gray text-gray group flex h-[28px] min-w-[120px] max-w-[160px] items-center justify-between rounded-sm border bg-black pl-2 text-sm transition-all
|
class="border-gray text-gray group flex h-[28px] min-w-[120px] max-w-[160px] items-center justify-between rounded-sm border bg-black pl-2 text-sm transition-all
|
||||||
hover:bg-[#e1e1e1] hover:text-black"
|
hover:bg-[#e1e1e1] hover:text-black"
|
||||||
on:click={() => (isLogoutOpen = !isLogoutOpen)}
|
on:click={() => (isLogoutOpen = !isLogoutOpen)}
|
||||||
on:dblclick={preventDoubleClick}
|
on:dblclick={preventDoubleClick}
|
||||||
>
|
>
|
||||||
<div class="text-gray line-clamp-1 mr-1 group-hover:text-black">@{$user?.login}</div>
|
<div class="text-gray line-clamp-1 mr-1 group-hover:text-black">@{$user?.login}</div>
|
||||||
<img
|
<img
|
||||||
id="avatar"
|
id="avatar"
|
||||||
class="flex rounded-sm"
|
class="flex rounded-sm"
|
||||||
src={$user?.avatar_url || "/images/bored-ape.png"}
|
src={$user?.avatar_url || "/images/bored-ape.png"}
|
||||||
alt="profile"
|
alt="profile"
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
<div
|
<div
|
||||||
class="border-gray text-gray group absolute z-50 mt-1 flex h-[28px] w-[120px] items-center justify-between rounded-sm border bg-black pl-3 text-sm transition-all
|
class="border-gray text-gray group absolute z-50 mt-1 flex h-[28px] w-[120px] items-center justify-between rounded-sm border bg-black pl-3 text-sm transition-all
|
||||||
hover:bg-[#e1e1e1] hover:text-black"
|
hover:bg-[#e1e1e1] hover:text-black"
|
||||||
class:invisible={!isLogoutOpen}
|
class:invisible={!isLogoutOpen}
|
||||||
class:visible={isLogoutOpen}
|
class:visible={isLogoutOpen}
|
||||||
on:click={logout}
|
on:click={logout}
|
||||||
>
|
>
|
||||||
log out
|
log out
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<button
|
<button
|
||||||
class="border-gray text-gray h-[28px] w-[120px] rounded-sm border px-1 text-sm transition-all hover:bg-[#e1e1e1] hover:text-black"
|
class="border-gray text-gray h-[28px] w-[120px] rounded-sm border px-1 text-sm transition-all hover:bg-[#e1e1e1] hover:text-black"
|
||||||
class:animate-pulse={authenticating}
|
class:animate-pulse={authenticating}
|
||||||
on:click={openGithub}
|
on:click={openGithub}
|
||||||
on:dblclick={preventDoubleClick}
|
on:dblclick={preventDoubleClick}
|
||||||
>
|
>
|
||||||
log in
|
log in
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
#avatar {
|
#avatar {
|
||||||
padding: 1px;
|
padding: 1px;
|
||||||
height: 26px !important;
|
height: 26px !important;
|
||||||
width: 26px !important;
|
width: 26px !important;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,18 +1,18 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { shellOpenExternal, submitLogs } from "@native";
|
import { shellOpenExternal, submitLogs } from "@native";
|
||||||
import LoginButton from "./login-button.svelte";
|
import LoginButton from "./login-button.svelte";
|
||||||
import ButtonIcon from "@tea/ui/button-icon/button-icon.svelte";
|
import ButtonIcon from "@tea/ui/button-icon/button-icon.svelte";
|
||||||
import SettingsMenu from "$components/settings-menu/settings-menu.svelte";
|
import SettingsMenu from "$components/settings-menu/settings-menu.svelte";
|
||||||
|
|
||||||
const submitBugReport = async () => {
|
const submitBugReport = async () => {
|
||||||
const logId = await submitLogs();
|
const logId = await submitLogs();
|
||||||
const bugFormUrl = `https://airtable.com/shravDxWeNwwpPkFV?prefill_log_id=${logId}&hide_log_id=true`;
|
const bugFormUrl = `https://airtable.com/shravDxWeNwwpPkFV?prefill_log_id=${logId}&hide_log_id=true`;
|
||||||
shellOpenExternal(bugFormUrl);
|
shellOpenExternal(bugFormUrl);
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="mr-1 flex h-full items-center justify-end gap-2 p-2">
|
<div class="mr-1 flex h-full items-center justify-end gap-2 p-2">
|
||||||
<ButtonIcon icon="bug" helpText="report feedback" on:click={() => submitBugReport()} />
|
<ButtonIcon icon="bug" helpText="report feedback" on:click={() => submitBugReport()} />
|
||||||
<SettingsMenu />
|
<SettingsMenu />
|
||||||
<LoginButton />
|
<LoginButton />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,96 +1,102 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { searchStore } from "$libs/stores";
|
import { searchStore } from "$libs/stores";
|
||||||
import SearchInput from "@tea/ui/search-input/search-input.svelte";
|
import SearchInput from "@tea/ui/search-input/search-input.svelte";
|
||||||
import { navStore } from "$libs/stores";
|
import { navStore } from "$libs/stores";
|
||||||
import { t } from "$libs/translations";
|
import { t } from "$libs/translations";
|
||||||
|
|
||||||
import TopBarMenu from "./top-bar-menu.svelte";
|
import TopBarMenu from "./top-bar-menu.svelte";
|
||||||
import { topbarDoubleClick } from "$libs/native-electron";
|
import { topbarDoubleClick } from "$libs/native-electron";
|
||||||
|
|
||||||
let { nextPath, prevPath } = navStore;
|
let { nextPath, prevPath } = navStore;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<header
|
<header
|
||||||
class="border-gray relative z-20 flex h-12 w-full items-center justify-between border border-x-0 border-t-0 pr-2"
|
class="border-gray relative z-20 flex h-12 w-full items-center justify-between border border-x-0 border-t-0 pr-2"
|
||||||
style="-webkit-app-region: drag"
|
style="-webkit-app-region: drag"
|
||||||
on:dblclick={topbarDoubleClick}
|
on:dblclick={topbarDoubleClick}
|
||||||
>
|
>
|
||||||
<ul class="text-gray flex h-10 gap-1 pl-20 align-middle items-center leading-10">
|
<ul class="text-gray flex h-10 items-center gap-1 pl-20 align-middle leading-10">
|
||||||
<a href="/?tab=all">
|
<a href="/?tab=all">
|
||||||
<div class="home-btn w-12 text-center text-2xl">
|
<div class="home-btn w-12 text-center text-2xl">
|
||||||
<i class="icon-tea-logo-iconasset-1" />
|
<i class="icon-tea-logo-iconasset-1" />
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
<p class="text-gray px-2">beta</p>
|
<p class="text-gray px-2">beta</p>
|
||||||
<button on:click={navStore.back} class:active={$prevPath} class="pt-1 px-2 h-[28px] text-xs rounded-sm transition-all opacity-50 hover:bg-gray hover:text-black" title="go back"
|
<button
|
||||||
><i class="icon-arrow-left" /></button
|
on:click={navStore.back}
|
||||||
>
|
class:active={$prevPath}
|
||||||
<button on:click={navStore.next} class:active={$nextPath} class="pt-1 px-2 h-[28px] text-xs rounded-sm transition-all opacity-50 hover:bg-gray hover:text-black" title="go forward"
|
class="hover:bg-gray h-[28px] rounded-sm px-2 pt-1 text-xs opacity-50 transition-all hover:text-black"
|
||||||
><i class="icon-arrow-right" /></button
|
title="go back"><i class="icon-arrow-left" /></button
|
||||||
>
|
>
|
||||||
</ul>
|
<button
|
||||||
<div class="relative w-1/3 px-2">
|
on:click={navStore.next}
|
||||||
<SearchInput
|
class:active={$nextPath}
|
||||||
class="border-gray h-9 w-full rounded-sm border"
|
class="hover:bg-gray h-[28px] rounded-sm px-2 pt-1 text-xs opacity-50 transition-all hover:text-black"
|
||||||
size="small"
|
title="go forward"><i class="icon-arrow-right" /></button
|
||||||
placeholder={$t("store-search-placeholder")}
|
>
|
||||||
onFocus={() => {
|
</ul>
|
||||||
searchStore.searching.set(true);
|
<div class="relative w-1/3 px-2">
|
||||||
}}
|
<SearchInput
|
||||||
readonly={true}
|
class="border-gray h-9 w-full rounded-sm border"
|
||||||
/>
|
size="small"
|
||||||
|
placeholder={$t("store-search-placeholder")}
|
||||||
|
onFocus={() => {
|
||||||
|
searchStore.searching.set(true);
|
||||||
|
}}
|
||||||
|
readonly={true}
|
||||||
|
/>
|
||||||
|
|
||||||
<kbd
|
<kbd
|
||||||
class="bg-gray pointer-events-none absolute top-0 right-3 mt-1 flex items-center rounded-sm px-2 text-white opacity-50"
|
class="bg-gray pointer-events-none absolute top-0 right-3 mt-1 flex items-center rounded-sm px-2 text-white opacity-50"
|
||||||
style="letter-spacing: 0.5pt"
|
style="letter-spacing: 0.5pt"
|
||||||
>
|
>
|
||||||
<span class="text-lg">⌘</span>
|
<span class="text-lg">⌘</span>
|
||||||
<span class="text-xs" style="font-size: smaller">K</span>
|
<span class="text-xs" style="font-size: smaller">K</span>
|
||||||
</kbd>
|
</kbd>
|
||||||
</div>
|
</div>
|
||||||
<TopBarMenu />
|
<TopBarMenu />
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@tailwind base;
|
@tailwind base;
|
||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
header {
|
header {
|
||||||
background: rgba(26, 26, 26, 0.9);
|
background: rgba(26, 26, 26, 0.9);
|
||||||
backdrop-filter: blur(2px);
|
backdrop-filter: blur(2px);
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
.home-btn {
|
.home-btn {
|
||||||
height: 46px;
|
height: 46px;
|
||||||
width: 46px;
|
width: 46px;
|
||||||
line-height: 46px;
|
line-height: 46px;
|
||||||
padding-left: 3px;
|
padding-left: 3px;
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
background-position: center center;
|
background-position: center center;
|
||||||
background-image: url("/images/gradient-bg.png");
|
background-image: url("/images/gradient-bg.png");
|
||||||
color: #222222;
|
color: #222222;
|
||||||
}
|
}
|
||||||
|
|
||||||
.home-btn:hover {
|
.home-btn:hover {
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.home-btn:active {
|
.home-btn:active {
|
||||||
color: #222222;
|
color: #222222;
|
||||||
border: 2px solid #222222
|
border: 2px solid #222222;
|
||||||
}
|
}
|
||||||
|
|
||||||
p {
|
p {
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
}
|
}
|
||||||
ul button {
|
ul button {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
ul button.active {
|
ul button.active {
|
||||||
color: white;
|
color: white;
|
||||||
pointer-events: all;
|
pointer-events: all;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,37 +1,40 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Button from "@tea/ui/button/button.svelte";
|
import Button from "@tea/ui/button/button.svelte";
|
||||||
import clickOutside from "@tea/ui/lib/clickOutside";
|
import clickOutside from "@tea/ui/lib/clickOutside";
|
||||||
|
|
||||||
import { authStore } from "$libs/stores"
|
import { authStore } from "$libs/stores";
|
||||||
|
|
||||||
const close = () => {
|
const close = () => {
|
||||||
authStore.updateSession({ hide_welcome: true });
|
authStore.updateSession({ hide_welcome: true });
|
||||||
}
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<section class="fixed z-50 top-0 left-0 flex items-center justify-center">
|
<section class="fixed top-0 left-0 z-50 flex items-center justify-center">
|
||||||
<aside class="relative" use:clickOutside on:click_outside={() => close()}>
|
<aside class="relative" use:clickOutside on:click_outside={() => close()}>
|
||||||
<article class="flex margin-auto p-2 border border-gray rounded-md">
|
<article class="margin-auto border-gray flex rounded-md border p-2">
|
||||||
<figure>
|
<figure>
|
||||||
<img class="object-contain" src="/images/welcome-bg.png" alt="welcome"/>
|
<img class="object-contain" src="/images/welcome-bg.png" alt="welcome" />
|
||||||
</figure>
|
</figure>
|
||||||
<div class="flex-grow mt-20 px-12 relative">
|
<div class="relative mt-20 flex-grow px-12">
|
||||||
<h1 class="text-primary text-4xl mb-4">Welcome to the tea app!</h1>
|
<h1 class="text-primary mb-4 text-4xl">Welcome to the tea app!</h1>
|
||||||
<p class="font-inter mb-4">This app is your gateway into the world of open-source software. Easily explore and manage packages with a click of a button. This app will notify you of any available software updates to ensure you’re safe and secure. Under the hood is the powerful tea cli.</p>
|
<p class="font-inter mb-4">
|
||||||
|
This app is your gateway into the world of open-source software. Easily explore and manage
|
||||||
|
packages with a click of a button. This app will notify you of any available software
|
||||||
|
updates to ensure you’re safe and secure. Under the hood is the powerful tea cli.
|
||||||
|
</p>
|
||||||
|
|
||||||
<Button type="plain" color="secondary" class="w-7/12"
|
<Button type="plain" color="secondary" class="w-7/12" onClick={() => close()}>
|
||||||
onClick={() => close()}
|
|
||||||
>
|
|
||||||
START EXPLORING
|
START EXPLORING
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
<button class="icon-tea-x-btn absolute text-gray top-5 right-5"
|
<button
|
||||||
|
class="icon-tea-x-btn text-gray absolute top-5 right-5"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
close()
|
close();
|
||||||
}}
|
}}
|
||||||
></button>
|
/>
|
||||||
</aside>
|
</aside>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
@ -39,13 +42,13 @@
|
||||||
section {
|
section {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
background: rgba(0,0,0, 0.5);
|
background: rgba(0, 0, 0, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
article {
|
article {
|
||||||
height: 472px;
|
height: 472px;
|
||||||
width: 725px;
|
width: 725px;
|
||||||
background: rgba(0,0,0, 0.8);
|
background: rgba(0, 0, 0, 0.8);
|
||||||
}
|
}
|
||||||
|
|
||||||
figure {
|
figure {
|
||||||
|
|
|
@ -3,47 +3,47 @@ import * as pub from "$env/static/public";
|
||||||
import { getSession } from "@native";
|
import { getSession } from "@native";
|
||||||
|
|
||||||
type DefaultMixpanelProps = {
|
type DefaultMixpanelProps = {
|
||||||
device_id: string;
|
device_id: string;
|
||||||
uid: string;
|
uid: string;
|
||||||
distinct_id: string;
|
distinct_id: string;
|
||||||
locale: string;
|
locale: string;
|
||||||
gui_version: string;
|
gui_version: string;
|
||||||
};
|
};
|
||||||
let mixpanelDefaultData: DefaultMixpanelProps;
|
let mixpanelDefaultData: DefaultMixpanelProps;
|
||||||
const getLocalSession = async (): Promise<DefaultMixpanelProps> => {
|
const getLocalSession = async (): Promise<DefaultMixpanelProps> => {
|
||||||
if (mixpanelDefaultData) return mixpanelDefaultData;
|
if (mixpanelDefaultData) return mixpanelDefaultData;
|
||||||
const session = await getSession();
|
const session = await getSession();
|
||||||
mixpanelDefaultData = {
|
mixpanelDefaultData = {
|
||||||
device_id: session?.device_id || "unregistered",
|
device_id: session?.device_id || "unregistered",
|
||||||
uid: session?.user?.developer_id || "unauthenticated",
|
uid: session?.user?.developer_id || "unauthenticated",
|
||||||
distinct_id: session?.device_id || "unregistered",
|
distinct_id: session?.device_id || "unregistered",
|
||||||
locale: session?.locale || "unknown",
|
locale: session?.locale || "unknown",
|
||||||
gui_version: pub.PUBLIC_VERSION
|
gui_version: pub.PUBLIC_VERSION
|
||||||
};
|
};
|
||||||
return mixpanelDefaultData;
|
return mixpanelDefaultData;
|
||||||
};
|
};
|
||||||
|
|
||||||
mixpanel.init(pub.PUBLIC_MIXPANEL_TOKEN, { debug: true });
|
mixpanel.init(pub.PUBLIC_MIXPANEL_TOKEN, { debug: true });
|
||||||
|
|
||||||
enum AnalyticsAction {
|
enum AnalyticsAction {
|
||||||
install = "INSTALL_ACTION",
|
install = "INSTALL_ACTION",
|
||||||
install_failed = "INSTALL_ACTION_FAILED",
|
install_failed = "INSTALL_ACTION_FAILED",
|
||||||
search = "SEARCH_ACTION",
|
search = "SEARCH_ACTION",
|
||||||
search_failed = "SEARCH_ACTION_FAILED"
|
search_failed = "SEARCH_ACTION_FAILED"
|
||||||
}
|
}
|
||||||
|
|
||||||
const trackAction = (action: AnalyticsAction, data?: { [key: string]: any }) => {
|
const trackAction = (action: AnalyticsAction, data?: { [key: string]: any }) => {
|
||||||
getLocalSession()
|
getLocalSession()
|
||||||
.then((props) => {
|
.then((props) => {
|
||||||
mixpanel.track(action, {
|
mixpanel.track(action, {
|
||||||
...(data || {}),
|
...(data || {}),
|
||||||
...props
|
...props
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
// TODO: log remote error stream
|
// TODO: log remote error stream
|
||||||
console.error(error);
|
console.error(error);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -52,7 +52,7 @@ const trackAction = (action: AnalyticsAction, data?: { [key: string]: any }) =>
|
||||||
* @returns void
|
* @returns void
|
||||||
*/
|
*/
|
||||||
export const trackInstall = (packageFullname: string) =>
|
export const trackInstall = (packageFullname: string) =>
|
||||||
trackAction(AnalyticsAction.install, { pkg: packageFullname });
|
trackAction(AnalyticsAction.install, { pkg: packageFullname });
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* save failed installation event to mixpanel
|
* save failed installation event to mixpanel
|
||||||
|
@ -60,21 +60,21 @@ export const trackInstall = (packageFullname: string) =>
|
||||||
* @param error error message
|
* @param error error message
|
||||||
*/
|
*/
|
||||||
export const trackInstallFailed = (packageFullname: string, error: string) => {
|
export const trackInstallFailed = (packageFullname: string, error: string) => {
|
||||||
trackAction(AnalyticsAction.install_failed, {
|
trackAction(AnalyticsAction.install_failed, {
|
||||||
pkg: packageFullname,
|
pkg: packageFullname,
|
||||||
error
|
error
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const trackSearch = (search_term: string, result_count: number) => {
|
export const trackSearch = (search_term: string, result_count: number) => {
|
||||||
if (result_count > 0) {
|
if (result_count > 0) {
|
||||||
trackAction(AnalyticsAction.search, {
|
trackAction(AnalyticsAction.search, {
|
||||||
search_term,
|
search_term,
|
||||||
result_count
|
result_count
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
trackAction(AnalyticsAction.search_failed, {
|
trackAction(AnalyticsAction.search_failed, {
|
||||||
search_term
|
search_term
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -2,57 +2,57 @@ import axios from "axios";
|
||||||
import type { Contributor, Package } from "@tea/ui/types";
|
import type { Contributor, Package } from "@tea/ui/types";
|
||||||
import yaml from "js-yaml";
|
import yaml from "js-yaml";
|
||||||
export async function getPackageYaml(pkgYamlUrl: string) {
|
export async function getPackageYaml(pkgYamlUrl: string) {
|
||||||
const url = pkgYamlUrl.replace("/github.com", "/raw.githubusercontent.com").replace("/blob", "");
|
const url = pkgYamlUrl.replace("/github.com", "/raw.githubusercontent.com").replace("/blob", "");
|
||||||
|
|
||||||
const { data: rawYaml } = await axios.get(url);
|
const { data: rawYaml } = await axios.get(url);
|
||||||
|
|
||||||
const data = await yaml.load(rawYaml);
|
const data = await yaml.load(rawYaml);
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getReadme(
|
export async function getReadme(
|
||||||
owner: string,
|
owner: string,
|
||||||
repo: string
|
repo: string
|
||||||
): Promise<{ data: string; type: "md" | "rst" }> {
|
): Promise<{ data: string; type: "md" | "rst" }> {
|
||||||
let type: "md" | "rst" = "md";
|
let type: "md" | "rst" = "md";
|
||||||
let data = "";
|
let data = "";
|
||||||
const req = await axios.get(`https://api.github.com/repos/${owner}/${repo}/readme`);
|
const req = await axios.get(`https://api.github.com/repos/${owner}/${repo}/readme`);
|
||||||
|
|
||||||
if (req.data?.download_url) {
|
if (req.data?.download_url) {
|
||||||
type = req.data.name.endsWith(".rst") ? "rst" : "md";
|
type = req.data.name.endsWith(".rst") ? "rst" : "md";
|
||||||
const reqDl = await axios.get(req.data.download_url);
|
const reqDl = await axios.get(req.data.download_url);
|
||||||
data = reqDl.data;
|
data = reqDl.data;
|
||||||
}
|
}
|
||||||
return { data, type };
|
return { data, type };
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getContributors(owner: string, repo: string): Promise<Contributor[]> {
|
export async function getContributors(owner: string, repo: string): Promise<Contributor[]> {
|
||||||
// maintainer/repo
|
// maintainer/repo
|
||||||
let contributors: Contributor[] = [];
|
let contributors: Contributor[] = [];
|
||||||
const req = await axios.get(`https://api.github.com/repos/${owner}/${repo}/contributors`);
|
const req = await axios.get(`https://api.github.com/repos/${owner}/${repo}/contributors`);
|
||||||
if (req.data) {
|
if (req.data) {
|
||||||
contributors = req.data.map((c: Contributor & { id: number }) => ({
|
contributors = req.data.map((c: Contributor & { id: number }) => ({
|
||||||
login: c.login,
|
login: c.login,
|
||||||
avatar_url: c.avatar_url,
|
avatar_url: c.avatar_url,
|
||||||
name: c.name || "",
|
name: c.name || "",
|
||||||
github_id: c.id,
|
github_id: c.id,
|
||||||
contributions: c.contributions
|
contributions: c.contributions
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
return contributors;
|
return contributors;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getRepoAsPackage(owner: string, repo: string): Promise<Partial<Package>> {
|
export async function getRepoAsPackage(owner: string, repo: string): Promise<Partial<Package>> {
|
||||||
const req = await axios.get(`https://api.github.com/repos/${owner}/${repo}`);
|
const req = await axios.get(`https://api.github.com/repos/${owner}/${repo}`);
|
||||||
const pkg: Partial<Package> = {};
|
const pkg: Partial<Package> = {};
|
||||||
if (req.data) {
|
if (req.data) {
|
||||||
pkg.license = req.data?.license?.name || "";
|
pkg.license = req.data?.license?.name || "";
|
||||||
}
|
}
|
||||||
return pkg;
|
return pkg;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const trimGithubSlug = (slug: string): string => {
|
export const trimGithubSlug = (slug: string): string => {
|
||||||
const [owner, repo] = slug.split("/");
|
const [owner, repo] = slug.split("/");
|
||||||
return [owner, repo].join("/");
|
return [owner, repo].join("/");
|
||||||
};
|
};
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { captureException } from "./sentry";
|
||||||
// TODO: figure out how to detect if pkaged
|
// TODO: figure out how to detect if pkaged
|
||||||
const oldError = log.error;
|
const oldError = log.error;
|
||||||
log.error = (...params: any[]) => {
|
log.error = (...params: any[]) => {
|
||||||
oldError(params);
|
oldError(params);
|
||||||
captureException(params[0].message);
|
captureException(params[0].message);
|
||||||
};
|
};
|
||||||
export default log;
|
export default log;
|
||||||
|
|
|
@ -13,12 +13,12 @@
|
||||||
|
|
||||||
import type { Package, Review, AirtablePost, Bottle } from "@tea/ui/types";
|
import type { Package, Review, AirtablePost, Bottle } from "@tea/ui/types";
|
||||||
import {
|
import {
|
||||||
type GUIPackage,
|
type GUIPackage,
|
||||||
type DeviceAuth,
|
type DeviceAuth,
|
||||||
type Session,
|
type Session,
|
||||||
AuthStatus,
|
AuthStatus,
|
||||||
type Packages,
|
type Packages,
|
||||||
type AutoUpdateStatus
|
type AutoUpdateStatus
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
import * as mock from "./native-mock";
|
import * as mock from "./native-mock";
|
||||||
|
@ -31,270 +31,270 @@ import log from "./logger";
|
||||||
const { ipcRenderer, shell } = window.require("electron");
|
const { ipcRenderer, shell } = window.require("electron");
|
||||||
|
|
||||||
export async function getDistPackages(): Promise<Package[]> {
|
export async function getDistPackages(): Promise<Package[]> {
|
||||||
try {
|
try {
|
||||||
return withRetry(async () => {
|
return withRetry(async () => {
|
||||||
const req = await axios.get<Package[]>(
|
const req = await axios.get<Package[]>(
|
||||||
"https://s3.amazonaws.com/preview.gui.tea.xyz/packages.json"
|
"https://s3.amazonaws.com/preview.gui.tea.xyz/packages.json"
|
||||||
);
|
);
|
||||||
log.info("packages received:", req.data.length);
|
log.info("packages received:", req.data.length);
|
||||||
return req.data;
|
return req.data;
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error("getDistPackagesList:", error);
|
log.error("getDistPackagesList:", error);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getInstalledPackages(): Promise<InstalledPackage[]> {
|
export async function getInstalledPackages(): Promise<InstalledPackage[]> {
|
||||||
let pkgs: InstalledPackage[] = [];
|
let pkgs: InstalledPackage[] = [];
|
||||||
try {
|
try {
|
||||||
log.info("getting installed packages");
|
log.info("getting installed packages");
|
||||||
pkgs = (await ipcRenderer.invoke("get-installed-packages")) as InstalledPackage[];
|
pkgs = (await ipcRenderer.invoke("get-installed-packages")) as InstalledPackage[];
|
||||||
log.info("got installed packages:", pkgs.length);
|
log.info("got installed packages:", pkgs.length);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error(error);
|
log.error(error);
|
||||||
}
|
}
|
||||||
return pkgs;
|
return pkgs;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getPackages(): Promise<GUIPackage[]> {
|
export async function getPackages(): Promise<GUIPackage[]> {
|
||||||
const [packages, installedPackages] = await Promise.all([
|
const [packages, installedPackages] = await Promise.all([
|
||||||
getDistPackages(),
|
getDistPackages(),
|
||||||
ipcRenderer.invoke("get-installed-packages") as InstalledPackage[]
|
ipcRenderer.invoke("get-installed-packages") as InstalledPackage[]
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// NOTE: its not ideal to get bottles or set package states here maybe do it async in the package store init
|
// NOTE: its not ideal to get bottles or set package states here maybe do it async in the package store init
|
||||||
// --- it has noticeable slowness
|
// --- it has noticeable slowness
|
||||||
log.info(`native: installed ${installedPackages.length} out of ${(packages || []).length}`);
|
log.info(`native: installed ${installedPackages.length} out of ${(packages || []).length}`);
|
||||||
return (packages || []).map((pkg) => {
|
return (packages || []).map((pkg) => {
|
||||||
const installedPkg = installedPackages.find((p) => p.full_name === pkg.full_name);
|
const installedPkg = installedPackages.find((p) => p.full_name === pkg.full_name);
|
||||||
return {
|
return {
|
||||||
...pkg,
|
...pkg,
|
||||||
state: installedPkg ? PackageStates.INSTALLED : PackageStates.AVAILABLE,
|
state: installedPkg ? PackageStates.INSTALLED : PackageStates.AVAILABLE,
|
||||||
installed_versions: installedPkg?.installed_versions || []
|
installed_versions: installedPkg?.installed_versions || []
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getFeaturedPackages(): Promise<Package[]> {
|
export async function getFeaturedPackages(): Promise<Package[]> {
|
||||||
const packages = await mock.getFeaturedPackages();
|
const packages = await mock.getFeaturedPackages();
|
||||||
return packages;
|
return packages;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getPackageReviews(full_name: string): Promise<Review[]> {
|
export async function getPackageReviews(full_name: string): Promise<Review[]> {
|
||||||
console.log(`getting reviews for ${full_name}`);
|
console.log(`getting reviews for ${full_name}`);
|
||||||
const reviews: Review[] =
|
const reviews: Review[] =
|
||||||
(await apiGet<Review[]>(`packages/${full_name.replaceAll("/", ":")}/reviews`)) ?? [];
|
(await apiGet<Review[]>(`packages/${full_name.replaceAll("/", ":")}/reviews`)) ?? [];
|
||||||
|
|
||||||
return reviews;
|
return reviews;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function installPackage(pkg: GUIPackage, version?: string) {
|
export async function installPackage(pkg: GUIPackage, version?: string) {
|
||||||
const latestVersion = pkg.version;
|
const latestVersion = pkg.version;
|
||||||
const specificVersion = version || latestVersion;
|
const specificVersion = version || latestVersion;
|
||||||
|
|
||||||
log.info(`installing package: ${pkg.name} version: ${specificVersion}`);
|
log.info(`installing package: ${pkg.name} version: ${specificVersion}`);
|
||||||
|
|
||||||
const res = await ipcRenderer.invoke("install-package", {
|
const res = await ipcRenderer.invoke("install-package", {
|
||||||
full_name: pkg.full_name,
|
full_name: pkg.full_name,
|
||||||
version: specificVersion
|
version: specificVersion
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res instanceof Error) {
|
if (res instanceof Error) {
|
||||||
throw res;
|
throw res;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function syncPantry() {
|
export async function syncPantry() {
|
||||||
const res = await ipcRenderer.invoke("sync-pantry");
|
const res = await ipcRenderer.invoke("sync-pantry");
|
||||||
if (res instanceof Error) {
|
if (res instanceof Error) {
|
||||||
throw res;
|
throw res;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getTopPackages(): Promise<GUIPackage[]> {
|
export async function getTopPackages(): Promise<GUIPackage[]> {
|
||||||
const packages = await mock.getTopPackages();
|
const packages = await mock.getTopPackages();
|
||||||
return packages;
|
return packages;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getAllPosts(tag?: string): Promise<AirtablePost[]> {
|
export async function getAllPosts(tag?: string): Promise<AirtablePost[]> {
|
||||||
// add filter here someday: tag = news | course
|
// add filter here someday: tag = news | course
|
||||||
try {
|
try {
|
||||||
const queryParams = {
|
const queryParams = {
|
||||||
...(tag ? { tag } : {}),
|
...(tag ? { tag } : {}),
|
||||||
nocache: "true"
|
nocache: "true"
|
||||||
};
|
};
|
||||||
const posts = await apiGet<AirtablePost[]>("posts", queryParams);
|
const posts = await apiGet<AirtablePost[]>("posts", queryParams);
|
||||||
return posts || [];
|
return posts || [];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error(error);
|
log.error(error);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getDeviceAuth(deviceId: string): Promise<DeviceAuth> {
|
export async function getDeviceAuth(deviceId: string): Promise<DeviceAuth> {
|
||||||
let auth: DeviceAuth = {
|
let auth: DeviceAuth = {
|
||||||
status: AuthStatus.UNKNOWN,
|
status: AuthStatus.UNKNOWN,
|
||||||
key: ""
|
key: ""
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
const data = await apiGet<DeviceAuth>(`/auth/device/${deviceId}`);
|
const data = await apiGet<DeviceAuth>(`/auth/device/${deviceId}`);
|
||||||
if (data) auth = data;
|
if (data) auth = data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error(error);
|
log.error(error);
|
||||||
auth = await getDeviceAuth(deviceId);
|
auth = await getDeviceAuth(deviceId);
|
||||||
}
|
}
|
||||||
return auth;
|
return auth;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getPackageBottles(packageName: string): Promise<Bottle[]> {
|
export async function getPackageBottles(packageName: string): Promise<Bottle[]> {
|
||||||
try {
|
try {
|
||||||
return withRetry(async () => {
|
return withRetry(async () => {
|
||||||
const pkg = await apiGet<Package>(`packages/${packageName.replaceAll("/", ":")}`);
|
const pkg = await apiGet<Package>(`packages/${packageName.replaceAll("/", ":")}`);
|
||||||
log.info(`got ${pkg?.bottles?.length || 0} bottles for ${packageName}`);
|
log.info(`got ${pkg?.bottles?.length || 0} bottles for ${packageName}`);
|
||||||
return (pkg && pkg.bottles) || [];
|
return (pkg && pkg.bottles) || [];
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error("getPackageBottles:", error);
|
log.error("getPackageBottles:", error);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getPackage(packageName: string): Promise<Partial<Package>> {
|
export async function getPackage(packageName: string): Promise<Partial<Package>> {
|
||||||
try {
|
try {
|
||||||
return await withRetry(async () => {
|
return await withRetry(async () => {
|
||||||
const data = await apiGet<Partial<Package>>(`packages/${packageName.replaceAll("/", ":")}`);
|
const data = await apiGet<Partial<Package>>(`packages/${packageName.replaceAll("/", ":")}`);
|
||||||
if (data) {
|
if (data) {
|
||||||
return data;
|
return data;
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`package:${packageName} not found`);
|
throw new Error(`package:${packageName} not found`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error("getPackage:", error);
|
log.error("getPackage:", error);
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getSession = async (): Promise<Session | null> => {
|
export const getSession = async (): Promise<Session | null> => {
|
||||||
try {
|
try {
|
||||||
const session = await ipcRenderer.invoke("get-session");
|
const session = await ipcRenderer.invoke("get-session");
|
||||||
if (!session) throw new Error("no session found");
|
if (!session) throw new Error("no session found");
|
||||||
return session;
|
return session;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error(error);
|
log.error(error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const updateSession = async (session: Partial<Session>) => {
|
export const updateSession = async (session: Partial<Session>) => {
|
||||||
try {
|
try {
|
||||||
await ipcRenderer.invoke("update-session", session);
|
await ipcRenderer.invoke("update-session", session);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error(error);
|
log.error(error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const openPackageEntrypointInTerminal = (pkg: string) => {
|
export const openPackageEntrypointInTerminal = (pkg: string) => {
|
||||||
try {
|
try {
|
||||||
ipcRenderer.invoke("open-terminal", { pkg });
|
ipcRenderer.invoke("open-terminal", { pkg });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error(error);
|
log.error(error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const shellOpenExternal = (link?: string) => {
|
export const shellOpenExternal = (link?: string) => {
|
||||||
if (!link) {
|
if (!link) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
shell.openExternal(link);
|
shell.openExternal(link);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const listenToChannel = (channel: string, callback: (data: any) => void) => {
|
export const listenToChannel = (channel: string, callback: (data: any) => void) => {
|
||||||
ipcRenderer.on(channel, (_: any, data: any) => callback(data));
|
ipcRenderer.on(channel, (_: any, data: any) => callback(data));
|
||||||
};
|
};
|
||||||
|
|
||||||
export const relaunch = () => ipcRenderer.invoke("relaunch");
|
export const relaunch = () => ipcRenderer.invoke("relaunch");
|
||||||
|
|
||||||
export const getProtocolPath = async (): Promise<string> => {
|
export const getProtocolPath = async (): Promise<string> => {
|
||||||
const path = await ipcRenderer.invoke("get-protocol-path");
|
const path = await ipcRenderer.invoke("get-protocol-path");
|
||||||
return path;
|
return path;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const submitLogs = async (): Promise<string> => {
|
export const submitLogs = async (): Promise<string> => {
|
||||||
const response = await ipcRenderer.invoke("submit-logs");
|
const response = await ipcRenderer.invoke("submit-logs");
|
||||||
return response;
|
return response;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isPackageInstalled = async (fullName: string, version?: string): Promise<boolean> => {
|
export const isPackageInstalled = async (fullName: string, version?: string): Promise<boolean> => {
|
||||||
let isInstalled = false;
|
let isInstalled = false;
|
||||||
const pkgs = await getInstalledPackages();
|
const pkgs = await getInstalledPackages();
|
||||||
const pkg = pkgs.find((p) => p.full_name === fullName);
|
const pkg = pkgs.find((p) => p.full_name === fullName);
|
||||||
|
|
||||||
if (pkg) {
|
if (pkg) {
|
||||||
isInstalled = true;
|
isInstalled = true;
|
||||||
if (version) {
|
if (version) {
|
||||||
isInstalled = pkg.installed_versions.includes(version);
|
isInstalled = pkg.installed_versions.includes(version);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return isInstalled;
|
return isInstalled;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const setBadgeCount = async (count: number) => {
|
export const setBadgeCount = async (count: number) => {
|
||||||
try {
|
try {
|
||||||
await ipcRenderer.invoke("set-badge-count", { count });
|
await ipcRenderer.invoke("set-badge-count", { count });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error(error);
|
log.error(error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const deletePackage = async (args: { fullName: string; version: string }) => {
|
export const deletePackage = async (args: { fullName: string; version: string }) => {
|
||||||
const result = await ipcRenderer.invoke("delete-package", args);
|
const result = await ipcRenderer.invoke("delete-package", args);
|
||||||
if (result instanceof Error) {
|
if (result instanceof Error) {
|
||||||
throw result;
|
throw result;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const loadPackageCache = async () => {
|
export const loadPackageCache = async () => {
|
||||||
try {
|
try {
|
||||||
return await ipcRenderer.invoke("load-package-cache");
|
return await ipcRenderer.invoke("load-package-cache");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error(error);
|
log.error(error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const writePackageCache = async (pkgs: Packages) => {
|
export const writePackageCache = async (pkgs: Packages) => {
|
||||||
try {
|
try {
|
||||||
await ipcRenderer.invoke("write-package-cache", pkgs);
|
await ipcRenderer.invoke("write-package-cache", pkgs);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error(error);
|
log.error(error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const topbarDoubleClick = async () => {
|
export const topbarDoubleClick = async () => {
|
||||||
try {
|
try {
|
||||||
ipcRenderer.invoke("topbar-double-click");
|
ipcRenderer.invoke("topbar-double-click");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error(error);
|
log.error(error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const cacheImageURL = async (url: string): Promise<string | undefined> => {
|
export const cacheImageURL = async (url: string): Promise<string | undefined> => {
|
||||||
if (!url) return "";
|
if (!url) return "";
|
||||||
try {
|
try {
|
||||||
const cachedSrc = await ipcRenderer.invoke("cache-image", url);
|
const cachedSrc = await ipcRenderer.invoke("cache-image", url);
|
||||||
return cachedSrc;
|
return cachedSrc;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error("Failed to cache image:", error);
|
log.error("Failed to cache image:", error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getAutoUpdateStatus = async (): Promise<AutoUpdateStatus> => {
|
export const getAutoUpdateStatus = async (): Promise<AutoUpdateStatus> => {
|
||||||
try {
|
try {
|
||||||
return await ipcRenderer.invoke("get-auto-update-status");
|
return await ipcRenderer.invoke("get-auto-update-status");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error(error);
|
log.error(error);
|
||||||
return { status: "up-to-date" };
|
return { status: "up-to-date" };
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -15,399 +15,399 @@ import _ from "lodash";
|
||||||
import * as v1Client from "$libs/v1-client";
|
import * as v1Client from "$libs/v1-client";
|
||||||
|
|
||||||
const packages: Package[] = [
|
const packages: Package[] = [
|
||||||
{
|
{
|
||||||
slug: "mesonbuild_com",
|
slug: "mesonbuild_com",
|
||||||
homepage: "https://mesonbuild.com",
|
homepage: "https://mesonbuild.com",
|
||||||
name: "mesonbuild.com",
|
name: "mesonbuild.com",
|
||||||
version: "0.63.3",
|
version: "0.63.3",
|
||||||
last_modified: "2022-10-06T15:45:08.000Z",
|
last_modified: "2022-10-06T15:45:08.000Z",
|
||||||
full_name: "mesonbuild.com",
|
full_name: "mesonbuild.com",
|
||||||
dl_count: 270745,
|
dl_count: 270745,
|
||||||
thumb_image_name: "mesonbuild_com_option 1.jpg ",
|
thumb_image_name: "mesonbuild_com_option 1.jpg ",
|
||||||
maintainer: "",
|
maintainer: "",
|
||||||
desc: "Fast and user friendly build system",
|
desc: "Fast and user friendly build system",
|
||||||
thumb_image_url: "https://tea.xyz/Images/packages/mesonbuild_com.jpg",
|
thumb_image_url: "https://tea.xyz/Images/packages/mesonbuild_com.jpg",
|
||||||
installs: 0,
|
installs: 0,
|
||||||
categories: ["foundation_essentials"],
|
categories: ["foundation_essentials"],
|
||||||
created: "2022-10-06T15:45:08.000Z",
|
created: "2022-10-06T15:45:08.000Z",
|
||||||
manual_sorting: 0,
|
manual_sorting: 0,
|
||||||
card_layout: "bottom"
|
card_layout: "bottom"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
slug: "pixman_org",
|
slug: "pixman_org",
|
||||||
homepage: "http://www.pixman.org/",
|
homepage: "http://www.pixman.org/",
|
||||||
maintainer: "freedesktop",
|
maintainer: "freedesktop",
|
||||||
name: "pixman.org",
|
name: "pixman.org",
|
||||||
version: "0.40.0",
|
version: "0.40.0",
|
||||||
last_modified: "2022-09-26T19:37:47.000Z",
|
last_modified: "2022-09-26T19:37:47.000Z",
|
||||||
full_name: "pixman.org",
|
full_name: "pixman.org",
|
||||||
dl_count: 0,
|
dl_count: 0,
|
||||||
thumb_image_name: "pixman_org_option 1.jpg ",
|
thumb_image_name: "pixman_org_option 1.jpg ",
|
||||||
desc: "Pixman is a library that provides low-level pixel manipulation features such as image compositing and trapezoid rasterization.",
|
desc: "Pixman is a library that provides low-level pixel manipulation features such as image compositing and trapezoid rasterization.",
|
||||||
thumb_image_url: "https://tea.xyz/Images/packages/pixman_org.jpg",
|
thumb_image_url: "https://tea.xyz/Images/packages/pixman_org.jpg",
|
||||||
installs: 0,
|
installs: 0,
|
||||||
categories: ["foundation_essentials"],
|
categories: ["foundation_essentials"],
|
||||||
created: "2022-09-26T19:37:47.000Z",
|
created: "2022-09-26T19:37:47.000Z",
|
||||||
manual_sorting: 1,
|
manual_sorting: 1,
|
||||||
card_layout: "bottom"
|
card_layout: "bottom"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
slug: "freedesktop_org_pkg_config",
|
slug: "freedesktop_org_pkg_config",
|
||||||
homepage: "https://freedesktop.org",
|
homepage: "https://freedesktop.org",
|
||||||
maintainer: "freedesktop.org",
|
maintainer: "freedesktop.org",
|
||||||
name: "pkg-config",
|
name: "pkg-config",
|
||||||
version: "0.29.2",
|
version: "0.29.2",
|
||||||
last_modified: "2022-10-20T01:32:15.000Z",
|
last_modified: "2022-10-20T01:32:15.000Z",
|
||||||
full_name: "freedesktop.org/pkg-config",
|
full_name: "freedesktop.org/pkg-config",
|
||||||
dl_count: 2661501,
|
dl_count: 2661501,
|
||||||
thumb_image_name: "freedecktop_org_pkg_config option 1.jpg ",
|
thumb_image_name: "freedecktop_org_pkg_config option 1.jpg ",
|
||||||
desc: "Manage compile and link flags for libraries",
|
desc: "Manage compile and link flags for libraries",
|
||||||
thumb_image_url: "https://tea.xyz/Images/packages/freedesktop_org_pkg_config.jpg",
|
thumb_image_url: "https://tea.xyz/Images/packages/freedesktop_org_pkg_config.jpg",
|
||||||
installs: 0,
|
installs: 0,
|
||||||
categories: ["foundation_essentials"],
|
categories: ["foundation_essentials"],
|
||||||
created: "2022-10-20T01:32:15.000Z",
|
created: "2022-10-20T01:32:15.000Z",
|
||||||
manual_sorting: 2,
|
manual_sorting: 2,
|
||||||
card_layout: "bottom"
|
card_layout: "bottom"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
slug: "gnu_org_gettext",
|
slug: "gnu_org_gettext",
|
||||||
homepage: "https://gnu.org",
|
homepage: "https://gnu.org",
|
||||||
maintainer: "gnu.org",
|
maintainer: "gnu.org",
|
||||||
name: "gettext",
|
name: "gettext",
|
||||||
version: "0.21.1",
|
version: "0.21.1",
|
||||||
last_modified: "2022-10-20T01:23:46.000Z",
|
last_modified: "2022-10-20T01:23:46.000Z",
|
||||||
full_name: "gnu.org/gettext",
|
full_name: "gnu.org/gettext",
|
||||||
dl_count: 3715970,
|
dl_count: 3715970,
|
||||||
thumb_image_name: "gnu_org_gettext_option 1.jpg ",
|
thumb_image_name: "gnu_org_gettext_option 1.jpg ",
|
||||||
desc: "GNU internationalization (i18n) and localization (l10n) library",
|
desc: "GNU internationalization (i18n) and localization (l10n) library",
|
||||||
thumb_image_url: "https://tea.xyz/Images/packages/gnu_org_gettext.jpg",
|
thumb_image_url: "https://tea.xyz/Images/packages/gnu_org_gettext.jpg",
|
||||||
installs: 0,
|
installs: 0,
|
||||||
categories: ["foundation_essentials"],
|
categories: ["foundation_essentials"],
|
||||||
created: "2022-10-20T01:23:46.000Z",
|
created: "2022-10-20T01:23:46.000Z",
|
||||||
manual_sorting: 3,
|
manual_sorting: 3,
|
||||||
card_layout: "bottom"
|
card_layout: "bottom"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
slug: "ipfs_tech",
|
slug: "ipfs_tech",
|
||||||
homepage: "https://ipfs.tech",
|
homepage: "https://ipfs.tech",
|
||||||
name: "ipfs.tech",
|
name: "ipfs.tech",
|
||||||
version: "0.16.0",
|
version: "0.16.0",
|
||||||
last_modified: "2022-10-19T21:36:52.000Z",
|
last_modified: "2022-10-19T21:36:52.000Z",
|
||||||
full_name: "ipfs.tech",
|
full_name: "ipfs.tech",
|
||||||
dl_count: 14457,
|
dl_count: 14457,
|
||||||
thumb_image_name: "ipfs_tech_option 2.jpg ",
|
thumb_image_name: "ipfs_tech_option 2.jpg ",
|
||||||
maintainer: "",
|
maintainer: "",
|
||||||
desc: "Peer-to-peer hypermedia protocol",
|
desc: "Peer-to-peer hypermedia protocol",
|
||||||
thumb_image_url: "https://tea.xyz/Images/packages/ipfs_tech.jpg",
|
thumb_image_url: "https://tea.xyz/Images/packages/ipfs_tech.jpg",
|
||||||
installs: 0,
|
installs: 0,
|
||||||
categories: ["foundation_essentials"],
|
categories: ["foundation_essentials"],
|
||||||
created: "2022-10-19T21:36:52.000Z",
|
created: "2022-10-19T21:36:52.000Z",
|
||||||
manual_sorting: 4,
|
manual_sorting: 4,
|
||||||
card_layout: "bottom"
|
card_layout: "bottom"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
slug: "nixos_org_patchelf",
|
slug: "nixos_org_patchelf",
|
||||||
homepage: "https://nixos.org",
|
homepage: "https://nixos.org",
|
||||||
maintainer: "nixos.org",
|
maintainer: "nixos.org",
|
||||||
name: "patchelf",
|
name: "patchelf",
|
||||||
version: "0.15.0",
|
version: "0.15.0",
|
||||||
last_modified: "2022-09-27T04:50:44.000Z",
|
last_modified: "2022-09-27T04:50:44.000Z",
|
||||||
full_name: "nixos.org/patchelf",
|
full_name: "nixos.org/patchelf",
|
||||||
dl_count: 0,
|
dl_count: 0,
|
||||||
thumb_image_name: "nixos_org_patchelf_option 1.jpg ",
|
thumb_image_name: "nixos_org_patchelf_option 1.jpg ",
|
||||||
desc: "PatchELF is a simple utility for modifying existing ELF executables and libraries.",
|
desc: "PatchELF is a simple utility for modifying existing ELF executables and libraries.",
|
||||||
thumb_image_url: "https://tea.xyz/Images/packages/nixos_org_patchelf.jpg",
|
thumb_image_url: "https://tea.xyz/Images/packages/nixos_org_patchelf.jpg",
|
||||||
installs: 0,
|
installs: 0,
|
||||||
categories: ["top_packages", "foundation_essentials"],
|
categories: ["top_packages", "foundation_essentials"],
|
||||||
created: "2022-09-27T04:50:44.000Z",
|
created: "2022-09-27T04:50:44.000Z",
|
||||||
manual_sorting: 5,
|
manual_sorting: 5,
|
||||||
card_layout: "bottom"
|
card_layout: "bottom"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
slug: "tea_xyz",
|
slug: "tea_xyz",
|
||||||
homepage: "https://tea.xyz",
|
homepage: "https://tea.xyz",
|
||||||
maintainer: "tea.xyz",
|
maintainer: "tea.xyz",
|
||||||
name: "tea.xyz",
|
name: "tea.xyz",
|
||||||
version: "0.8.6",
|
version: "0.8.6",
|
||||||
last_modified: "2022-10-19T19:13:51.000Z",
|
last_modified: "2022-10-19T19:13:51.000Z",
|
||||||
full_name: "tea.xyz",
|
full_name: "tea.xyz",
|
||||||
dl_count: 0,
|
dl_count: 0,
|
||||||
thumb_image_name: "tea_xyz_option 2.jpg ",
|
thumb_image_name: "tea_xyz_option 2.jpg ",
|
||||||
desc: "Website of tea.xyz",
|
desc: "Website of tea.xyz",
|
||||||
thumb_image_url: "https://tea.xyz/Images/packages/tea_xyz.jpg",
|
thumb_image_url: "https://tea.xyz/Images/packages/tea_xyz.jpg",
|
||||||
installs: 0,
|
installs: 0,
|
||||||
categories: ["top_packages", "foundation_essentials"],
|
categories: ["top_packages", "foundation_essentials"],
|
||||||
created: "2022-10-19T19:13:51.000Z",
|
created: "2022-10-19T19:13:51.000Z",
|
||||||
manual_sorting: 6,
|
manual_sorting: 6,
|
||||||
card_layout: "bottom"
|
card_layout: "bottom"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
slug: "charm_sh_gum",
|
slug: "charm_sh_gum",
|
||||||
homepage: "https://charm.sh",
|
homepage: "https://charm.sh",
|
||||||
maintainer: "charm.sh",
|
maintainer: "charm.sh",
|
||||||
name: "gum",
|
name: "gum",
|
||||||
version: "0.8.0",
|
version: "0.8.0",
|
||||||
last_modified: "2022-10-21T02:15:16.000Z",
|
last_modified: "2022-10-21T02:15:16.000Z",
|
||||||
full_name: "charm.sh/gum",
|
full_name: "charm.sh/gum",
|
||||||
dl_count: 0,
|
dl_count: 0,
|
||||||
thumb_image_name: "charm_sh_gum.jpg ",
|
thumb_image_name: "charm_sh_gum.jpg ",
|
||||||
desc: "",
|
desc: "",
|
||||||
thumb_image_url: "https://tea.xyz/Images/packages/charm_sh_gum.jpg",
|
thumb_image_url: "https://tea.xyz/Images/packages/charm_sh_gum.jpg",
|
||||||
installs: 0,
|
installs: 0,
|
||||||
categories: ["top_packages", "foundation_essentials"],
|
categories: ["top_packages", "foundation_essentials"],
|
||||||
created: "2022-10-21T02:15:16.000Z",
|
created: "2022-10-21T02:15:16.000Z",
|
||||||
manual_sorting: 7,
|
manual_sorting: 7,
|
||||||
card_layout: "bottom"
|
card_layout: "bottom"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
slug: "pyyaml_org",
|
slug: "pyyaml_org",
|
||||||
homepage: "https://pyyaml.org",
|
homepage: "https://pyyaml.org",
|
||||||
name: "pyyaml.org",
|
name: "pyyaml.org",
|
||||||
version: "0.2.5",
|
version: "0.2.5",
|
||||||
last_modified: "2022-10-03T15:35:14.000Z",
|
last_modified: "2022-10-03T15:35:14.000Z",
|
||||||
full_name: "pyyaml.org",
|
full_name: "pyyaml.org",
|
||||||
dl_count: 107505,
|
dl_count: 107505,
|
||||||
thumb_image_name: "pyyaml_org_option 1.jpg ",
|
thumb_image_name: "pyyaml_org_option 1.jpg ",
|
||||||
maintainer: "",
|
maintainer: "",
|
||||||
desc: "YAML framework for Python",
|
desc: "YAML framework for Python",
|
||||||
thumb_image_url: "https://tea.xyz/Images/packages/pyyaml_org.jpg",
|
thumb_image_url: "https://tea.xyz/Images/packages/pyyaml_org.jpg",
|
||||||
installs: 0,
|
installs: 0,
|
||||||
categories: ["top_packages", "foundation_essentials"],
|
categories: ["top_packages", "foundation_essentials"],
|
||||||
created: "2022-10-03T15:35:14.000Z",
|
created: "2022-10-03T15:35:14.000Z",
|
||||||
manual_sorting: 8,
|
manual_sorting: 8,
|
||||||
card_layout: "bottom"
|
card_layout: "bottom"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
slug: "tea_xyz_gx_cc",
|
slug: "tea_xyz_gx_cc",
|
||||||
homepage: "https://tea.xyz",
|
homepage: "https://tea.xyz",
|
||||||
maintainer: "tea.xyz",
|
maintainer: "tea.xyz",
|
||||||
name: "cc",
|
name: "cc",
|
||||||
version: "0.1.0",
|
version: "0.1.0",
|
||||||
last_modified: "2022-10-19T16:47:44.000Z",
|
last_modified: "2022-10-19T16:47:44.000Z",
|
||||||
full_name: "tea.xyz/gx/cc",
|
full_name: "tea.xyz/gx/cc",
|
||||||
dl_count: 0,
|
dl_count: 0,
|
||||||
thumb_image_name: "tea_xyz_gx.jpg ",
|
thumb_image_name: "tea_xyz_gx.jpg ",
|
||||||
desc: "",
|
desc: "",
|
||||||
thumb_image_url: "https://tea.xyz/Images/packages/tea_xyz_gx_cc.jpg",
|
thumb_image_url: "https://tea.xyz/Images/packages/tea_xyz_gx_cc.jpg",
|
||||||
installs: 0,
|
installs: 0,
|
||||||
categories: ["top_packages", "foundation_essentials"],
|
categories: ["top_packages", "foundation_essentials"],
|
||||||
created: "2022-10-19T16:47:44.000Z",
|
created: "2022-10-19T16:47:44.000Z",
|
||||||
manual_sorting: 9,
|
manual_sorting: 9,
|
||||||
card_layout: "bottom"
|
card_layout: "bottom"
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
export const getInstalledPackages = () => [];
|
export const getInstalledPackages = () => [];
|
||||||
export async function getDistPackages(): Promise<Package[]> {
|
export async function getDistPackages(): Promise<Package[]> {
|
||||||
return packages;
|
return packages;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getPackages(): Promise<GUIPackage[]> {
|
export async function getPackages(): Promise<GUIPackage[]> {
|
||||||
return packages.map((pkg) => {
|
return packages.map((pkg) => {
|
||||||
return {
|
return {
|
||||||
...pkg,
|
...pkg,
|
||||||
state: PackageStates.AVAILABLE
|
state: PackageStates.AVAILABLE
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getFeaturedPackages(): Promise<Package[]> {
|
export async function getFeaturedPackages(): Promise<Package[]> {
|
||||||
await delay(2000);
|
await delay(2000);
|
||||||
return packages.slice(0, 4);
|
return packages.slice(0, 4);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getPackageReviews(full_name: string): Promise<Review[]> {
|
export async function getPackageReviews(full_name: string): Promise<Review[]> {
|
||||||
console.log(`generating reviews for ${full_name}`);
|
console.log(`generating reviews for ${full_name}`);
|
||||||
|
|
||||||
const reviewCount = _.random(9, 21);
|
const reviewCount = _.random(9, 21);
|
||||||
const reviews: Review[] = [];
|
const reviews: Review[] = [];
|
||||||
|
|
||||||
for (let i = 0; i < reviewCount; i++) {
|
for (let i = 0; i < reviewCount; i++) {
|
||||||
const title = loremIpsum({
|
const title = loremIpsum({
|
||||||
count: _.random(2, 5),
|
count: _.random(2, 5),
|
||||||
format: "plain",
|
format: "plain",
|
||||||
paragraphLowerBound: 3,
|
paragraphLowerBound: 3,
|
||||||
paragraphUpperBound: 7,
|
paragraphUpperBound: 7,
|
||||||
random: Math.random,
|
random: Math.random,
|
||||||
sentenceLowerBound: 5,
|
sentenceLowerBound: 5,
|
||||||
sentenceUpperBound: 15,
|
sentenceUpperBound: 15,
|
||||||
units: "words"
|
units: "words"
|
||||||
});
|
});
|
||||||
const comment = loremIpsum({
|
const comment = loremIpsum({
|
||||||
count: 2,
|
count: 2,
|
||||||
format: "plain",
|
format: "plain",
|
||||||
paragraphLowerBound: 3,
|
paragraphLowerBound: 3,
|
||||||
paragraphUpperBound: 7,
|
paragraphUpperBound: 7,
|
||||||
random: Math.random,
|
random: Math.random,
|
||||||
sentenceLowerBound: 5,
|
sentenceLowerBound: 5,
|
||||||
sentenceUpperBound: 15,
|
sentenceUpperBound: 15,
|
||||||
units: "sentences"
|
units: "sentences"
|
||||||
});
|
});
|
||||||
const rating = _.random(0, 5);
|
const rating = _.random(0, 5);
|
||||||
reviews.push({
|
reviews.push({
|
||||||
title,
|
title,
|
||||||
comment,
|
comment,
|
||||||
rating
|
rating
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
await delay(2000);
|
await delay(2000);
|
||||||
return reviews;
|
return reviews;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function installPackage(pkg: GUIPackage, version?: string) {
|
export async function installPackage(pkg: GUIPackage, version?: string) {
|
||||||
console.log("installing: ", pkg.full_name, version);
|
console.log("installing: ", pkg.full_name, version);
|
||||||
await delay(10000);
|
await delay(10000);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function syncPantry() {
|
export async function syncPantry() {
|
||||||
console.log("syncing pantry");
|
console.log("syncing pantry");
|
||||||
await delay(1000);
|
await delay(1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
function delay(ms: number) {
|
function delay(ms: number) {
|
||||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getTopPackages(): Promise<GUIPackage[]> {
|
export async function getTopPackages(): Promise<GUIPackage[]> {
|
||||||
await delay(500);
|
await delay(500);
|
||||||
return packages.slice(0, 9).map((pkg) => {
|
return packages.slice(0, 9).map((pkg) => {
|
||||||
return {
|
return {
|
||||||
...pkg,
|
...pkg,
|
||||||
state: PackageStates.AVAILABLE
|
state: PackageStates.AVAILABLE
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getAllPosts(type: string): Promise<AirtablePost[]> {
|
export async function getAllPosts(type: string): Promise<AirtablePost[]> {
|
||||||
console.log("filter by type:", type);
|
console.log("filter by type:", type);
|
||||||
const posts: AirtablePost[] = [
|
const posts: AirtablePost[] = [
|
||||||
{
|
{
|
||||||
airtable_record_id: "a",
|
airtable_record_id: "a",
|
||||||
link: "https://google.com",
|
link: "https://google.com",
|
||||||
title: "Tea Inc releases game changing api!",
|
title: "Tea Inc releases game changing api!",
|
||||||
sub_title: "lorem ipsum dolor sit amet",
|
sub_title: "lorem ipsum dolor sit amet",
|
||||||
short_description: "lorem ipsum dolor sit amet",
|
short_description: "lorem ipsum dolor sit amet",
|
||||||
thumb_image_url: "/images/bored-ape.png",
|
thumb_image_url: "/images/bored-ape.png",
|
||||||
thumb_image_name: "borred-api.png",
|
thumb_image_name: "borred-api.png",
|
||||||
created_at: new Date(),
|
created_at: new Date(),
|
||||||
updated_at: new Date(),
|
updated_at: new Date(),
|
||||||
published_at: new Date(),
|
published_at: new Date(),
|
||||||
tags: ["news"]
|
tags: ["news"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
airtable_record_id: "b",
|
airtable_record_id: "b",
|
||||||
link: "https://google.com",
|
link: "https://google.com",
|
||||||
title: "Bored Ape not bored anymore",
|
title: "Bored Ape not bored anymore",
|
||||||
sub_title: "lorem ipsum dolor sit amet",
|
sub_title: "lorem ipsum dolor sit amet",
|
||||||
short_description: "lorem ipsum dolor sit amet",
|
short_description: "lorem ipsum dolor sit amet",
|
||||||
thumb_image_url: "/images/bored-ape.png",
|
thumb_image_url: "/images/bored-ape.png",
|
||||||
thumb_image_name: "borred-api.png",
|
thumb_image_name: "borred-api.png",
|
||||||
created_at: new Date(),
|
created_at: new Date(),
|
||||||
updated_at: new Date(),
|
updated_at: new Date(),
|
||||||
published_at: new Date(),
|
published_at: new Date(),
|
||||||
tags: ["news"]
|
tags: ["news"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
airtable_record_id: "c",
|
airtable_record_id: "c",
|
||||||
link: "https://google.com",
|
link: "https://google.com",
|
||||||
title: "Markdown can be executed! hoohah!",
|
title: "Markdown can be executed! hoohah!",
|
||||||
sub_title: "lorem ipsum dolor sit amet",
|
sub_title: "lorem ipsum dolor sit amet",
|
||||||
short_description: "lorem ipsum dolor sit amet",
|
short_description: "lorem ipsum dolor sit amet",
|
||||||
thumb_image_url: "/images/bored-ape.png",
|
thumb_image_url: "/images/bored-ape.png",
|
||||||
thumb_image_name: "borred-api.png",
|
thumb_image_name: "borred-api.png",
|
||||||
created_at: new Date(),
|
created_at: new Date(),
|
||||||
updated_at: new Date(),
|
updated_at: new Date(),
|
||||||
published_at: new Date(),
|
published_at: new Date(),
|
||||||
tags: ["news"]
|
tags: ["news"]
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
return posts;
|
return posts;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getDeviceAuth(deviceId: string): Promise<any> {
|
export async function getDeviceAuth(deviceId: string): Promise<any> {
|
||||||
const data = await v1Client.get<any>(`/auth/device/${deviceId}`);
|
const data = await v1Client.get<any>(`/auth/device/${deviceId}`);
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getPackageBottles(name: string): Promise<Bottle[]> {
|
export async function getPackageBottles(name: string): Promise<Bottle[]> {
|
||||||
return [
|
return [
|
||||||
{ name, platform: "darwin", arch: "aarch64", version: "3.39.4", bytes: 123456 },
|
{ name, platform: "darwin", arch: "aarch64", version: "3.39.4", bytes: 123456 },
|
||||||
{ name, platform: "darwin", arch: "aarch64", version: "3.40.0", bytes: 123456 },
|
{ name, platform: "darwin", arch: "aarch64", version: "3.40.0", bytes: 123456 },
|
||||||
{ name, platform: "darwin", arch: "x86-64", version: "3.39.4", bytes: 123456 },
|
{ name, platform: "darwin", arch: "x86-64", version: "3.39.4", bytes: 123456 },
|
||||||
{ name, platform: "darwin", arch: "x86-64", version: "3.40.0", bytes: 123456 },
|
{ name, platform: "darwin", arch: "x86-64", version: "3.40.0", bytes: 123456 },
|
||||||
{ name, platform: "linux", arch: "aarch64", version: "3.39.4", bytes: 123456 },
|
{ name, platform: "linux", arch: "aarch64", version: "3.39.4", bytes: 123456 },
|
||||||
{ name, platform: "linux", arch: "aarch64", version: "3.40.0", bytes: 123456 },
|
{ name, platform: "linux", arch: "aarch64", version: "3.40.0", bytes: 123456 },
|
||||||
{ name, platform: "linux", arch: "x86-64", version: "3.39.4", bytes: 123456 },
|
{ name, platform: "linux", arch: "x86-64", version: "3.39.4", bytes: 123456 },
|
||||||
{ name, platform: "linux", arch: "x86-64", version: "3.40.0", bytes: 123456 }
|
{ name, platform: "linux", arch: "x86-64", version: "3.40.0", bytes: 123456 }
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getPackage(packageName: string): Promise<Partial<Package>> {
|
export async function getPackage(packageName: string): Promise<Partial<Package>> {
|
||||||
return packages.find((pkg) => pkg.full_name === packageName) || packages[0];
|
return packages.find((pkg) => pkg.full_name === packageName) || packages[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getSession = async (): Promise<Session | null> => {
|
export const getSession = async (): Promise<Session | null> => {
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const updateSession = async (session: Partial<Session>) => {
|
export const updateSession = async (session: Partial<Session>) => {
|
||||||
console.log(session);
|
console.log(session);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const openTerminal = (cmd: string) => console.log(cmd);
|
export const openTerminal = (cmd: string) => console.log(cmd);
|
||||||
|
|
||||||
export const shellOpenExternal = (link?: string) => {
|
export const shellOpenExternal = (link?: string) => {
|
||||||
window.open(link, "_blank");
|
window.open(link, "_blank");
|
||||||
};
|
};
|
||||||
|
|
||||||
export const listenToChannel = (channel: string, callback: (msg: string, ...args: any) => void) => {
|
export const listenToChannel = (channel: string, callback: (msg: string, ...args: any) => void) => {
|
||||||
console.log("listen to channel", channel, callback);
|
console.log("listen to channel", channel, callback);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const relaunch = () => {
|
export const relaunch = () => {
|
||||||
console.log("relaunch");
|
console.log("relaunch");
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getProtocolPath = async (): Promise<string> => "";
|
export const getProtocolPath = async (): Promise<string> => "";
|
||||||
|
|
||||||
export const submitLogs = async (): Promise<string> => {
|
export const submitLogs = async (): Promise<string> => {
|
||||||
return "deviceId---logid";
|
return "deviceId---logid";
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isPackageInstalled = async (_v?: string): Promise<boolean> => {
|
export const isPackageInstalled = async (_v?: string): Promise<boolean> => {
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const setBadgeCount = async (count: number) => {
|
export const setBadgeCount = async (count: number) => {
|
||||||
console.log("set badge count", count);
|
console.log("set badge count", count);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const deletePackage = async (args: { fullName: string; version: string }) => {
|
export const deletePackage = async (args: { fullName: string; version: string }) => {
|
||||||
console.log("delete package", args);
|
console.log("delete package", args);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const loadPackageCache = async () => {
|
export const loadPackageCache = async () => {
|
||||||
return { version: "1", packages: {} };
|
return { version: "1", packages: {} };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const writePackageCache = async (pkgs: Packages) => {
|
export const writePackageCache = async (pkgs: Packages) => {
|
||||||
console.log("write package cache", pkgs);
|
console.log("write package cache", pkgs);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const topbarDoubleClick = async () => {
|
export const topbarDoubleClick = async () => {
|
||||||
console.log("topbar double click");
|
console.log("topbar double click");
|
||||||
};
|
};
|
||||||
|
|
||||||
export const cacheImageURL = async (_url: string): Promise<string | undefined> => {
|
export const cacheImageURL = async (_url: string): Promise<string | undefined> => {
|
||||||
return undefined;
|
return undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getAutoUpdateStatus = async (): Promise<AutoUpdateStatus> => {
|
export const getAutoUpdateStatus = async (): Promise<AutoUpdateStatus> => {
|
||||||
return { status: "up-to-date" };
|
return { status: "up-to-date" };
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function openPackageEntrypointInTerminal(pkg: string) {
|
export async function openPackageEntrypointInTerminal(pkg: string) {
|
||||||
//noop
|
//noop
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,49 +1,49 @@
|
||||||
import { getPkgBottles } from "../tea-dir";
|
import { getPkgBottles } from "../tea-dir";
|
||||||
|
|
||||||
describe("tea-dir module", () => {
|
describe("tea-dir module", () => {
|
||||||
it("should getPkgBottles from nested Dir object/s", () => {
|
it("should getPkgBottles from nested Dir object/s", () => {
|
||||||
const results = getPkgBottles({
|
const results = getPkgBottles({
|
||||||
name: "kkos",
|
name: "kkos",
|
||||||
path: "/Users/x/.tea/github.com/kkos",
|
path: "/Users/x/.tea/github.com/kkos",
|
||||||
children: [
|
children: [
|
||||||
{ name: ".DS_Store", path: "/Users/x/.tea/github.com/kkos/.DS_Store" },
|
{ name: ".DS_Store", path: "/Users/x/.tea/github.com/kkos/.DS_Store" },
|
||||||
{
|
{
|
||||||
name: "oniguruma",
|
name: "oniguruma",
|
||||||
path: "/Users/x/.tea/github.com/kkos/oniguruma",
|
path: "/Users/x/.tea/github.com/kkos/oniguruma",
|
||||||
children: [
|
children: [
|
||||||
{ name: ".DS_Store", path: "/Users/x/.tea/github.com/kkos/oniguruma/.DS_Store" },
|
{ name: ".DS_Store", path: "/Users/x/.tea/github.com/kkos/oniguruma/.DS_Store" },
|
||||||
{
|
{
|
||||||
path: "/Users/x/.tea/github.com/kkos/oniguruma/v6",
|
path: "/Users/x/.tea/github.com/kkos/oniguruma/v6",
|
||||||
name: "v6",
|
name: "v6",
|
||||||
children: [
|
children: [
|
||||||
{ name: ".DS_Store", path: "/Users/x/.tea/github.com/kkos/oniguruma/v6/.DS_Store" }
|
{ name: ".DS_Store", path: "/Users/x/.tea/github.com/kkos/oniguruma/v6/.DS_Store" }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "v*",
|
name: "v*",
|
||||||
path: "/Users/x/.tea/github.com/kkos/oniguruma/v*",
|
path: "/Users/x/.tea/github.com/kkos/oniguruma/v*",
|
||||||
children: []
|
children: []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "v6.9.8",
|
name: "v6.9.8",
|
||||||
path: "/Users/x/.tea/github.com/kkos/oniguruma/v6.9.8",
|
path: "/Users/x/.tea/github.com/kkos/oniguruma/v6.9.8",
|
||||||
children: []
|
children: []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "v6.9",
|
name: "v6.9",
|
||||||
path: "/Users/x/.tea/github.com/kkos/oniguruma/v6.9",
|
path: "/Users/x/.tea/github.com/kkos/oniguruma/v6.9",
|
||||||
children: []
|
children: []
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(results).toEqual([
|
expect(results).toEqual([
|
||||||
"github.com/kkos/oniguruma/v*",
|
"github.com/kkos/oniguruma/v*",
|
||||||
"github.com/kkos/oniguruma/v6",
|
"github.com/kkos/oniguruma/v6",
|
||||||
"github.com/kkos/oniguruma/v6.9",
|
"github.com/kkos/oniguruma/v6.9",
|
||||||
"github.com/kkos/oniguruma/v6.9.8"
|
"github.com/kkos/oniguruma/v6.9.8"
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -3,30 +3,30 @@
|
||||||
// import { join } from 'upath';
|
// import { join } from 'upath';
|
||||||
|
|
||||||
type Dir = {
|
type Dir = {
|
||||||
name: string;
|
name: string;
|
||||||
path: string;
|
path: string;
|
||||||
children?: Dir[];
|
children?: Dir[];
|
||||||
};
|
};
|
||||||
|
|
||||||
const semverTest =
|
const semverTest =
|
||||||
/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/g;
|
/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/g;
|
||||||
|
|
||||||
export const getPkgBottles = (packageDir: Dir): string[] => {
|
export const getPkgBottles = (packageDir: Dir): string[] => {
|
||||||
const bottles: string[] = [];
|
const bottles: string[] = [];
|
||||||
|
|
||||||
const pkg = packageDir.path.split(".tea/")[1];
|
const pkg = packageDir.path.split(".tea/")[1];
|
||||||
const version = pkg.split("/v")[1];
|
const version = pkg.split("/v")[1];
|
||||||
|
|
||||||
const isVersion = semverTest.test(version) || !isNaN(+version) || version === "*";
|
const isVersion = semverTest.test(version) || !isNaN(+version) || version === "*";
|
||||||
|
|
||||||
if (version && isVersion) {
|
if (version && isVersion) {
|
||||||
bottles.push(pkg);
|
bottles.push(pkg);
|
||||||
} else if (packageDir?.children?.length) {
|
} else if (packageDir?.children?.length) {
|
||||||
const childBottles = packageDir.children
|
const childBottles = packageDir.children
|
||||||
.map(getPkgBottles)
|
.map(getPkgBottles)
|
||||||
.reduce((arr, bottles) => [...arr, ...bottles], []);
|
.reduce((arr, bottles) => [...arr, ...bottles], []);
|
||||||
bottles.push(...childBottles);
|
bottles.push(...childBottles);
|
||||||
}
|
}
|
||||||
|
|
||||||
return bottles.filter((b) => b !== undefined).sort(); // ie: ["gohugo.io/v*", "gohugo.io/v0", "gohugo.io/v0.108", "gohugo.io/v0.108.0"]
|
return bottles.filter((b) => b !== undefined).sort(); // ie: ["gohugo.io/v*", "gohugo.io/v0", "gohugo.io/v0.108", "gohugo.io/v0.108.0"]
|
||||||
};
|
};
|
||||||
|
|
|
@ -4,44 +4,44 @@ import semverCompare from "semver/functions/compare";
|
||||||
|
|
||||||
// Find a list of available versions for a package based on the bottles
|
// Find a list of available versions for a package based on the bottles
|
||||||
export const findAvailableVersions = (pkg: GUIPackage) => {
|
export const findAvailableVersions = (pkg: GUIPackage) => {
|
||||||
// default to just showing the latest if bottles haven't loaded yet
|
// default to just showing the latest if bottles haven't loaded yet
|
||||||
if (!pkg.bottles) {
|
if (!pkg.bottles) {
|
||||||
return [pkg.version];
|
return [pkg.version];
|
||||||
}
|
}
|
||||||
|
|
||||||
const versionSet = new Set<string>();
|
const versionSet = new Set<string>();
|
||||||
for (const b of pkg.bottles) {
|
for (const b of pkg.bottles) {
|
||||||
versionSet.add(b.version);
|
versionSet.add(b.version);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Array.from(versionSet).sort((a, b) => semverCompare(cleanVersion(b), cleanVersion(a)));
|
return Array.from(versionSet).sort((a, b) => semverCompare(cleanVersion(b), cleanVersion(a)));
|
||||||
};
|
};
|
||||||
|
|
||||||
export const cleanVersion = (version: string) => clean(version) || "0.0.0";
|
export const cleanVersion = (version: string) => clean(version) || "0.0.0";
|
||||||
|
|
||||||
// Add a new version to the list of installed versions while maintaining the sort order
|
// Add a new version to the list of installed versions while maintaining the sort order
|
||||||
export const addInstalledVersion = (
|
export const addInstalledVersion = (
|
||||||
installedVersions: string[] | undefined,
|
installedVersions: string[] | undefined,
|
||||||
newVersion: string
|
newVersion: string
|
||||||
) => {
|
) => {
|
||||||
if (!installedVersions) {
|
if (!installedVersions) {
|
||||||
return [newVersion];
|
return [newVersion];
|
||||||
}
|
}
|
||||||
|
|
||||||
return [...installedVersions, newVersion].sort((a, b) =>
|
return [...installedVersions, newVersion].sort((a, b) =>
|
||||||
semverCompare(cleanVersion(b), cleanVersion(a))
|
semverCompare(cleanVersion(b), cleanVersion(a))
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const findRecentInstalledVersion = (pkg: GUIPackage) => {
|
export const findRecentInstalledVersion = (pkg: GUIPackage) => {
|
||||||
// this assumes that the versions are already sorted
|
// this assumes that the versions are already sorted
|
||||||
return pkg.installed_versions?.[0];
|
return pkg.installed_versions?.[0];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isInstalling = (pkg: GUIPackage) => {
|
export const isInstalling = (pkg: GUIPackage) => {
|
||||||
return (
|
return (
|
||||||
pkg.install_progress_percentage &&
|
pkg.install_progress_percentage &&
|
||||||
pkg.install_progress_percentage > 0 &&
|
pkg.install_progress_percentage > 0 &&
|
||||||
pkg.install_progress_percentage < 100
|
pkg.install_progress_percentage < 100
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -2,20 +2,20 @@ import * as Sentry from "@sentry/browser";
|
||||||
import type { Session } from "./types";
|
import type { Session } from "./types";
|
||||||
|
|
||||||
export function initSentry(session?: Session) {
|
export function initSentry(session?: Session) {
|
||||||
Sentry.init({
|
Sentry.init({
|
||||||
dsn: "https://5ff29bb5b3b64cd4bd4f4960ef1db2e3@o4504750197899264.ingest.sentry.io/4504750206746624"
|
dsn: "https://5ff29bb5b3b64cd4bd4f4960ef1db2e3@o4504750197899264.ingest.sentry.io/4504750206746624"
|
||||||
});
|
});
|
||||||
if (session) {
|
if (session) {
|
||||||
console.log("sentry init", session);
|
console.log("sentry init", session);
|
||||||
Sentry.configureScope(async (scope) => {
|
Sentry.configureScope(async (scope) => {
|
||||||
scope.setUser({
|
scope.setUser({
|
||||||
id: session.device_id, // device_id this should exist in our pg db: developer_id is to many device_id
|
id: session.device_id, // device_id this should exist in our pg db: developer_id is to many device_id
|
||||||
username: session?.user?.login || "" // github username or handler
|
username: session?.user?.login || "" // github username or handler
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function captureException(exception: any) {
|
export function captureException(exception: any) {
|
||||||
Sentry.captureException(exception);
|
Sentry.captureException(exception);
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,118 +17,118 @@ export const featuredPackages = writable<Package[]>([]);
|
||||||
export const packagesStore = initPackagesStore();
|
export const packagesStore = initPackagesStore();
|
||||||
|
|
||||||
export const initializeFeaturedPackages = async () => {
|
export const initializeFeaturedPackages = async () => {
|
||||||
console.log("intialize featured packages");
|
console.log("intialize featured packages");
|
||||||
const packages = await getFeaturedPackages();
|
const packages = await getFeaturedPackages();
|
||||||
featuredPackages.set(packages);
|
featuredPackages.set(packages);
|
||||||
};
|
};
|
||||||
|
|
||||||
interface PackagesReview {
|
interface PackagesReview {
|
||||||
[full_name: string]: Review[];
|
[full_name: string]: Review[];
|
||||||
}
|
}
|
||||||
|
|
||||||
function initPackagesReviewStore() {
|
function initPackagesReviewStore() {
|
||||||
const { update, subscribe } = writable<PackagesReview>({});
|
const { update, subscribe } = writable<PackagesReview>({});
|
||||||
|
|
||||||
let packagesReviews: PackagesReview = {};
|
let packagesReviews: PackagesReview = {};
|
||||||
|
|
||||||
subscribe((v) => (packagesReviews = v));
|
subscribe((v) => (packagesReviews = v));
|
||||||
|
|
||||||
const getSetPackageReviews = async (full_name: string) => {
|
const getSetPackageReviews = async (full_name: string) => {
|
||||||
if (full_name && !packagesReviews[full_name]) {
|
if (full_name && !packagesReviews[full_name]) {
|
||||||
packagesReviews[full_name] = [];
|
packagesReviews[full_name] = [];
|
||||||
const reviews = await getPackageReviews(full_name);
|
const reviews = await getPackageReviews(full_name);
|
||||||
update((v) => {
|
update((v) => {
|
||||||
return {
|
return {
|
||||||
...v,
|
...v,
|
||||||
[full_name]: reviews
|
[full_name]: reviews
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
subscribe: (full_name: string, reset: (reviews: Review[]) => void) => {
|
subscribe: (full_name: string, reset: (reviews: Review[]) => void) => {
|
||||||
getSetPackageReviews(full_name);
|
getSetPackageReviews(full_name);
|
||||||
return subscribe((value) => {
|
return subscribe((value) => {
|
||||||
if (value[full_name]) {
|
if (value[full_name]) {
|
||||||
reset(value[full_name]);
|
reset(value[full_name]);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const packagesReviewStore = initPackagesReviewStore();
|
export const packagesReviewStore = initPackagesReviewStore();
|
||||||
|
|
||||||
function initPosts() {
|
function initPosts() {
|
||||||
let initialized = false;
|
let initialized = false;
|
||||||
const { subscribe } = writable<AirtablePost[]>([]);
|
const { subscribe } = writable<AirtablePost[]>([]);
|
||||||
const posts: AirtablePost[] = [];
|
const posts: AirtablePost[] = [];
|
||||||
let postsIndex: Fuse<AirtablePost>;
|
let postsIndex: Fuse<AirtablePost>;
|
||||||
|
|
||||||
if (!initialized) {
|
if (!initialized) {
|
||||||
initialized = true;
|
initialized = true;
|
||||||
// getAllPosts().then(set);
|
// getAllPosts().then(set);
|
||||||
}
|
}
|
||||||
|
|
||||||
subscribe((v) => {
|
subscribe((v) => {
|
||||||
posts.push(...v);
|
posts.push(...v);
|
||||||
postsIndex = new Fuse(posts, {
|
postsIndex = new Fuse(posts, {
|
||||||
keys: ["title", "sub_title", "short_description", "tags"]
|
keys: ["title", "sub_title", "short_description", "tags"]
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
subscribe,
|
subscribe,
|
||||||
search: async (term: string, limit = 10) => {
|
search: async (term: string, limit = 10) => {
|
||||||
const res = postsIndex.search(term, { limit });
|
const res = postsIndex.search(term, { limit });
|
||||||
const matchingPosts: AirtablePost[] = res.map((v) => v.item);
|
const matchingPosts: AirtablePost[] = res.map((v) => v.item);
|
||||||
return matchingPosts;
|
return matchingPosts;
|
||||||
},
|
},
|
||||||
subscribeByTag: (tag: string, cb: (posts: AirtablePost[]) => void) => {
|
subscribeByTag: (tag: string, cb: (posts: AirtablePost[]) => void) => {
|
||||||
subscribe((newPosts: AirtablePost[]) => {
|
subscribe((newPosts: AirtablePost[]) => {
|
||||||
const filteredPosts = newPosts.filter((post) => post.tags.includes(tag));
|
const filteredPosts = newPosts.filter((post) => post.tags.includes(tag));
|
||||||
cb(filteredPosts);
|
cb(filteredPosts);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
export const postsStore = initPosts();
|
export const postsStore = initPosts();
|
||||||
|
|
||||||
function initSearchStore() {
|
function initSearchStore() {
|
||||||
const searching = writable<boolean>(false);
|
const searching = writable<boolean>(false);
|
||||||
const packagesSearch = writable<GUIPackage[]>([]);
|
const packagesSearch = writable<GUIPackage[]>([]);
|
||||||
const postsSearch = writable<AirtablePost[]>([]);
|
const postsSearch = writable<AirtablePost[]>([]);
|
||||||
|
|
||||||
// TODO:
|
// TODO:
|
||||||
// should use algolia if user is somehow online
|
// should use algolia if user is somehow online
|
||||||
|
|
||||||
return {
|
return {
|
||||||
searching,
|
searching,
|
||||||
packagesSearch,
|
packagesSearch,
|
||||||
postsSearch,
|
postsSearch,
|
||||||
search: async (term: string) => {
|
search: async (term: string) => {
|
||||||
try {
|
try {
|
||||||
if (term) {
|
if (term) {
|
||||||
const [
|
const [
|
||||||
resultPkgs
|
resultPkgs
|
||||||
// resultPosts
|
// resultPosts
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
packagesStore.search(term, 5)
|
packagesStore.search(term, 5)
|
||||||
// postsStore.search(term, 10)
|
// postsStore.search(term, 10)
|
||||||
]);
|
]);
|
||||||
trackSearch(term, resultPkgs.length);
|
trackSearch(term, resultPkgs.length);
|
||||||
packagesSearch.set(resultPkgs);
|
packagesSearch.set(resultPkgs);
|
||||||
// postsSearch.set(resultPosts);
|
// postsSearch.set(resultPosts);
|
||||||
} else {
|
} else {
|
||||||
packagesSearch.set([]);
|
packagesSearch.set([]);
|
||||||
// postsSearch.set([]);
|
// postsSearch.set([]);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const searchStore = initSearchStore();
|
export const searchStore = initSearchStore();
|
||||||
|
|
|
@ -8,81 +8,81 @@ import { initSentry } from "../sentry";
|
||||||
|
|
||||||
export let session: Session | null = null;
|
export let session: Session | null = null;
|
||||||
export const getSession = async (): Promise<Session | null> => {
|
export const getSession = async (): Promise<Session | null> => {
|
||||||
session = await electronGetSession();
|
session = await electronGetSession();
|
||||||
return session;
|
return session;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function initAuthStore() {
|
export default function initAuthStore() {
|
||||||
const user = writable<Developer | undefined>();
|
const user = writable<Developer | undefined>();
|
||||||
const sessionStore = writable<Session>({});
|
const sessionStore = writable<Session>({});
|
||||||
let pollLoop = 0;
|
let pollLoop = 0;
|
||||||
|
|
||||||
const deviceIdStore = writable<string>("");
|
const deviceIdStore = writable<string>("");
|
||||||
let deviceId = "";
|
let deviceId = "";
|
||||||
|
|
||||||
getSession().then((sess) => {
|
getSession().then((sess) => {
|
||||||
if (sess) {
|
if (sess) {
|
||||||
session = sess;
|
session = sess;
|
||||||
initSentry(sess);
|
initSentry(sess);
|
||||||
sessionStore.set(sess);
|
sessionStore.set(sess);
|
||||||
deviceIdStore.set(sess.device_id!);
|
deviceIdStore.set(sess.device_id!);
|
||||||
deviceId = sess.device_id!;
|
deviceId = sess.device_id!;
|
||||||
if (sess.user) user.set(sess.user);
|
if (sess.user) user.set(sess.user);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let timer: NodeJS.Timer | null;
|
let timer: NodeJS.Timer | null;
|
||||||
|
|
||||||
async function updateSession(data: Session) {
|
async function updateSession(data: Session) {
|
||||||
sessionStore.update((val) => ({
|
sessionStore.update((val) => ({
|
||||||
...val,
|
...val,
|
||||||
...data
|
...data
|
||||||
}));
|
}));
|
||||||
|
|
||||||
initSentry(data);
|
initSentry(data);
|
||||||
await electronUpdateSession(data);
|
await electronUpdateSession(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function pollSession() {
|
async function pollSession() {
|
||||||
if (!timer) {
|
if (!timer) {
|
||||||
timer = setInterval(async () => {
|
timer = setInterval(async () => {
|
||||||
pollLoop++;
|
pollLoop++;
|
||||||
try {
|
try {
|
||||||
const data = await getDeviceAuth(deviceId);
|
const data = await getDeviceAuth(deviceId);
|
||||||
if (data.status === "SUCCESS") {
|
if (data.status === "SUCCESS") {
|
||||||
updateSession({
|
updateSession({
|
||||||
key: data.key,
|
key: data.key,
|
||||||
user: data.user
|
user: data.user
|
||||||
});
|
});
|
||||||
user.set(data.user!);
|
user.set(data.user!);
|
||||||
timer && clearInterval(timer);
|
timer && clearInterval(timer);
|
||||||
timer = null;
|
timer = null;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pollLoop > 20 && timer) {
|
if (pollLoop > 20 && timer) {
|
||||||
clearInterval(timer);
|
clearInterval(timer);
|
||||||
pollLoop = 0;
|
pollLoop = 0;
|
||||||
timer = null;
|
timer = null;
|
||||||
}
|
}
|
||||||
}, 2000);
|
}, 2000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearSession() {
|
function clearSession() {
|
||||||
updateSession({ key: undefined, user: undefined });
|
updateSession({ key: undefined, user: undefined });
|
||||||
user.set(undefined);
|
user.set(undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
user,
|
user,
|
||||||
session: sessionStore,
|
session: sessionStore,
|
||||||
deviceId,
|
deviceId,
|
||||||
deviceIdStore,
|
deviceIdStore,
|
||||||
pollSession,
|
pollSession,
|
||||||
clearSession,
|
clearSession,
|
||||||
updateSession
|
updateSession
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,55 +2,55 @@ import { writable } from "svelte/store";
|
||||||
import { goto } from "$app/navigation";
|
import { goto } from "$app/navigation";
|
||||||
|
|
||||||
export default function initNavStore() {
|
export default function initNavStore() {
|
||||||
const historyStore = writable<string[]>(["/"]);
|
const historyStore = writable<string[]>(["/"]);
|
||||||
|
|
||||||
let history = ["/"];
|
let history = ["/"];
|
||||||
|
|
||||||
historyStore.subscribe((v) => (history = v));
|
historyStore.subscribe((v) => (history = v));
|
||||||
|
|
||||||
const prevPathStore = writable<string>("");
|
const prevPathStore = writable<string>("");
|
||||||
const nextPathStore = writable<string>("");
|
const nextPathStore = writable<string>("");
|
||||||
|
|
||||||
let currentIndex = 0; // if non next/back click
|
let currentIndex = 0; // if non next/back click
|
||||||
|
|
||||||
let isMovingNext = false;
|
let isMovingNext = false;
|
||||||
let isMovingBack = false;
|
let isMovingBack = false;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
historyStore,
|
historyStore,
|
||||||
prevPath: prevPathStore,
|
prevPath: prevPathStore,
|
||||||
nextPath: nextPathStore,
|
nextPath: nextPathStore,
|
||||||
next: () => {
|
next: () => {
|
||||||
if (currentIndex < history.length - 1) {
|
if (currentIndex < history.length - 1) {
|
||||||
isMovingNext = true;
|
isMovingNext = true;
|
||||||
goto(history[currentIndex + 1]);
|
goto(history[currentIndex + 1]);
|
||||||
prevPathStore.set(history[currentIndex]);
|
prevPathStore.set(history[currentIndex]);
|
||||||
currentIndex++;
|
currentIndex++;
|
||||||
if (currentIndex >= history.length - 1) nextPathStore.set("");
|
if (currentIndex >= history.length - 1) nextPathStore.set("");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
back: () => {
|
back: () => {
|
||||||
if (currentIndex > 0) {
|
if (currentIndex > 0) {
|
||||||
isMovingBack = true;
|
isMovingBack = true;
|
||||||
goto(history[currentIndex - 1]);
|
goto(history[currentIndex - 1]);
|
||||||
nextPathStore.set(history[currentIndex]);
|
nextPathStore.set(history[currentIndex]);
|
||||||
currentIndex--;
|
currentIndex--;
|
||||||
if (currentIndex === 0) prevPathStore.set("");
|
if (currentIndex === 0) prevPathStore.set("");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
setNewPath: (newNextPath: string, _newPrevPath: string) => {
|
setNewPath: (newNextPath: string, _newPrevPath: string) => {
|
||||||
const oldCurrentPath = history[currentIndex];
|
const oldCurrentPath = history[currentIndex];
|
||||||
const isNavArrows = isMovingBack || isMovingNext;
|
const isNavArrows = isMovingBack || isMovingNext;
|
||||||
if (!isNavArrows && newNextPath !== oldCurrentPath) {
|
if (!isNavArrows && newNextPath !== oldCurrentPath) {
|
||||||
historyStore.update((history) => {
|
historyStore.update((history) => {
|
||||||
const cleanHistory = history.filter((_v, i) => i <= currentIndex);
|
const cleanHistory = history.filter((_v, i) => i <= currentIndex);
|
||||||
currentIndex = cleanHistory.length;
|
currentIndex = cleanHistory.length;
|
||||||
prevPathStore.set(cleanHistory[currentIndex - 1]);
|
prevPathStore.set(cleanHistory[currentIndex - 1]);
|
||||||
return [...cleanHistory, newNextPath];
|
return [...cleanHistory, newNextPath];
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
isMovingNext = false;
|
isMovingNext = false;
|
||||||
isMovingBack = false;
|
isMovingBack = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,51 +7,51 @@ import type { Notification } from "@tea/ui/types";
|
||||||
import { listenToChannel, relaunch } from "@native";
|
import { listenToChannel, relaunch } from "@native";
|
||||||
|
|
||||||
export default function initNotificationStore() {
|
export default function initNotificationStore() {
|
||||||
const notifications: Notification[] = [];
|
const notifications: Notification[] = [];
|
||||||
const { update, subscribe } = writable<Notification[]>([]);
|
const { update, subscribe } = writable<Notification[]>([]);
|
||||||
|
|
||||||
const remove = (id: string) => {
|
const remove = (id: string) => {
|
||||||
update((notifications) => notifications.filter((n) => n.id != id));
|
update((notifications) => notifications.filter((n) => n.id != id));
|
||||||
};
|
};
|
||||||
|
|
||||||
listenToChannel("message", (data: any) => {
|
listenToChannel("message", (data: any) => {
|
||||||
const { message, params }: { message: string; params: { [key: string]: string } } = data;
|
const { message, params }: { message: string; params: { [key: string]: string } } = data;
|
||||||
|
|
||||||
update((value) => {
|
update((value) => {
|
||||||
const newNotification: Notification = {
|
const newNotification: Notification = {
|
||||||
id: nanoid(4),
|
id: nanoid(4),
|
||||||
message,
|
message,
|
||||||
i18n_key: params["i18n_key"] || "",
|
i18n_key: params["i18n_key"] || "",
|
||||||
type: NotificationType.ACTION_BANNER,
|
type: NotificationType.ACTION_BANNER,
|
||||||
params
|
params
|
||||||
};
|
};
|
||||||
if (params.action) {
|
if (params.action) {
|
||||||
newNotification.callback_label = params.action.toUpperCase();
|
newNotification.callback_label = params.action.toUpperCase();
|
||||||
newNotification.callback = () => {
|
newNotification.callback = () => {
|
||||||
relaunch();
|
relaunch();
|
||||||
remove(newNotification.id); // not sure yet
|
remove(newNotification.id); // not sure yet
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return [...value, newNotification];
|
return [...value, newNotification];
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
notifications,
|
notifications,
|
||||||
subscribe,
|
subscribe,
|
||||||
remove,
|
remove,
|
||||||
add: (partialNotification: Partial<Notification>) => {
|
add: (partialNotification: Partial<Notification>) => {
|
||||||
if (!partialNotification.message) throw new Error("message is required");
|
if (!partialNotification.message) throw new Error("message is required");
|
||||||
|
|
||||||
const notification: Notification = {
|
const notification: Notification = {
|
||||||
id: nanoid(4),
|
id: nanoid(4),
|
||||||
i18n_key: partialNotification.i18n_key || "",
|
i18n_key: partialNotification.i18n_key || "",
|
||||||
type: NotificationType.MESSAGE,
|
type: NotificationType.MESSAGE,
|
||||||
message: partialNotification.message || "",
|
message: partialNotification.message || "",
|
||||||
...partialNotification
|
...partialNotification
|
||||||
};
|
};
|
||||||
|
|
||||||
update((values) => [notification, ...values]);
|
update((values) => [notification, ...values]);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,18 +3,18 @@ import type { GUIPackage, InstalledPackage, Packages } from "../types";
|
||||||
import { PackageStates } from "../types";
|
import { PackageStates } from "../types";
|
||||||
import Fuse from "fuse.js";
|
import Fuse from "fuse.js";
|
||||||
import {
|
import {
|
||||||
getPackage,
|
getPackage,
|
||||||
getDistPackages,
|
getDistPackages,
|
||||||
getInstalledPackages,
|
getInstalledPackages,
|
||||||
installPackage,
|
installPackage,
|
||||||
deletePackage,
|
deletePackage,
|
||||||
getPackageBottles,
|
getPackageBottles,
|
||||||
setBadgeCount,
|
setBadgeCount,
|
||||||
loadPackageCache,
|
loadPackageCache,
|
||||||
writePackageCache,
|
writePackageCache,
|
||||||
syncPantry,
|
syncPantry,
|
||||||
cacheImageURL,
|
cacheImageURL,
|
||||||
listenToChannel
|
listenToChannel
|
||||||
} from "@native";
|
} from "@native";
|
||||||
|
|
||||||
import { getReadme, getContributors, getRepoAsPackage } from "$libs/github";
|
import { getReadme, getContributors, getRepoAsPackage } from "$libs/github";
|
||||||
|
@ -31,337 +31,337 @@ import log from "$libs/logger";
|
||||||
const packageRefreshInterval = 1000 * 60 * 60; // 1 hour
|
const packageRefreshInterval = 1000 * 60 * 60; // 1 hour
|
||||||
|
|
||||||
export default function initPackagesStore() {
|
export default function initPackagesStore() {
|
||||||
let initialized = false;
|
let initialized = false;
|
||||||
let isDestroyed = false;
|
let isDestroyed = false;
|
||||||
let refreshTimeoutId: ReturnType<typeof setTimeout> | null = null;
|
let refreshTimeoutId: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
|
||||||
const packageMap = writable<Packages>({ version: "0", packages: {} });
|
const packageMap = writable<Packages>({ version: "0", packages: {} });
|
||||||
const packageList = derived(packageMap, ($packages) => Object.values($packages.packages));
|
const packageList = derived(packageMap, ($packages) => Object.values($packages.packages));
|
||||||
|
|
||||||
let packagesIndex: Fuse<GUIPackage>;
|
let packagesIndex: Fuse<GUIPackage>;
|
||||||
|
|
||||||
const updateAllPackages = (guiPkgs: GUIPackage[]) => {
|
const updateAllPackages = (guiPkgs: GUIPackage[]) => {
|
||||||
packageMap.update((pkgs) => {
|
packageMap.update((pkgs) => {
|
||||||
guiPkgs.forEach((pkg) => {
|
guiPkgs.forEach((pkg) => {
|
||||||
const oldPkg = pkgs.packages[pkg.full_name];
|
const oldPkg = pkgs.packages[pkg.full_name];
|
||||||
pkgs.packages[pkg.full_name] = { ...oldPkg, ...pkg };
|
pkgs.packages[pkg.full_name] = { ...oldPkg, ...pkg };
|
||||||
});
|
});
|
||||||
setBadgeCountFromPkgs(pkgs);
|
setBadgeCountFromPkgs(pkgs);
|
||||||
return pkgs;
|
return pkgs;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const updatePackage = (full_name: string, props: Partial<GUIPackage>, newVersion?: string) => {
|
const updatePackage = (full_name: string, props: Partial<GUIPackage>, newVersion?: string) => {
|
||||||
packageMap.update((pkgs) => {
|
packageMap.update((pkgs) => {
|
||||||
const pkg = pkgs.packages[full_name];
|
const pkg = pkgs.packages[full_name];
|
||||||
if (pkg) {
|
if (pkg) {
|
||||||
const updatedPkg = { ...pkg, ...props };
|
const updatedPkg = { ...pkg, ...props };
|
||||||
|
|
||||||
if (newVersion) {
|
if (newVersion) {
|
||||||
updatedPkg.installed_versions = addInstalledVersion(
|
updatedPkg.installed_versions = addInstalledVersion(
|
||||||
updatedPkg.installed_versions,
|
updatedPkg.installed_versions,
|
||||||
newVersion
|
newVersion
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
updatedPkg.state = getPackageState(updatedPkg);
|
updatedPkg.state = getPackageState(updatedPkg);
|
||||||
pkgs.packages[full_name] = updatedPkg;
|
pkgs.packages[full_name] = updatedPkg;
|
||||||
|
|
||||||
setBadgeCountFromPkgs(pkgs);
|
setBadgeCountFromPkgs(pkgs);
|
||||||
}
|
}
|
||||||
return pkgs;
|
return pkgs;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// getPackage state centralizes the logic for determining the state of the package based on the other properties
|
// getPackage state centralizes the logic for determining the state of the package based on the other properties
|
||||||
const getPackageState = (pkg: GUIPackage): PackageStates => {
|
const getPackageState = (pkg: GUIPackage): PackageStates => {
|
||||||
if (pkg.isUninstalling) {
|
if (pkg.isUninstalling) {
|
||||||
//TODO: maybe there should be an uninstalling state too? Although that needs UI/UX changes
|
//TODO: maybe there should be an uninstalling state too? Although that needs UI/UX changes
|
||||||
return PackageStates.AVAILABLE;
|
return PackageStates.AVAILABLE;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isUpToDate = pkg.version === pkg.installed_versions?.[0];
|
const isUpToDate = pkg.version === pkg.installed_versions?.[0];
|
||||||
|
|
||||||
if (isInstalling(pkg)) {
|
if (isInstalling(pkg)) {
|
||||||
const hasNoVersions = !pkg.installed_versions?.length;
|
const hasNoVersions = !pkg.installed_versions?.length;
|
||||||
if (hasNoVersions || isUpToDate) {
|
if (hasNoVersions || isUpToDate) {
|
||||||
return PackageStates.INSTALLING;
|
return PackageStates.INSTALLING;
|
||||||
}
|
}
|
||||||
return PackageStates.UPDATING;
|
return PackageStates.UPDATING;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!pkg.installed_versions?.length) {
|
if (!pkg.installed_versions?.length) {
|
||||||
return PackageStates.AVAILABLE;
|
return PackageStates.AVAILABLE;
|
||||||
}
|
}
|
||||||
|
|
||||||
return isUpToDate ? PackageStates.INSTALLED : PackageStates.NEEDS_UPDATE;
|
return isUpToDate ? PackageStates.INSTALLED : PackageStates.NEEDS_UPDATE;
|
||||||
};
|
};
|
||||||
|
|
||||||
const syncPackageData = async (guiPkg: Partial<GUIPackage> | undefined) => {
|
const syncPackageData = async (guiPkg: Partial<GUIPackage> | undefined) => {
|
||||||
if (!guiPkg) return;
|
if (!guiPkg) return;
|
||||||
|
|
||||||
const pkg = await getPackage(guiPkg.full_name!); // ATM: pkg only bottles and github:string
|
const pkg = await getPackage(guiPkg.full_name!); // ATM: pkg only bottles and github:string
|
||||||
const readmeMd = `# ${guiPkg.full_name} #
|
const readmeMd = `# ${guiPkg.full_name} #
|
||||||
To read more about this package go to [${guiPkg.homepage}](${guiPkg.homepage}).
|
To read more about this package go to [${guiPkg.homepage}](${guiPkg.homepage}).
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const updatedPackage: Partial<GUIPackage> = {
|
const updatedPackage: Partial<GUIPackage> = {
|
||||||
...pkg,
|
...pkg,
|
||||||
readme: {
|
readme: {
|
||||||
data: readmeMd,
|
data: readmeMd,
|
||||||
type: "md"
|
type: "md"
|
||||||
},
|
},
|
||||||
synced: true,
|
synced: true,
|
||||||
github: pkg.github
|
github: pkg.github
|
||||||
? trimGithubSlug(pkg.github)
|
? trimGithubSlug(pkg.github)
|
||||||
: pkg.full_name?.includes("github.com")
|
: pkg.full_name?.includes("github.com")
|
||||||
? trimGithubSlug(pkg.full_name.split("github.com/")[1])
|
? trimGithubSlug(pkg.full_name.split("github.com/")[1])
|
||||||
: ""
|
: ""
|
||||||
};
|
};
|
||||||
if (updatedPackage.github) {
|
if (updatedPackage.github) {
|
||||||
const [owner, repo] = updatedPackage.github.split("/");
|
const [owner, repo] = updatedPackage.github.split("/");
|
||||||
const [readme, contributors, repoData] = await Promise.all([
|
const [readme, contributors, repoData] = await Promise.all([
|
||||||
getReadme(owner, repo),
|
getReadme(owner, repo),
|
||||||
getContributors(owner, repo),
|
getContributors(owner, repo),
|
||||||
getRepoAsPackage(owner, repo)
|
getRepoAsPackage(owner, repo)
|
||||||
]);
|
]);
|
||||||
if (readme) {
|
if (readme) {
|
||||||
updatedPackage.readme = readme;
|
updatedPackage.readme = readme;
|
||||||
}
|
}
|
||||||
updatedPackage.contributors = contributors;
|
updatedPackage.contributors = contributors;
|
||||||
updatedPackage.license = repoData.license;
|
updatedPackage.license = repoData.license;
|
||||||
}
|
}
|
||||||
|
|
||||||
updatePackage(guiPkg.full_name!, updatedPackage);
|
updatePackage(guiPkg.full_name!, updatedPackage);
|
||||||
};
|
};
|
||||||
|
|
||||||
const init = async function () {
|
const init = async function () {
|
||||||
log.info("packages store: try initialize");
|
log.info("packages store: try initialize");
|
||||||
|
|
||||||
if (!initialized) {
|
if (!initialized) {
|
||||||
const cachedPkgs: Packages = await loadPackageCache();
|
const cachedPkgs: Packages = await loadPackageCache();
|
||||||
log.info(`Loaded ${Object.keys(cachedPkgs.packages).length} packages from cache`);
|
log.info(`Loaded ${Object.keys(cachedPkgs.packages).length} packages from cache`);
|
||||||
packageMap.set(cachedPkgs);
|
packageMap.set(cachedPkgs);
|
||||||
|
|
||||||
await refreshPackages();
|
await refreshPackages();
|
||||||
initialized = true;
|
initialized = true;
|
||||||
}
|
}
|
||||||
log.info("packages store: initialized!");
|
log.info("packages store: initialized!");
|
||||||
};
|
};
|
||||||
|
|
||||||
const refreshPackages = async () => {
|
const refreshPackages = async () => {
|
||||||
if (isDestroyed) return;
|
if (isDestroyed) return;
|
||||||
|
|
||||||
log.info("packages store: refreshing...");
|
log.info("packages store: refreshing...");
|
||||||
|
|
||||||
const pkgs = await getDistPackages();
|
const pkgs = await getDistPackages();
|
||||||
const guiPkgs: GUIPackage[] = pkgs.map((p) => ({
|
const guiPkgs: GUIPackage[] = pkgs.map((p) => ({
|
||||||
...p,
|
...p,
|
||||||
state: PackageStates.AVAILABLE
|
state: PackageStates.AVAILABLE
|
||||||
}));
|
}));
|
||||||
|
|
||||||
if (!initialized) {
|
if (!initialized) {
|
||||||
// set packages data so that i can render something in the UI already
|
// set packages data so that i can render something in the UI already
|
||||||
updateAllPackages(guiPkgs);
|
updateAllPackages(guiPkgs);
|
||||||
log.info("initialized packages store with ", guiPkgs.length);
|
log.info("initialized packages store with ", guiPkgs.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
packagesIndex = new Fuse(guiPkgs, {
|
packagesIndex = new Fuse(guiPkgs, {
|
||||||
keys: ["name", "full_name", "desc", "categories"],
|
keys: ["name", "full_name", "desc", "categories"],
|
||||||
minMatchCharLength: 3,
|
minMatchCharLength: 3,
|
||||||
threshold: 0.3
|
threshold: 0.3
|
||||||
});
|
});
|
||||||
log.info("refreshed packages fuse index");
|
log.info("refreshed packages fuse index");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const installedPkgs: InstalledPackage[] = await getInstalledPackages();
|
const installedPkgs: InstalledPackage[] = await getInstalledPackages();
|
||||||
|
|
||||||
log.info("updating state of packages");
|
log.info("updating state of packages");
|
||||||
for (const pkg of guiPkgs) {
|
for (const pkg of guiPkgs) {
|
||||||
const iPkg = installedPkgs.find((p) => p.full_name === pkg.full_name);
|
const iPkg = installedPkgs.find((p) => p.full_name === pkg.full_name);
|
||||||
if (iPkg) {
|
if (iPkg) {
|
||||||
pkg.installed_versions = iPkg.installed_versions;
|
pkg.installed_versions = iPkg.installed_versions;
|
||||||
updatePackage(pkg.full_name, {
|
updatePackage(pkg.full_name, {
|
||||||
installed_versions: iPkg.installed_versions
|
installed_versions: iPkg.installed_versions
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error(error);
|
log.error(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await withRetry(syncPantry);
|
await withRetry(syncPantry);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log.error(err);
|
log.error(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
refreshTimeoutId = setTimeout(() => refreshPackages(), packageRefreshInterval); // refresh every hour
|
refreshTimeoutId = setTimeout(() => refreshPackages(), packageRefreshInterval); // refresh every hour
|
||||||
};
|
};
|
||||||
|
|
||||||
// Destructor for the package store
|
// Destructor for the package store
|
||||||
const destroy = () => {
|
const destroy = () => {
|
||||||
isDestroyed = true;
|
isDestroyed = true;
|
||||||
if (refreshTimeoutId) {
|
if (refreshTimeoutId) {
|
||||||
clearTimeout(refreshTimeoutId);
|
clearTimeout(refreshTimeoutId);
|
||||||
}
|
}
|
||||||
log.info("packages store: destroyed");
|
log.info("packages store: destroyed");
|
||||||
};
|
};
|
||||||
|
|
||||||
const installPkg = async (pkg: GUIPackage, version?: string) => {
|
const installPkg = async (pkg: GUIPackage, version?: string) => {
|
||||||
const versionToInstall = version || pkg.version;
|
const versionToInstall = version || pkg.version;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
updatePackage(pkg.full_name, { install_progress_percentage: 0.01 });
|
updatePackage(pkg.full_name, { install_progress_percentage: 0.01 });
|
||||||
await installPackage(pkg, versionToInstall);
|
await installPackage(pkg, versionToInstall);
|
||||||
trackInstall(pkg.full_name);
|
trackInstall(pkg.full_name);
|
||||||
notificationStore.add({
|
notificationStore.add({
|
||||||
message: `Package ${pkg.full_name} v${versionToInstall} has been installed.`
|
message: `Package ${pkg.full_name} v${versionToInstall} has been installed.`
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error(error);
|
log.error(error);
|
||||||
let message = "Unknown Error";
|
let message = "Unknown Error";
|
||||||
if (error instanceof Error) message = error.message;
|
if (error instanceof Error) message = error.message;
|
||||||
trackInstallFailed(pkg.full_name, message || "unknown");
|
trackInstallFailed(pkg.full_name, message || "unknown");
|
||||||
|
|
||||||
notificationStore.add({
|
notificationStore.add({
|
||||||
message: `Package ${pkg.full_name} v${versionToInstall} failed to install.`,
|
message: `Package ${pkg.full_name} v${versionToInstall} failed to install.`,
|
||||||
type: NotificationType.ERROR
|
type: NotificationType.ERROR
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
updatePackage(pkg.full_name, { install_progress_percentage: 100 });
|
updatePackage(pkg.full_name, { install_progress_percentage: 100 });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const uninstallPkg = async (pkg: GUIPackage) => {
|
const uninstallPkg = async (pkg: GUIPackage) => {
|
||||||
let fakeTimer: NodeJS.Timer | null = null;
|
let fakeTimer: NodeJS.Timer | null = null;
|
||||||
try {
|
try {
|
||||||
fakeTimer = withFakeLoader(pkg, (progress) => {
|
fakeTimer = withFakeLoader(pkg, (progress) => {
|
||||||
updatePackage(pkg.full_name, {
|
updatePackage(pkg.full_name, {
|
||||||
install_progress_percentage: progress,
|
install_progress_percentage: progress,
|
||||||
isUninstalling: true
|
isUninstalling: true
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const v of pkg.installed_versions || []) {
|
for (const v of pkg.installed_versions || []) {
|
||||||
await deletePkg(pkg, v);
|
await deletePkg(pkg, v);
|
||||||
}
|
}
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
updatePackage(pkg.full_name, {
|
updatePackage(pkg.full_name, {
|
||||||
installed_versions: []
|
installed_versions: []
|
||||||
});
|
});
|
||||||
}, 3000);
|
}, 3000);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error(error);
|
log.error(error);
|
||||||
notificationStore.add({
|
notificationStore.add({
|
||||||
message: `Package ${pkg.full_name} failed to uninstall.`,
|
message: `Package ${pkg.full_name} failed to uninstall.`,
|
||||||
type: NotificationType.ERROR
|
type: NotificationType.ERROR
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
fakeTimer && clearTimeout(fakeTimer);
|
fakeTimer && clearTimeout(fakeTimer);
|
||||||
updatePackage(pkg.full_name, { install_progress_percentage: 0, isUninstalling: false });
|
updatePackage(pkg.full_name, { install_progress_percentage: 0, isUninstalling: false });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchPackageBottles = async (pkgName: string) => {
|
const fetchPackageBottles = async (pkgName: string) => {
|
||||||
// TODO: this api should take an architecture argument or else an architecture filter should be applied downstreawm
|
// TODO: this api should take an architecture argument or else an architecture filter should be applied downstreawm
|
||||||
const bottles = await getPackageBottles(pkgName);
|
const bottles = await getPackageBottles(pkgName);
|
||||||
if (bottles?.length) {
|
if (bottles?.length) {
|
||||||
updatePackage(pkgName, { bottles });
|
updatePackage(pkgName, { bottles });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const deletePkg = async (pkg: GUIPackage, version: string) => {
|
const deletePkg = async (pkg: GUIPackage, version: string) => {
|
||||||
log.info("deleting package: ", pkg.full_name, " version: ", version);
|
log.info("deleting package: ", pkg.full_name, " version: ", version);
|
||||||
await deletePackage({ fullName: pkg.full_name, version });
|
await deletePackage({ fullName: pkg.full_name, version });
|
||||||
updatePackage(pkg.full_name, {
|
updatePackage(pkg.full_name, {
|
||||||
installed_versions: pkg.installed_versions?.filter((v) => v !== version)
|
installed_versions: pkg.installed_versions?.filter((v) => v !== version)
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const writePackageCacheWithDebounce = withDebounce(writePackageCache);
|
const writePackageCacheWithDebounce = withDebounce(writePackageCache);
|
||||||
packageMap.subscribe(async (pkgs) => {
|
packageMap.subscribe(async (pkgs) => {
|
||||||
writePackageCacheWithDebounce(pkgs);
|
writePackageCacheWithDebounce(pkgs);
|
||||||
});
|
});
|
||||||
|
|
||||||
const cachePkgImage = async (pkg: GUIPackage): Promise<string> => {
|
const cachePkgImage = async (pkg: GUIPackage): Promise<string> => {
|
||||||
let cacheFileURL = "";
|
let cacheFileURL = "";
|
||||||
updatePackage(pkg.full_name, { cached_image_url: "" });
|
updatePackage(pkg.full_name, { cached_image_url: "" });
|
||||||
if (pkg.thumb_image_url && !pkg.thumb_image_url.includes("package-thumb-nolabel4.jpg")) {
|
if (pkg.thumb_image_url && !pkg.thumb_image_url.includes("package-thumb-nolabel4.jpg")) {
|
||||||
const result = await cacheImageURL(pkg.thumb_image_url);
|
const result = await cacheImageURL(pkg.thumb_image_url);
|
||||||
if (result) {
|
if (result) {
|
||||||
cacheFileURL = result;
|
cacheFileURL = result;
|
||||||
updatePackage(pkg.full_name, { cached_image_url: cacheFileURL });
|
updatePackage(pkg.full_name, { cached_image_url: cacheFileURL });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return cacheFileURL;
|
return cacheFileURL;
|
||||||
};
|
};
|
||||||
|
|
||||||
listenToChannel("install-progress", ({ full_name, progress }: any) => {
|
listenToChannel("install-progress", ({ full_name, progress }: any) => {
|
||||||
if (!full_name) {
|
if (!full_name) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
updatePackage(full_name, { install_progress_percentage: progress });
|
updatePackage(full_name, { install_progress_percentage: progress });
|
||||||
});
|
});
|
||||||
|
|
||||||
listenToChannel("pkg-installed", ({ full_name, version }: any) => {
|
listenToChannel("pkg-installed", ({ full_name, version }: any) => {
|
||||||
if (!full_name) {
|
if (!full_name) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
updatePackage(full_name, {}, version);
|
updatePackage(full_name, {}, version);
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
packageList,
|
packageList,
|
||||||
search: async (term: string, limit = 5): Promise<GUIPackage[]> => {
|
search: async (term: string, limit = 5): Promise<GUIPackage[]> => {
|
||||||
if (!term || !packagesIndex) return [];
|
if (!term || !packagesIndex) return [];
|
||||||
// TODO: if online, use algolia else use Fuse
|
// TODO: if online, use algolia else use Fuse
|
||||||
const res = packagesIndex.search(term, { limit });
|
const res = packagesIndex.search(term, { limit });
|
||||||
const matchingPackages: GUIPackage[] = res.map((v) => v.item);
|
const matchingPackages: GUIPackage[] = res.map((v) => v.item);
|
||||||
return matchingPackages;
|
return matchingPackages;
|
||||||
},
|
},
|
||||||
fetchPackageBottles,
|
fetchPackageBottles,
|
||||||
init,
|
init,
|
||||||
installPkg,
|
installPkg,
|
||||||
uninstallPkg,
|
uninstallPkg,
|
||||||
syncPackageData,
|
syncPackageData,
|
||||||
deletePkg,
|
deletePkg,
|
||||||
destroy,
|
destroy,
|
||||||
cachePkgImage
|
cachePkgImage
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// This is only used for uninstall now
|
// This is only used for uninstall now
|
||||||
export const withFakeLoader = (
|
export const withFakeLoader = (
|
||||||
pkg: GUIPackage,
|
pkg: GUIPackage,
|
||||||
callback: (progress: number) => void
|
callback: (progress: number) => void
|
||||||
): NodeJS.Timer => {
|
): NodeJS.Timer => {
|
||||||
let fakeLoadingProgress = 1;
|
let fakeLoadingProgress = 1;
|
||||||
const ms = 100;
|
const ms = 100;
|
||||||
const assumedDlSpeedMb = 1024 * 1024 * 3; // 3mbps
|
const assumedDlSpeedMb = 1024 * 1024 * 3; // 3mbps
|
||||||
const size = pkg?.bottles?.length ? pkg.bottles[0].bytes : assumedDlSpeedMb * 10;
|
const size = pkg?.bottles?.length ? pkg.bottles[0].bytes : assumedDlSpeedMb * 10;
|
||||||
const eta = size / assumedDlSpeedMb;
|
const eta = size / assumedDlSpeedMb;
|
||||||
|
|
||||||
const increment = 1 / eta / 10;
|
const increment = 1 / eta / 10;
|
||||||
|
|
||||||
const fakeTimer = setInterval(() => {
|
const fakeTimer = setInterval(() => {
|
||||||
const progressLeft = 100 - fakeLoadingProgress;
|
const progressLeft = 100 - fakeLoadingProgress;
|
||||||
const addProgress = progressLeft * increment;
|
const addProgress = progressLeft * increment;
|
||||||
fakeLoadingProgress = fakeLoadingProgress + addProgress;
|
fakeLoadingProgress = fakeLoadingProgress + addProgress;
|
||||||
callback(fakeLoadingProgress);
|
callback(fakeLoadingProgress);
|
||||||
}, ms);
|
}, ms);
|
||||||
|
|
||||||
return fakeTimer;
|
return fakeTimer;
|
||||||
};
|
};
|
||||||
|
|
||||||
const setBadgeCountFromPkgs = (pkgs: Packages) => {
|
const setBadgeCountFromPkgs = (pkgs: Packages) => {
|
||||||
try {
|
try {
|
||||||
const needsUpdateCount = Object.values(pkgs.packages).filter(
|
const needsUpdateCount = Object.values(pkgs.packages).filter(
|
||||||
(p) => p.state === PackageStates.NEEDS_UPDATE
|
(p) => p.state === PackageStates.NEEDS_UPDATE
|
||||||
).length;
|
).length;
|
||||||
setBadgeCount(needsUpdateCount);
|
setBadgeCount(needsUpdateCount);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error(error);
|
log.error(error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -3,19 +3,19 @@ import { getAutoUpdateStatus, listenToChannel } from "@native";
|
||||||
import { writable } from "svelte/store";
|
import { writable } from "svelte/store";
|
||||||
|
|
||||||
export default function initAppUpdateStore() {
|
export default function initAppUpdateStore() {
|
||||||
const updateStatus = writable<AutoUpdateStatus>({ status: "up-to-date" });
|
const updateStatus = writable<AutoUpdateStatus>({ status: "up-to-date" });
|
||||||
|
|
||||||
getAutoUpdateStatus().then((status: AutoUpdateStatus) => {
|
getAutoUpdateStatus().then((status: AutoUpdateStatus) => {
|
||||||
updateStatus.update(() => status);
|
updateStatus.update(() => status);
|
||||||
});
|
});
|
||||||
|
|
||||||
listenToChannel("app-update-status", (status: AutoUpdateStatus) => {
|
listenToChannel("app-update-status", (status: AutoUpdateStatus) => {
|
||||||
if (status.status) {
|
if (status.status) {
|
||||||
updateStatus.update(() => status);
|
updateStatus.update(() => status);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
updateStatus
|
updateStatus
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,9 +3,9 @@ import translations from "./translations.json";
|
||||||
|
|
||||||
/** @type {import('sveltekit-i18n').Config} */
|
/** @type {import('sveltekit-i18n').Config} */
|
||||||
const config = {
|
const config = {
|
||||||
initLocale: "en",
|
initLocale: "en",
|
||||||
fallbackLocale: "en",
|
fallbackLocale: "en",
|
||||||
translations
|
translations
|
||||||
};
|
};
|
||||||
|
|
||||||
export const { t, l, locales, locale } = new i18n(config);
|
export const { t, l, locales, locale } = new i18n(config);
|
||||||
|
|
|
@ -1,83 +1,83 @@
|
||||||
{
|
{
|
||||||
"en": {
|
"en": {
|
||||||
"lang": {
|
"lang": {
|
||||||
"en": "English"
|
"en": "English"
|
||||||
},
|
},
|
||||||
"store-search-placeholder": "search packages",
|
"store-search-placeholder": "search packages",
|
||||||
"package": {
|
"package": {
|
||||||
"update-all": "UPDATE ALL",
|
"update-all": "UPDATE ALL",
|
||||||
"cta-AVAILABLE": "INSTALL",
|
"cta-AVAILABLE": "INSTALL",
|
||||||
"cta-INSTALLED": "INSTALLED",
|
"cta-INSTALLED": "INSTALLED",
|
||||||
"cta-INSTALLING": "INSTALLING",
|
"cta-INSTALLING": "INSTALLING",
|
||||||
"cta-UNINSTALLED": "RE-INSTALL",
|
"cta-UNINSTALLED": "RE-INSTALL",
|
||||||
"cta-UNINSTALL": "UNINSTALL",
|
"cta-UNINSTALL": "UNINSTALL",
|
||||||
"cta-NEEDS_UPDATE": "UPDATE",
|
"cta-NEEDS_UPDATE": "UPDATE",
|
||||||
"cta-UPDATING": "UPDATING",
|
"cta-UPDATING": "UPDATING",
|
||||||
"cta-PRUNE": "PRUNE",
|
"cta-PRUNE": "PRUNE",
|
||||||
"cta-PRUNING": "PRUNING"
|
"cta-PRUNING": "PRUNING"
|
||||||
},
|
},
|
||||||
"footer": {
|
"footer": {
|
||||||
"quick-links-title": "quick links",
|
"quick-links-title": "quick links",
|
||||||
"about-tea-store": "about the tea store",
|
"about-tea-store": "about the tea store",
|
||||||
"report-a-problem": "report a problem",
|
"report-a-problem": "report a problem",
|
||||||
"visit-website": "visit tea.xyz",
|
"visit-website": "visit tea.xyz",
|
||||||
"terms-services": "terms & services",
|
"terms-services": "terms & services",
|
||||||
"privacy-policy": "privacy-policy"
|
"privacy-policy": "privacy-policy"
|
||||||
},
|
},
|
||||||
"documentation": {
|
"documentation": {
|
||||||
"title": "documentation",
|
"title": "documentation",
|
||||||
"featured-courses-title": "featured courses",
|
"featured-courses-title": "featured courses",
|
||||||
"workshops": "workshops"
|
"workshops": "workshops"
|
||||||
},
|
},
|
||||||
"view-all": "view all",
|
"view-all": "view all",
|
||||||
"sorting": {
|
"sorting": {
|
||||||
"label": "Sort by",
|
"label": "Sort by",
|
||||||
"popularity": "Most popular",
|
"popularity": "Most popular",
|
||||||
"most-recent": "Most recent"
|
"most-recent": "Most recent"
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
"home": "home",
|
"home": "home",
|
||||||
"all": "All",
|
"all": "All",
|
||||||
"articles": "Articles",
|
"articles": "Articles",
|
||||||
"workshops": "Workshops",
|
"workshops": "Workshops",
|
||||||
"details": "details",
|
"details": "details",
|
||||||
"versions": "versions",
|
"versions": "versions",
|
||||||
"metadata": "Metadata",
|
"metadata": "Metadata",
|
||||||
"homepage": "Homepage",
|
"homepage": "Homepage",
|
||||||
"documentation": "Documentation",
|
"documentation": "Documentation",
|
||||||
"github-repository": "Github Repository",
|
"github-repository": "Github Repository",
|
||||||
"contributors": "Contributors",
|
"contributors": "Contributors",
|
||||||
"view-on-github": "VIEW ON GITHUB"
|
"view-on-github": "VIEW ON GITHUB"
|
||||||
},
|
},
|
||||||
"notification": {
|
"notification": {
|
||||||
"gui-downloading": "A new tea gui({{version}}) is being downloaded. Please don't close the app.",
|
"gui-downloading": "A new tea gui({{version}}) is being downloaded. Please don't close the app.",
|
||||||
"gui-downloaded": "A new tea gui({{version}}) is now available. Relaunch the app to update."
|
"gui-downloaded": "A new tea gui({{version}}) is now available. Relaunch the app to update."
|
||||||
},
|
},
|
||||||
"side-menu-title": {
|
"side-menu-title": {
|
||||||
"discover": "discover",
|
"discover": "discover",
|
||||||
"all": "All Packages",
|
"all": "All Packages",
|
||||||
"installed": "Installed Packages",
|
"installed": "Installed Packages",
|
||||||
"installed_updates_available": "Available Updates",
|
"installed_updates_available": "Available Updates",
|
||||||
"recently_updated": "Recently Updated",
|
"recently_updated": "Recently Updated",
|
||||||
"new_packages": "New Packages",
|
"new_packages": "New Packages",
|
||||||
"popular": "Popular",
|
"popular": "Popular",
|
||||||
"featured": "Featured",
|
"featured": "Featured",
|
||||||
"essentials": "Essentials",
|
"essentials": "Essentials",
|
||||||
"starstruck": "Starstruck Heavyweights",
|
"starstruck": "Starstruck Heavyweights",
|
||||||
"made_by_tea": "made by tea"
|
"made_by_tea": "made by tea"
|
||||||
},
|
},
|
||||||
"tags": {
|
"tags": {
|
||||||
"discover": "discover",
|
"discover": "discover",
|
||||||
"all": "all",
|
"all": "all",
|
||||||
"installed": "installed",
|
"installed": "installed",
|
||||||
"installed_updates_available": "Updates available",
|
"installed_updates_available": "Updates available",
|
||||||
"recently_updated": "Recently updated",
|
"recently_updated": "Recently updated",
|
||||||
"new_packages": "New packages",
|
"new_packages": "New packages",
|
||||||
"popular": "Popular",
|
"popular": "Popular",
|
||||||
"featured": "Featured",
|
"featured": "Featured",
|
||||||
"essentials": "Essentials",
|
"essentials": "Essentials",
|
||||||
"starstruck": "Starstruck",
|
"starstruck": "Starstruck",
|
||||||
"made_by_tea": "Made by tea"
|
"made_by_tea": "Made by tea"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,77 +6,77 @@
|
||||||
import type { Package, Developer } from "@tea/ui/types";
|
import type { Package, Developer } from "@tea/ui/types";
|
||||||
|
|
||||||
export enum PackageStates {
|
export enum PackageStates {
|
||||||
AVAILABLE = "AVAILABLE",
|
AVAILABLE = "AVAILABLE",
|
||||||
INSTALLED = "INSTALLED",
|
INSTALLED = "INSTALLED",
|
||||||
INSTALLING = "INSTALLING",
|
INSTALLING = "INSTALLING",
|
||||||
NEEDS_UPDATE = "NEEDS_UPDATE",
|
NEEDS_UPDATE = "NEEDS_UPDATE",
|
||||||
UPDATING = "UPDATING"
|
UPDATING = "UPDATING"
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Packages = {
|
export type Packages = {
|
||||||
version: string;
|
version: string;
|
||||||
packages: { [full_name: string]: GUIPackage };
|
packages: { [full_name: string]: GUIPackage };
|
||||||
};
|
};
|
||||||
|
|
||||||
export type GUIPackage = Package & {
|
export type GUIPackage = Package & {
|
||||||
state: PackageStates;
|
state: PackageStates;
|
||||||
installed_versions?: string[];
|
installed_versions?: string[];
|
||||||
synced?: boolean;
|
synced?: boolean;
|
||||||
install_progress_percentage?: number;
|
install_progress_percentage?: number;
|
||||||
isUninstalling?: boolean;
|
isUninstalling?: boolean;
|
||||||
cached_image_url?: string;
|
cached_image_url?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Course = {
|
export type Course = {
|
||||||
title: string;
|
title: string;
|
||||||
sub_title: string;
|
sub_title: string;
|
||||||
banner_image_url: string;
|
banner_image_url: string;
|
||||||
link: string;
|
link: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Category = {
|
export type Category = {
|
||||||
label: string;
|
label: string;
|
||||||
cta_label: string;
|
cta_label: string;
|
||||||
packages: GUIPackage[];
|
packages: GUIPackage[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export enum AuthStatus {
|
export enum AuthStatus {
|
||||||
UNKNOWN = "UNKNOWN",
|
UNKNOWN = "UNKNOWN",
|
||||||
PENDING = "PENDING",
|
PENDING = "PENDING",
|
||||||
SUCCESS = "SUCCESS",
|
SUCCESS = "SUCCESS",
|
||||||
FAILED = "FAILED"
|
FAILED = "FAILED"
|
||||||
}
|
}
|
||||||
|
|
||||||
export type DeviceAuth = {
|
export type DeviceAuth = {
|
||||||
status: AuthStatus;
|
status: AuthStatus;
|
||||||
user?: Developer;
|
user?: Developer;
|
||||||
key: string;
|
key: string;
|
||||||
};
|
};
|
||||||
export interface Session {
|
export interface Session {
|
||||||
device_id?: string;
|
device_id?: string;
|
||||||
key?: string;
|
key?: string;
|
||||||
user?: Developer;
|
user?: Developer;
|
||||||
locale?: string;
|
locale?: string;
|
||||||
hide_welcome?: boolean;
|
hide_welcome?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum SideMenuOptions {
|
export enum SideMenuOptions {
|
||||||
discover = "discover",
|
discover = "discover",
|
||||||
all = "all",
|
all = "all",
|
||||||
installed = "installed",
|
installed = "installed",
|
||||||
installed_updates_available = "installed_updates_available",
|
installed_updates_available = "installed_updates_available",
|
||||||
recently_updated = "recently_updated",
|
recently_updated = "recently_updated",
|
||||||
new_packages = "new_packages",
|
new_packages = "new_packages",
|
||||||
popular = "popular",
|
popular = "popular",
|
||||||
featured = "featured",
|
featured = "featured",
|
||||||
essentials = "essentials",
|
essentials = "essentials",
|
||||||
starstruck = "starstruck",
|
starstruck = "starstruck",
|
||||||
made_by_tea = "made_by_tea"
|
made_by_tea = "made_by_tea"
|
||||||
}
|
}
|
||||||
|
|
||||||
export type InstalledPackage = Required<Pick<GUIPackage, "full_name" | "installed_versions">>;
|
export type InstalledPackage = Required<Pick<GUIPackage, "full_name" | "installed_versions">>;
|
||||||
|
|
||||||
export type AutoUpdateStatus = {
|
export type AutoUpdateStatus = {
|
||||||
status: "up-to-date" | "available" | "ready";
|
status: "up-to-date" | "available" | "ready";
|
||||||
version?: string;
|
version?: string;
|
||||||
};
|
};
|
||||||
|
|
|
@ -3,27 +3,27 @@ import log from "$libs/logger";
|
||||||
type DebounceableFunc = (...args: any[]) => void;
|
type DebounceableFunc = (...args: any[]) => void;
|
||||||
|
|
||||||
export type DebounceOptions = {
|
export type DebounceOptions = {
|
||||||
lingerMs?: number;
|
lingerMs?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function withDebounce(
|
export default function withDebounce(
|
||||||
f: DebounceableFunc,
|
f: DebounceableFunc,
|
||||||
{ lingerMs = 1000 }: DebounceOptions = {}
|
{ lingerMs = 1000 }: DebounceOptions = {}
|
||||||
) {
|
) {
|
||||||
let timeoutId: ReturnType<typeof setTimeout> | undefined = undefined;
|
let timeoutId: ReturnType<typeof setTimeout> | undefined = undefined;
|
||||||
|
|
||||||
return (...args: any[]) => {
|
return (...args: any[]) => {
|
||||||
if (timeoutId) {
|
if (timeoutId) {
|
||||||
clearTimeout(timeoutId);
|
clearTimeout(timeoutId);
|
||||||
}
|
}
|
||||||
|
|
||||||
timeoutId = setTimeout(() => {
|
timeoutId = setTimeout(() => {
|
||||||
try {
|
try {
|
||||||
f(...args);
|
f(...args);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
//swallow the error, there is no good way to signal failure to the caller
|
//swallow the error, there is no good way to signal failure to the caller
|
||||||
log.error(err);
|
log.error(err);
|
||||||
}
|
}
|
||||||
}, lingerMs);
|
}, lingerMs);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,34 +1,34 @@
|
||||||
import log from "$libs/logger";
|
import log from "$libs/logger";
|
||||||
|
|
||||||
export type RetryOptions = {
|
export type RetryOptions = {
|
||||||
// Number of times to retry. default 10
|
// Number of times to retry. default 10
|
||||||
maxRetries?: number;
|
maxRetries?: number;
|
||||||
// Initial delay in ms. default 100
|
// Initial delay in ms. default 100
|
||||||
initialDelayMs?: number;
|
initialDelayMs?: number;
|
||||||
// Maximum delay in ms. default 5000
|
// Maximum delay in ms. default 5000
|
||||||
maxDelayMs?: number;
|
maxDelayMs?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Retry a function up to maxRetries times, with exponential backoff
|
// Retry a function up to maxRetries times, with exponential backoff
|
||||||
// With defaults retry cadence will look like this:
|
// With defaults retry cadence will look like this:
|
||||||
// 100ms, 200ms, 400ms, 800ms, 1600ms, 3200ms, 5000ms, 5000ms, 5000ms, 5000ms
|
// 100ms, 200ms, 400ms, 800ms, 1600ms, 3200ms, 5000ms, 5000ms, 5000ms, 5000ms
|
||||||
export default async function withRetry<T>(
|
export default async function withRetry<T>(
|
||||||
fn: () => Promise<T>,
|
fn: () => Promise<T>,
|
||||||
{ maxRetries = 10, initialDelayMs = 100, maxDelayMs = 5000 }: RetryOptions = {}
|
{ maxRetries = 10, initialDelayMs = 100, maxDelayMs = 5000 }: RetryOptions = {}
|
||||||
) {
|
) {
|
||||||
let retries = 0;
|
let retries = 0;
|
||||||
let currentDelay = initialDelayMs;
|
let currentDelay = initialDelayMs;
|
||||||
while (retries <= maxRetries) {
|
while (retries <= maxRetries) {
|
||||||
try {
|
try {
|
||||||
return await fn();
|
return await fn();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log.error(err);
|
log.error(err);
|
||||||
retries++;
|
retries++;
|
||||||
await wait(currentDelay);
|
await wait(currentDelay);
|
||||||
currentDelay = Math.min(currentDelay * 2, maxDelayMs);
|
currentDelay = Math.min(currentDelay * 2, maxDelayMs);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
throw new Error(`Failed after ${maxRetries} retries`);
|
throw new Error(`Failed after ${maxRetries} retries`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
|
|
@ -6,42 +6,42 @@ import { getSession } from "$libs/stores/auth";
|
||||||
export const baseUrl = "https://api.tea.xyz/v1";
|
export const baseUrl = "https://api.tea.xyz/v1";
|
||||||
|
|
||||||
export async function get<T>(
|
export async function get<T>(
|
||||||
urlPath: string,
|
urlPath: string,
|
||||||
params?: { [key: string]: string }
|
params?: { [key: string]: string }
|
||||||
): Promise<T | null> {
|
): Promise<T | null> {
|
||||||
console.log(`GET /v1/${urlPath}`);
|
console.log(`GET /v1/${urlPath}`);
|
||||||
|
|
||||||
const [session] = await Promise.all([getSession()]);
|
const [session] = await Promise.all([getSession()]);
|
||||||
|
|
||||||
const headers =
|
const headers =
|
||||||
session?.device_id && session?.user
|
session?.device_id && session?.user
|
||||||
? await getHeaders(`GET/${urlPath}`, session)
|
? await getHeaders(`GET/${urlPath}`, session)
|
||||||
: { Authorization: "public " };
|
: { Authorization: "public " };
|
||||||
|
|
||||||
const req = await axios.request({
|
const req = await axios.request({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
baseURL: "https://api.tea.xyz",
|
baseURL: "https://api.tea.xyz",
|
||||||
url: ["v1", ...urlPath.split("/")].filter((p) => p).join("/"),
|
url: ["v1", ...urlPath.split("/")].filter((p) => p).join("/"),
|
||||||
headers,
|
headers,
|
||||||
params,
|
params,
|
||||||
validateStatus: (status) => status >= 200 && status < 300
|
validateStatus: (status) => status >= 200 && status < 300
|
||||||
});
|
});
|
||||||
|
|
||||||
return req.data as T;
|
return req.data as T;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getHeaders(path: string, session: Session) {
|
async function getHeaders(path: string, session: Session) {
|
||||||
const unixMs = new Date().getTime();
|
const unixMs = new Date().getTime();
|
||||||
const unixHexSecs = Math.round(unixMs / 1000).toString(16); // hex
|
const unixHexSecs = Math.round(unixMs / 1000).toString(16); // hex
|
||||||
const deviceId = session.device_id?.split("-")[0];
|
const deviceId = session.device_id?.split("-")[0];
|
||||||
const preHash = [unixHexSecs, session.key, deviceId, path].join("");
|
const preHash = [unixHexSecs, session.key, deviceId, path].join("");
|
||||||
|
|
||||||
const Authorization = bcrypt.hashSync(preHash, 10);
|
const Authorization = bcrypt.hashSync(preHash, 10);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
Authorization,
|
Authorization,
|
||||||
["tea-ts"]: unixMs.toString(),
|
["tea-ts"]: unixMs.toString(),
|
||||||
["tea-uid"]: session.user?.developer_id,
|
["tea-uid"]: session.user?.developer_id,
|
||||||
["tea-gui_id"]: session.device_id
|
["tea-gui_id"]: session.device_id
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,135 +1,135 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import "$appcss";
|
import "$appcss";
|
||||||
import { goto } from "$app/navigation";
|
import { goto } from "$app/navigation";
|
||||||
import { initSentry } from "$libs/sentry";
|
import { initSentry } from "$libs/sentry";
|
||||||
import { navigating } from "$app/stores";
|
import { navigating } from "$app/stores";
|
||||||
import { afterNavigate } from "$app/navigation";
|
import { afterNavigate } from "$app/navigation";
|
||||||
import TopBar from "$components/top-bar/top-bar.svelte";
|
import TopBar from "$components/top-bar/top-bar.svelte";
|
||||||
import { navStore, packagesStore, searchStore } from "$libs/stores";
|
import { navStore, packagesStore, searchStore } from "$libs/stores";
|
||||||
import { listenToChannel } from "@native";
|
import { listenToChannel } from "@native";
|
||||||
import Mousetrap from "mousetrap";
|
import Mousetrap from "mousetrap";
|
||||||
|
|
||||||
import SearchPopupResults from "$components/search-popup-results/search-popup-results.svelte";
|
import SearchPopupResults from "$components/search-popup-results/search-popup-results.svelte";
|
||||||
import { getProtocolPath } from "@native";
|
import { getProtocolPath } from "@native";
|
||||||
|
|
||||||
import { onDestroy, onMount } from "svelte";
|
import { onDestroy, onMount } from "svelte";
|
||||||
|
|
||||||
let view: HTMLElement;
|
let view: HTMLElement;
|
||||||
|
|
||||||
const { setNewPath } = navStore;
|
const { setNewPath } = navStore;
|
||||||
const { searching } = searchStore;
|
const { searching } = searchStore;
|
||||||
|
|
||||||
$: if ($navigating) view.scrollTop = 0;
|
$: if ($navigating) view.scrollTop = 0;
|
||||||
|
|
||||||
afterNavigate(({ from, to }) => {
|
afterNavigate(({ from, to }) => {
|
||||||
if (to && to?.route.id && from && from?.url) {
|
if (to && to?.route.id && from && from?.url) {
|
||||||
const nextPath = to.url.href.replace(to.url.origin, "");
|
const nextPath = to.url.href.replace(to.url.origin, "");
|
||||||
const fromPath = from?.url.href.replace(from.url.origin, "");
|
const fromPath = from?.url.href.replace(from.url.origin, "");
|
||||||
setNewPath(nextPath, fromPath || "/");
|
setNewPath(nextPath, fromPath || "/");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const syncPath = async () => {
|
const syncPath = async () => {
|
||||||
// used by the tea:// protocol to suggest a path to open
|
// used by the tea:// protocol to suggest a path to open
|
||||||
const path = await getProtocolPath();
|
const path = await getProtocolPath();
|
||||||
if (path) goto(path);
|
if (path) goto(path);
|
||||||
};
|
};
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
// used by the tea:// protocol to suggest a path to open
|
// used by the tea:// protocol to suggest a path to open
|
||||||
syncPath();
|
syncPath();
|
||||||
listenToChannel("sync-path", syncPath);
|
listenToChannel("sync-path", syncPath);
|
||||||
Mousetrap.bind(["command+k", "ctrl+k"], function () {
|
Mousetrap.bind(["command+k", "ctrl+k"], function () {
|
||||||
searchStore.searching.set(!$searching);
|
searchStore.searching.set(!$searching);
|
||||||
// return false to prevent default browser behavior
|
// return false to prevent default browser behavior
|
||||||
// and stop event from bubbling
|
// and stop event from bubbling
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
Mousetrap.bind(["esc"], function () {
|
Mousetrap.bind(["esc"], function () {
|
||||||
searchStore.searching.set(false);
|
searchStore.searching.set(false);
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
packagesStore.init();
|
packagesStore.init();
|
||||||
initSentry();
|
initSentry();
|
||||||
});
|
});
|
||||||
|
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
packagesStore.destroy();
|
packagesStore.destroy();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div id="main-layout" class="font-inter border-gray rounded-xl border transition-all">
|
<div id="main-layout" class="font-inter border-gray rounded-xl border transition-all">
|
||||||
<TopBar />
|
<TopBar />
|
||||||
<div class="scroll-manager relative z-10">
|
<div class="scroll-manager relative z-10">
|
||||||
<section class="relative" bind:this={view}>
|
<section class="relative" bind:this={view}>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
<SearchPopupResults />
|
<SearchPopupResults />
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
#main-layout {
|
#main-layout {
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scroll-manager {
|
.scroll-manager {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
padding-right: 8px;
|
padding-right: 8px;
|
||||||
}
|
}
|
||||||
section {
|
section {
|
||||||
height: calc(100vh - 50px); /* win.height - header*/
|
height: calc(100vh - 50px); /* win.height - header*/
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
slot {
|
slot {
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
div {
|
div {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
aside {
|
aside {
|
||||||
top: 52px;
|
top: 52px;
|
||||||
right: 5px;
|
right: 5px;
|
||||||
width: 210px;
|
width: 210px;
|
||||||
overflow: clip;
|
overflow: clip;
|
||||||
height: auto;
|
height: auto;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
height: auto;
|
height: auto;
|
||||||
overflow-y: hidden;
|
overflow-y: hidden;
|
||||||
padding-left: 4px;
|
padding-left: 4px;
|
||||||
padding-right: 4px;
|
padding-right: 4px;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* width */
|
/* width */
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
width: 6px;
|
width: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Track */
|
/* Track */
|
||||||
::-webkit-scrollbar-track {
|
::-webkit-scrollbar-track {
|
||||||
background: #272626;
|
background: #272626;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Handle */
|
/* Handle */
|
||||||
::-webkit-scrollbar-thumb {
|
::-webkit-scrollbar-thumb {
|
||||||
background: #949494;
|
background: #949494;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Handle on hover */
|
/* Handle on hover */
|
||||||
::-webkit-scrollbar-thumb:hover {
|
::-webkit-scrollbar-thumb:hover {
|
||||||
background: white;
|
background: white;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,83 +1,81 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import "$appcss";
|
import "$appcss";
|
||||||
|
|
||||||
import { page } from "$app/stores";
|
import { page } from "$app/stores";
|
||||||
import { t } from "$libs/translations";
|
import { t } from "$libs/translations";
|
||||||
import { afterNavigate } from "$app/navigation";
|
import { afterNavigate } from "$app/navigation";
|
||||||
import { packagesStore, authStore } from "$libs/stores";
|
import { packagesStore, authStore } from "$libs/stores";
|
||||||
import Packages from "$components/packages/packages.svelte";
|
import Packages from "$components/packages/packages.svelte";
|
||||||
import DiscoverPackages from "$components/discover-packages/discover-packages.svelte";
|
import DiscoverPackages from "$components/discover-packages/discover-packages.svelte";
|
||||||
import { PackageStates, SideMenuOptions, type GUIPackage } from "$libs/types";
|
import { PackageStates, SideMenuOptions, type GUIPackage } from "$libs/types";
|
||||||
// import SortingButtons from "$components/search-packages/sorting-buttons.svelte";
|
// import SortingButtons from "$components/search-packages/sorting-buttons.svelte";
|
||||||
import SideMenu from "$components/side-menu/side-menu.svelte";
|
import SideMenu from "$components/side-menu/side-menu.svelte";
|
||||||
import NotificationBar from "$components/notification-bar/notification-bar.svelte";
|
import NotificationBar from "$components/notification-bar/notification-bar.svelte";
|
||||||
import WelcomeModal from "$components/welcome-modal/welcome-modal.svelte";
|
import WelcomeModal from "$components/welcome-modal/welcome-modal.svelte";
|
||||||
import Button from "@tea/ui/button/button.svelte";
|
import Button from "@tea/ui/button/button.svelte";
|
||||||
import log from "$libs/logger";
|
import log from "$libs/logger";
|
||||||
|
|
||||||
const { packageList } = packagesStore;
|
const { packageList } = packagesStore;
|
||||||
const { session } = authStore;
|
const { session } = authStore;
|
||||||
|
|
||||||
const url = $page.url;
|
const url = $page.url;
|
||||||
|
|
||||||
let sideMenuOption = (url.searchParams.get("tab") as SideMenuOptions) || SideMenuOptions.discover;
|
let sideMenuOption = (url.searchParams.get("tab") as SideMenuOptions) || SideMenuOptions.discover;
|
||||||
|
|
||||||
let sortBy: "popularity" | "most recent" = "most recent";
|
let sortBy: "popularity" | "most recent" = "most recent";
|
||||||
let sortDirection: "asc" | "desc" = "desc";
|
let sortDirection: "asc" | "desc" = "desc";
|
||||||
|
|
||||||
let updating = false;
|
let updating = false;
|
||||||
|
|
||||||
let packagesScrollY = 0;
|
let packagesScrollY = 0;
|
||||||
$: currentUpdatingPkg = $packageList.find((p) => p.state === PackageStates.UPDATING);
|
$: currentUpdatingPkg = $packageList.find((p) => p.state === PackageStates.UPDATING);
|
||||||
$: updatingMessage = `updating ${currentUpdatingPkg?.full_name} (${currentUpdatingPkg?.install_progress_percentage}%)`;
|
$: updatingMessage = `updating ${currentUpdatingPkg?.full_name} (${currentUpdatingPkg?.install_progress_percentage}%)`;
|
||||||
|
|
||||||
$: pkgsToUpdate = $packageList.filter((p: GUIPackage) => p.state === PackageStates.NEEDS_UPDATE);
|
$: pkgsToUpdate = $packageList.filter((p: GUIPackage) => p.state === PackageStates.NEEDS_UPDATE);
|
||||||
async function updateAll() {
|
async function updateAll() {
|
||||||
updating = true;
|
updating = true;
|
||||||
log.info(`updating: ${pkgsToUpdate.length} packages`);
|
log.info(`updating: ${pkgsToUpdate.length} packages`);
|
||||||
for (const pkg of pkgsToUpdate) {
|
for (const pkg of pkgsToUpdate) {
|
||||||
try {
|
try {
|
||||||
await packagesStore.installPkg(pkg);
|
await packagesStore.installPkg(pkg);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error(error);
|
log.error(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
updating = false;
|
updating = false;
|
||||||
sideMenuOption = SideMenuOptions.all;
|
sideMenuOption = SideMenuOptions.all;
|
||||||
}
|
}
|
||||||
|
|
||||||
$: needsUpdateCount = pkgsToUpdate.length;
|
$: needsUpdateCount = pkgsToUpdate.length;
|
||||||
|
|
||||||
afterNavigate(({ to }) => {
|
afterNavigate(({ to }) => {
|
||||||
if (to?.url?.pathname === "/") {
|
if (to?.url?.pathname === "/") {
|
||||||
const tab = to.url.searchParams.get("tab");
|
const tab = to.url.searchParams.get("tab");
|
||||||
sideMenuOption = !tab ? SideMenuOptions.discover : (tab as SideMenuOptions);
|
sideMenuOption = !tab ? SideMenuOptions.discover : (tab as SideMenuOptions);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div id="content" class="flex flex-col">
|
<div id="content" class="flex flex-col">
|
||||||
<NotificationBar />
|
<NotificationBar />
|
||||||
<article class="relative h-auto w-full flex-grow overflow-hidden">
|
<article class="relative h-auto w-full flex-grow overflow-hidden">
|
||||||
<ul class="px-2">
|
<ul class="px-2">
|
||||||
{#if sideMenuOption == SideMenuOptions.discover}
|
{#if sideMenuOption == SideMenuOptions.discover}
|
||||||
<DiscoverPackages
|
<DiscoverPackages bind:scrollY={packagesScrollY} />
|
||||||
bind:scrollY={packagesScrollY}
|
{:else}
|
||||||
/>
|
<Packages
|
||||||
{:else}
|
packageFilter={sideMenuOption}
|
||||||
<Packages
|
{sortBy}
|
||||||
packageFilter={sideMenuOption}
|
{sortDirection}
|
||||||
{sortBy}
|
bind:scrollY={packagesScrollY}
|
||||||
{sortDirection}
|
/>
|
||||||
bind:scrollY={packagesScrollY}
|
{/if}
|
||||||
/>
|
</ul>
|
||||||
{/if}
|
<header class="z-30 flex items-center justify-between" class:scrolling={packagesScrollY > 150}>
|
||||||
</ul>
|
<h1 class="text-primary font-mona pl-3 text-2xl font-bold">
|
||||||
<header class="z-30 flex items-center justify-between" class:scrolling={packagesScrollY > 150}>
|
{$t(`side-menu-title.${sideMenuOption}`).toLowerCase()}
|
||||||
<h1 class="text-primary font-mona pl-3 text-2xl font-bold">
|
</h1>
|
||||||
{$t(`side-menu-title.${sideMenuOption}`).toLowerCase()}
|
<!--
|
||||||
</h1>
|
|
||||||
<!--
|
|
||||||
<section class="border-gray mt-4 mr-4 h-10 w-48 border rounded-sm">
|
<section class="border-gray mt-4 mr-4 h-10 w-48 border rounded-sm">
|
||||||
|
|
||||||
we might bring it back?
|
we might bring it back?
|
||||||
|
@ -87,63 +85,63 @@
|
||||||
}} />
|
}} />
|
||||||
</section>
|
</section>
|
||||||
-->
|
-->
|
||||||
{#if needsUpdateCount && sideMenuOption === SideMenuOptions.installed_updates_available}
|
{#if needsUpdateCount && sideMenuOption === SideMenuOptions.installed_updates_available}
|
||||||
<!-- 22px right margin to account for the scrollbar on the package cards -->
|
<!-- 22px right margin to account for the scrollbar on the package cards -->
|
||||||
<div class="mr-[22px] flex items-center justify-end text-sm">
|
<div class="mr-[22px] flex items-center justify-end text-sm">
|
||||||
{#if currentUpdatingPkg}
|
{#if currentUpdatingPkg}
|
||||||
<p class="text-gray px-2">{updatingMessage}</p>
|
<p class="text-gray px-2">{updatingMessage}</p>
|
||||||
{/if}
|
{/if}
|
||||||
<div>
|
<div>
|
||||||
<Button
|
<Button
|
||||||
class="h-8 w-48 text-xs"
|
class="h-8 w-48 text-xs"
|
||||||
loading={updating}
|
loading={updating}
|
||||||
type="plain"
|
type="plain"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
onClick={updateAll}
|
onClick={updateAll}
|
||||||
>
|
>
|
||||||
{$t(`package.update-all`)} [{needsUpdateCount}]
|
{$t(`package.update-all`)} [{needsUpdateCount}]
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</header>
|
</header>
|
||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<SideMenu bind:activeOption={sideMenuOption} />
|
<SideMenu bind:activeOption={sideMenuOption} />
|
||||||
{#if !$session.hide_welcome}
|
{#if !$session.hide_welcome}
|
||||||
<WelcomeModal />
|
<WelcomeModal />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
#content {
|
#content {
|
||||||
width: calc(100vw - 211px);
|
width: calc(100vw - 211px);
|
||||||
margin-left: 205px;
|
margin-left: 205px;
|
||||||
height: calc(100vh - 50px);
|
height: calc(100vh - 50px);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
header {
|
header {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0px;
|
top: 0px;
|
||||||
left: 1px;
|
left: 1px;
|
||||||
height: 72px;
|
height: 72px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background-image: linear-gradient(rgba(26, 26, 26, 1), rgba(26, 26, 26, 0));
|
background-image: linear-gradient(rgba(26, 26, 26, 1), rgba(26, 26, 26, 0));
|
||||||
padding-top: 15px;
|
padding-top: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
header h1 {
|
header h1 {
|
||||||
padding-top: 8px;
|
padding-top: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
header.scrolling {
|
header.scrolling {
|
||||||
height: 60px;
|
height: 60px;
|
||||||
background-color: #222222;
|
background-color: #222222;
|
||||||
padding-top: 5px;
|
padding-top: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
header.scrolling h1 {
|
header.scrolling h1 {
|
||||||
padding-top: 0px;
|
padding-top: 0px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,22 +1,22 @@
|
||||||
<script>
|
<script>
|
||||||
import '$appcss';
|
import "$appcss";
|
||||||
import { t } from '$libs/translations';
|
import { t } from "$libs/translations";
|
||||||
import PageHeader from '$components/page-header/page-header.svelte';
|
import PageHeader from "$components/page-header/page-header.svelte";
|
||||||
import FeaturedCourses from '$components/featured-courses/featured-courses.svelte';
|
import FeaturedCourses from "$components/featured-courses/featured-courses.svelte";
|
||||||
import EssentialWorkshops from '$components/essential-workshops/essential-workshops.svelte';
|
import EssentialWorkshops from "$components/essential-workshops/essential-workshops.svelte";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<PageHeader>{$t('documentation.title').toUpperCase()}</PageHeader>
|
<PageHeader>{$t("documentation.title").toUpperCase()}</PageHeader>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<FeaturedCourses />
|
<FeaturedCourses />
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="mt-8">
|
<section class="mt-8">
|
||||||
<EssentialWorkshops
|
<EssentialWorkshops
|
||||||
title={$t('documentation.workshops').toUpperCase()}
|
title={$t("documentation.workshops").toUpperCase()}
|
||||||
ctaLabel={$t("view-all")}
|
ctaLabel={$t("view-all")}
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
<script>
|
<script>
|
||||||
import '$appcss';
|
import "$appcss";
|
||||||
import PageHeader from '$components/page-header/page-header.svelte';
|
import PageHeader from "$components/page-header/page-header.svelte";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<PageHeader>Packages</PageHeader>
|
<PageHeader>Packages</PageHeader>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,95 +1,97 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import "$appcss";
|
import "$appcss";
|
||||||
import { t } from "$libs/translations";
|
import { t } from "$libs/translations";
|
||||||
|
|
||||||
import { page } from "$app/stores";
|
import { page } from "$app/stores";
|
||||||
// import PageHeader from '$components/page-header/page-header.svelte';
|
// import PageHeader from '$components/page-header/page-header.svelte';
|
||||||
import PackageBanner from "$components/package-banner/package-banner.svelte";
|
import PackageBanner from "$components/package-banner/package-banner.svelte";
|
||||||
// import SuggestedPackages from '$components/suggested-packages/suggested-packages.svelte';
|
// import SuggestedPackages from '$components/suggested-packages/suggested-packages.svelte';
|
||||||
import Tabs from "@tea/ui/tabs/tabs.svelte";
|
import Tabs from "@tea/ui/tabs/tabs.svelte";
|
||||||
import type { Tab } from "@tea/ui/types";
|
import type { Tab } from "@tea/ui/types";
|
||||||
import Bottles from "@tea/ui/bottles/bottles.svelte";
|
import Bottles from "@tea/ui/bottles/bottles.svelte";
|
||||||
import PackageMetas from "@tea/ui/package-metas/package-metas.svelte";
|
import PackageMetas from "@tea/ui/package-metas/package-metas.svelte";
|
||||||
import Markdown from "@tea/ui/markdown/markdown.svelte";
|
import Markdown from "@tea/ui/markdown/markdown.svelte";
|
||||||
// import PackageSnippets from '@tea/ui/package-snippets/package-snippets.svelte';
|
// import PackageSnippets from '@tea/ui/package-snippets/package-snippets.svelte';
|
||||||
import Preloader from "@tea/ui/Preloader/Preloader.svelte";
|
import Preloader from "@tea/ui/Preloader/Preloader.svelte";
|
||||||
|
|
||||||
/** @type {import('./$types').PageData} */
|
/** @type {import('./$types').PageData} */
|
||||||
export let data: { slug: string; content: string; title: string };
|
export let data: { slug: string; content: string; title: string };
|
||||||
|
|
||||||
import { packagesStore } from "$libs/stores";
|
import { packagesStore } from "$libs/stores";
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import NotificationBar from "$components/notification-bar/notification-bar.svelte";
|
import NotificationBar from "$components/notification-bar/notification-bar.svelte";
|
||||||
|
|
||||||
const { packageList } = packagesStore;
|
const { packageList } = packagesStore;
|
||||||
|
|
||||||
$: pkg = $packageList.find((p) => p.slug === data?.slug);
|
$: pkg = $packageList.find((p) => p.slug === data?.slug);
|
||||||
|
|
||||||
// let reviews: Review[];
|
// let reviews: Review[];
|
||||||
$: bottles = pkg?.bottles || [];
|
$: bottles = pkg?.bottles || [];
|
||||||
$: versions = [...new Set(bottles.map((b) => b.version))];
|
$: versions = [...new Set(bottles.map((b) => b.version))];
|
||||||
$: readme = pkg?.readme || { data: "", type: "md" };
|
$: readme = pkg?.readme || { data: "", type: "md" };
|
||||||
|
|
||||||
$: tabs = [
|
$: tabs = [
|
||||||
readme?.data !== "" && {
|
readme?.data !== "" && {
|
||||||
label: $t("common.details"),
|
label: $t("common.details"),
|
||||||
component: Markdown,
|
component: Markdown,
|
||||||
props: { pkg, source: readme }
|
props: { pkg, source: readme }
|
||||||
},
|
},
|
||||||
bottles?.length && {
|
bottles?.length && {
|
||||||
label: `${$t("common.versions")} (${versions.length || 0})`,
|
label: `${$t("common.versions")} (${versions.length || 0})`,
|
||||||
component: Bottles,
|
component: Bottles,
|
||||||
props: {
|
props: {
|
||||||
bottles
|
bottles
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
].filter((t) => t && t?.label) as unknown as Tab[];
|
].filter((t) => t && t?.label) as unknown as Tab[];
|
||||||
|
|
||||||
const url = $page.url;
|
const url = $page.url;
|
||||||
|
|
||||||
const tab = url.searchParams.get("tab");
|
const tab = url.searchParams.get("tab");
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
packagesStore.syncPackageData(pkg);
|
packagesStore.syncPackageData(pkg);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<header class="text-gray mx-16 mb-4 border border-x-0 border-t-0 py-5">
|
<header class="text-gray mx-16 mb-4 border border-x-0 border-t-0 py-5">
|
||||||
<a class="hover:text-white hover:opacity-80" href="/">{$t("common.home")}</a>
|
<a class="hover:text-white hover:opacity-80" href="/">{$t("common.home")}</a>
|
||||||
›
|
›
|
||||||
{#if tab && tab !== "all"}
|
{#if tab && tab !== "all"}
|
||||||
<a class="hover:text-white hover:opacity-80" href="/?tab={tab || "all"}">{$t(`tags.${tab}`).toLowerCase() || "all"}</a>
|
<a class="hover:text-white hover:opacity-80" href="/?tab={tab || 'all'}"
|
||||||
›
|
>{$t(`tags.${tab}`).toLowerCase() || "all"}</a
|
||||||
{/if}
|
>
|
||||||
<span class="text-white">{pkg?.full_name}</span>
|
›
|
||||||
|
{/if}
|
||||||
|
<span class="text-white">{pkg?.full_name}</span>
|
||||||
</header>
|
</header>
|
||||||
<div class="mx-16 mb-4">
|
<div class="mx-16 mb-4">
|
||||||
<NotificationBar />
|
<NotificationBar />
|
||||||
</div>
|
</div>
|
||||||
{#if pkg}
|
{#if pkg}
|
||||||
<div class="px-16">
|
<div class="px-16">
|
||||||
<section>
|
<section>
|
||||||
<PackageBanner {pkg} />
|
<PackageBanner {pkg} />
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="mt-8 flex gap-8">
|
<section class="mt-8 flex gap-8">
|
||||||
<div class="w-2/3">
|
<div class="w-2/3">
|
||||||
<Tabs {tabs} defaultTab={$t("common.details")} />
|
<Tabs {tabs} defaultTab={$t("common.details")} />
|
||||||
</div>
|
</div>
|
||||||
<div class="w-1/3">
|
<div class="w-1/3">
|
||||||
{#if pkg}
|
{#if pkg}
|
||||||
<PackageMetas {pkg} />
|
<PackageMetas {pkg} />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<!-- <PageHeader class="mt-8" coverUrl="/images/headers/header_bg_1.png">SNIPPETS</PageHeader> -->
|
<!-- <PageHeader class="mt-8" coverUrl="/images/headers/header_bg_1.png">SNIPPETS</PageHeader> -->
|
||||||
<!-- <section class="mt-8">
|
<!-- <section class="mt-8">
|
||||||
<PackageSnippets />
|
<PackageSnippets />
|
||||||
</section> -->
|
</section> -->
|
||||||
<!-- <section class="mt-8">
|
<!-- <section class="mt-8">
|
||||||
<PackageReviews reviews={reviews || []} />
|
<PackageReviews reviews={reviews || []} />
|
||||||
</section> -->
|
</section> -->
|
||||||
<!-- {#if pkg}
|
<!-- {#if pkg}
|
||||||
<PageHeader class="mt-8" coverUrl="/images/headers/header_bg_1.png"
|
<PageHeader class="mt-8" coverUrl="/images/headers/header_bg_1.png"
|
||||||
>YOU MAY ALSO LIKE...</PageHeader
|
>YOU MAY ALSO LIKE...</PageHeader
|
||||||
>
|
>
|
||||||
|
@ -97,7 +99,7 @@
|
||||||
<SuggestedPackages {pkg} />
|
<SuggestedPackages {pkg} />
|
||||||
</section>
|
</section>
|
||||||
{/if} -->
|
{/if} -->
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<Preloader />
|
<Preloader />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -2,10 +2,10 @@ import type { LoadEvent } from "@sveltejs/kit";
|
||||||
|
|
||||||
/** @type {import('./$types').PageLoad} */
|
/** @type {import('./$types').PageLoad} */
|
||||||
export function load({ params }: LoadEvent) {
|
export function load({ params }: LoadEvent) {
|
||||||
// TODO: search package details here
|
// TODO: search package details here
|
||||||
return {
|
return {
|
||||||
title: `${params.slug}`,
|
title: `${params.slug}`,
|
||||||
content: "",
|
content: "",
|
||||||
slug: params.slug
|
slug: params.slug
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,26 +3,26 @@ import preprocess from "svelte-preprocess";
|
||||||
|
|
||||||
/** @type {import('@sveltejs/kit').Config} */
|
/** @type {import('@sveltejs/kit').Config} */
|
||||||
const config = {
|
const config = {
|
||||||
// Consult https://github.com/sveltejs/svelte-preprocess
|
// Consult https://github.com/sveltejs/svelte-preprocess
|
||||||
// for more information about preprocessors
|
// for more information about preprocessors
|
||||||
preprocess: [
|
preprocess: [
|
||||||
preprocess({
|
preprocess({
|
||||||
postcss: true
|
postcss: true
|
||||||
})
|
})
|
||||||
],
|
],
|
||||||
kit: {
|
kit: {
|
||||||
adapter: adapter({
|
adapter: adapter({
|
||||||
pages: "build",
|
pages: "build",
|
||||||
assets: "build",
|
assets: "build",
|
||||||
fallback: "app.html"
|
fallback: "app.html"
|
||||||
}),
|
}),
|
||||||
alias: {
|
alias: {
|
||||||
"@tea/ui/*": "../ui/src/*"
|
"@tea/ui/*": "../ui/src/*"
|
||||||
}
|
}
|
||||||
// ssr: false,
|
// ssr: false,
|
||||||
// hydrate the <div id="svelte"> element in src/app.html
|
// hydrate the <div id="svelte"> element in src/app.html
|
||||||
// target: '#svelte'
|
// target: '#svelte'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export default config;
|
export default config;
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { theme, plugins } from "@tea/ui/tailwind.config.cjs";
|
import { theme, plugins } from "@tea/ui/tailwind.config.cjs";
|
||||||
module.exports = {
|
module.exports = {
|
||||||
content: ["./src/**/*.{html,svelte,ts,js}", "../ui/src/**/*.{html,svelte,ts,js}"],
|
content: ["./src/**/*.{html,svelte,ts,js}", "../ui/src/**/*.{html,svelte,ts,js}"],
|
||||||
theme,
|
theme,
|
||||||
plugins: [...plugins]
|
plugins: [...plugins]
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,28 +1,28 @@
|
||||||
{
|
{
|
||||||
"extends": "./.svelte-kit/tsconfig.json",
|
"extends": "./.svelte-kit/tsconfig.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"allowSyntheticDefaultImports": true,
|
"allowSyntheticDefaultImports": true,
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"checkJs": true,
|
"checkJs": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"types": ["vitest/globals", "@testing-library/jest-dom"],
|
"types": ["vitest/globals", "@testing-library/jest-dom"],
|
||||||
"paths": {
|
"paths": {
|
||||||
"$appcss": ["src/app.css"],
|
"$appcss": ["src/app.css"],
|
||||||
"$libs/*": ["src/libs/*"],
|
"$libs/*": ["src/libs/*"],
|
||||||
"@native": ["src/libs/native-electron.ts"],
|
"@native": ["src/libs/native-electron.ts"],
|
||||||
"$components/*": ["src/components/*"],
|
"$components/*": ["src/components/*"],
|
||||||
"@tea/ui/*": ["../ui/src/*"]
|
"@tea/ui/*": ["../ui/src/*"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": ["../ui/types/*.d.ts"]
|
"include": ["../ui/types/*.d.ts"]
|
||||||
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
|
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
|
||||||
//
|
//
|
||||||
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
|
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
|
||||||
// from the referenced tsconfig.json - TypeScript does not merge them in
|
// from the referenced tsconfig.json - TypeScript does not merge them in
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,37 +5,37 @@ import path from "path";
|
||||||
const isMock = process.env.BUILD_FOR === "preview";
|
const isMock = process.env.BUILD_FOR === "preview";
|
||||||
|
|
||||||
const config: UserConfig = {
|
const config: UserConfig = {
|
||||||
plugins: [sveltekit()],
|
plugins: [sveltekit()],
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
"@tea/ui/*": path.resolve("../ui/src/*"),
|
"@tea/ui/*": path.resolve("../ui/src/*"),
|
||||||
// this dynamic-ish static importing is followed by the svelte build
|
// this dynamic-ish static importing is followed by the svelte build
|
||||||
// but for vscode editing intellisense tsconfig.json is being used
|
// but for vscode editing intellisense tsconfig.json is being used
|
||||||
"@native": isMock
|
"@native": isMock
|
||||||
? path.resolve("src/libs/native-mock.ts")
|
? path.resolve("src/libs/native-mock.ts")
|
||||||
: path.resolve("src/libs/native-electron.ts"),
|
: path.resolve("src/libs/native-electron.ts"),
|
||||||
$components: path.resolve("./src/components"),
|
$components: path.resolve("./src/components"),
|
||||||
$libs: path.resolve("./src/libs"),
|
$libs: path.resolve("./src/libs"),
|
||||||
$appcss: path.resolve("./src/app.css")
|
$appcss: path.resolve("./src/app.css")
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
server: {
|
server: {
|
||||||
port: 3000,
|
port: 3000,
|
||||||
fs: {
|
fs: {
|
||||||
allow: [".."]
|
allow: [".."]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
test: {
|
test: {
|
||||||
// Jest like globals
|
// Jest like globals
|
||||||
globals: true,
|
globals: true,
|
||||||
environment: "jsdom",
|
environment: "jsdom",
|
||||||
include: ["src/**/*.{test,spec}.ts"],
|
include: ["src/**/*.{test,spec}.ts"],
|
||||||
// Extend jest-dom matchers
|
// Extend jest-dom matchers
|
||||||
setupFiles: ["./setupTest.js"],
|
setupFiles: ["./setupTest.js"],
|
||||||
coverage: {
|
coverage: {
|
||||||
provider: "c8"
|
provider: "c8"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export default config;
|
export default config;
|
||||||
|
|
|
@ -1,41 +1,41 @@
|
||||||
module.exports = {
|
module.exports = {
|
||||||
root: true,
|
root: true,
|
||||||
globals: {
|
globals: {
|
||||||
NodeJS: true
|
NodeJS: true
|
||||||
},
|
},
|
||||||
parser: "@typescript-eslint/parser",
|
parser: "@typescript-eslint/parser",
|
||||||
extends: [
|
extends: [
|
||||||
"eslint:recommended",
|
"eslint:recommended",
|
||||||
"plugin:@typescript-eslint/recommended",
|
"plugin:@typescript-eslint/recommended",
|
||||||
"prettier",
|
"prettier",
|
||||||
"plugin:storybook/recommended"
|
"plugin:storybook/recommended"
|
||||||
],
|
],
|
||||||
plugins: ["svelte3", "@typescript-eslint"],
|
plugins: ["svelte3", "@typescript-eslint"],
|
||||||
ignorePatterns: ["*.cjs"],
|
ignorePatterns: ["*.cjs"],
|
||||||
overrides: [
|
overrides: [
|
||||||
{
|
{
|
||||||
files: ["*.svelte"],
|
files: ["*.svelte"],
|
||||||
processor: "svelte3/svelte3"
|
processor: "svelte3/svelte3"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
settings: {
|
settings: {
|
||||||
"svelte3/typescript": () => require("typescript")
|
"svelte3/typescript": () => require("typescript")
|
||||||
},
|
},
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
sourceType: "module",
|
sourceType: "module",
|
||||||
ecmaVersion: 2020
|
ecmaVersion: 2020
|
||||||
},
|
},
|
||||||
env: {
|
env: {
|
||||||
browser: true,
|
browser: true,
|
||||||
es2017: true,
|
es2017: true,
|
||||||
node: true
|
node: true
|
||||||
},
|
},
|
||||||
rules: {
|
rules: {
|
||||||
"@typescript-eslint/no-unused-vars": [
|
"@typescript-eslint/no-unused-vars": [
|
||||||
"warn",
|
"warn",
|
||||||
{
|
{
|
||||||
argsIgnorePattern: "^_"
|
argsIgnorePattern: "^_"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
{
|
{
|
||||||
"tabWidth": 2,
|
"tabWidth": 2,
|
||||||
"useTabs": true,
|
"useTabs": false,
|
||||||
"singleQuote": false,
|
"singleQuote": false,
|
||||||
"trailingComma": "none",
|
"trailingComma": "none",
|
||||||
"printWidth": 100,
|
"printWidth": 100,
|
||||||
"plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"],
|
"plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"],
|
||||||
"pluginSearchDirs": ["."],
|
"pluginSearchDirs": ["../../node_modules"],
|
||||||
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
|
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,16 +1,16 @@
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
module.exports = {
|
module.exports = {
|
||||||
stories: ["../src/**/*.stories.mdx", "../src/**/*.stories.@(js|jsx|ts|tsx|svelte)"],
|
stories: ["../src/**/*.stories.mdx", "../src/**/*.stories.@(js|jsx|ts|tsx|svelte)"],
|
||||||
addons: [
|
addons: [
|
||||||
"@storybook/addon-links",
|
"@storybook/addon-links",
|
||||||
"@storybook/addon-essentials",
|
"@storybook/addon-essentials",
|
||||||
"@storybook/addon-interactions"
|
"@storybook/addon-interactions"
|
||||||
],
|
],
|
||||||
framework: {
|
framework: {
|
||||||
name: "@storybook/svelte-vite",
|
name: "@storybook/svelte-vite",
|
||||||
options: {}
|
options: {}
|
||||||
},
|
},
|
||||||
docs: {
|
docs: {
|
||||||
docsPage: true
|
docsPage: true
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue