* Fix Prettier config
This commit is contained in:
ABevier 2023-04-28 00:14:44 -04:00 committed by GitHub
parent d300efd805
commit 6c3be19da2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
166 changed files with 7717 additions and 7703 deletions

View file

@ -91,7 +91,7 @@ pnpm dev
## Prettier ## Prettier
```sh ```sh
pnpm run -r format pnpm run --reporter append-only -r format
``` ```
## Dist ## Dist

View file

@ -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" }]
} }
}; };

View file

@ -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" } }]
} }

View file

@ -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"
} }
}; };

View file

@ -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);
} }
} }

View file

@ -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);
} }
} }

View file

@ -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 });
}); });

View file

@ -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`);
} }

View file

@ -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;
} }

View file

@ -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);
} }
}); });
} }

View file

@ -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

View file

@ -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;
}; };

View file

@ -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;
} }

View file

@ -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}`;
} }

View file

@ -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;

View file

@ -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
); );
} }

View file

@ -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;

View file

@ -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"
} }

View file

@ -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;

View file

@ -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: {}
} }
}; };

View file

@ -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();

View file

@ -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();

View file

@ -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";
} }

View file

@ -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;
} }
} }

View file

@ -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>

View file

@ -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" />

View file

@ -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" />

View file

@ -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>

View file

@ -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}

View file

@ -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"
/> />

View file

@ -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}`
}))} }))}
/> />

View file

@ -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>&#8594</div> <div>&#8594</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>&#8594</div> <div>&#8594</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>&#8594</div> <div>&#8594</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>

View file

@ -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>

View file

@ -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}

View file

@ -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);
}} }}
/> />

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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}

View file

@ -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 dont have anything installed You dont 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>

View file

@ -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]">Youre all up to date 👍</div>
Youre all up to date 👍
</div>
</div> </div>

View file

@ -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);
}} }}
/> />

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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}

View file

@ -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>

View file

@ -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' : ''}>&uarr;</button class={sortBy === option && sortDirection === "asc" ? "active" : ""}>&uarr;</button
> >
<button <button
on:click={() => setSortDir(option, 'desc')} on:click={() => setSortDir(option, "desc")}
class={sortBy === option && sortDirection === 'desc' ? 'active' : ''}>&darr;</button class={sortBy === option && sortDirection === "desc" ? "active" : ""}>&darr;</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>

View file

@ -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>

View file

@ -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>

View file

@ -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}>&#x2715</button> <button class="mr-2" on:click={onClose}>&#x2715</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>

View file

@ -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
}))} }))}
/> />

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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 youre 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 youre 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 {

View file

@ -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
}); });
} }
}; };

View file

@ -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("/");
}; };

View file

@ -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;

View file

@ -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" };
} }
}; };

View file

@ -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
} }

View file

@ -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"
]); ]);
}); });
}); });

View file

@ -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"]
}; };

View file

@ -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
); );
}; };

View file

@ -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);
} }

View file

@ -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();

View file

@ -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
}; };
} }

View file

@ -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;
} }
}; };
} }

View file

@ -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]);
} }
}; };
} }

View file

@ -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);
} }
}; };

View file

@ -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
}; };
} }

View file

@ -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);

View file

@ -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"
} }
} }
} }

View file

@ -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;
}; };

View file

@ -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);
}; };
} }

View file

@ -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));

View file

@ -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
}; };
} }

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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}

View file

@ -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
}; };
} }

View file

@ -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;

View file

@ -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]
}; };

View file

@ -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
} }

View file

@ -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;

View file

@ -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: "^_"
} }
] ]
} }
}; };

View file

@ -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" } }]
} }

View file

@ -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