* 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
```sh
pnpm run -r format
pnpm run --reporter append-only -r format
```
## Dist

View file

@ -1,31 +1,31 @@
module.exports = {
root: true,
globals: {
NodeJS: true
},
parser: "@typescript-eslint/parser",
extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended", "prettier"],
plugins: ["svelte3", "@typescript-eslint"],
ignorePatterns: ["*.cjs"],
overrides: [
{
files: ["*.svelte"],
processor: "svelte3/svelte3"
}
],
settings: {
"svelte3/typescript": () => require("typescript")
},
parserOptions: {
sourceType: "module",
ecmaVersion: 2020
},
env: {
browser: true,
es2017: true,
node: true
},
rules: {
"@typescript-eslint/ban-ts-comment": ["error", { "ts-ignore": "allow-with-description" }]
}
root: true,
globals: {
NodeJS: true
},
parser: "@typescript-eslint/parser",
extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended", "prettier"],
plugins: ["svelte3", "@typescript-eslint"],
ignorePatterns: ["*.cjs"],
overrides: [
{
files: ["*.svelte"],
processor: "svelte3/svelte3"
}
],
settings: {
"svelte3/typescript": () => require("typescript")
},
parserOptions: {
sourceType: "module",
ecmaVersion: 2020
},
env: {
browser: true,
es2017: true,
node: true
},
rules: {
"@typescript-eslint/ban-ts-comment": ["error", { "ts-ignore": "allow-with-description" }]
}
};

View file

@ -1,10 +1,10 @@
{
"tabWidth": 2,
"useTabs": true,
"singleQuote": false,
"trailingComma": "none",
"printWidth": 100,
"plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"],
"pluginSearchDirs": ["."],
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
"tabWidth": 2,
"useTabs": false,
"singleQuote": false,
"trailingComma": "none",
"printWidth": 100,
"plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"],
"pluginSearchDirs": ["../../node_modules"],
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
}

View file

@ -5,59 +5,59 @@ const otaClient = require("@crowdin/ota-client");
const _ = require("lodash");
module.exports = {
appId: "xyz.tea.gui",
productName: "tea",
asar: false,
directories: { output: "dist" },
files: ["electron/dist/electron.cjs", { from: "build", to: "" }],
linux: {
icon: "./icon.png"
},
mac: {
icon: "./electron/icon.icns",
target: {
target: "default",
arch: ["x64", "arm64"]
}
},
afterSign: async (params) => {
if (process.platform !== "darwin" || process.env.CSC_IDENTITY_AUTO_DISCOVERY === "false") {
console.log("not notarizing app");
return;
}
appId: "xyz.tea.gui",
productName: "tea",
asar: false,
directories: { output: "dist" },
files: ["electron/dist/electron.cjs", { from: "build", to: "" }],
linux: {
icon: "./icon.png"
},
mac: {
icon: "./electron/icon.icns",
target: {
target: "default",
arch: ["x64", "arm64"]
}
},
afterSign: async (params) => {
if (process.platform !== "darwin" || process.env.CSC_IDENTITY_AUTO_DISCOVERY === "false") {
console.log("not notarizing app");
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`);
if (!fs.existsSync(appPath)) {
console.log("skip");
return;
}
let appPath = path.join(params.appOutDir, `${params.packager.appInfo.productFilename}.app`);
if (!fs.existsSync(appPath)) {
console.log("skip");
return;
}
console.log(
`Notarizing ${appBundleId} found at ${appPath} with Apple ID ${process.env.APPLE_ID}`
);
console.log(
`Notarizing ${appBundleId} found at ${appPath} with Apple ID ${process.env.APPLE_ID}`
);
try {
await notarize({
appBundleId,
appPath,
appleId: process.env.APPLE_ID,
appleIdPassword: process.env.APPLE_APP_SPECIFIC_PASSWORD
});
} catch (error) {
console.error(error);
}
try {
await notarize({
appBundleId,
appPath,
appleId: process.env.APPLE_ID,
appleIdPassword: process.env.APPLE_APP_SPECIFIC_PASSWORD
});
} catch (error) {
console.error(error);
}
console.log(`Done notarizing`);
},
// this determines the configuration of the auto-update feature
publish: {
provider: "generic",
// TODO: replace this with tea branded domain: gui-dist.tea.xyz
// url: "https://d2ovumu63qzbn6.cloudfront.net/"
url: "https://s3.amazonaws.com/preview.gui.tea.xyz/release"
}
console.log(`Done notarizing`);
},
// this determines the configuration of the auto-update feature
publish: {
provider: "generic",
// TODO: replace this with tea branded domain: gui-dist.tea.xyz
// url: "https://d2ovumu63qzbn6.cloudfront.net/"
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 initializePushNotification, {
syncPackageTopicSubscriptions
syncPackageTopicSubscriptions
} from "./libs/push-notification";
import init from "./libs/initialize";
@ -19,26 +19,26 @@ import { readSessionData } from "./libs/auth";
log.info("App starting...");
if (app.isPackaged) {
Sentry.init({
dsn: "https://5ff29bb5b3b64cd4bd4f4960ef1db2e3@o4504750197899264.ingest.sentry.io/4504750206746624",
debug: true,
transportOptions: {
maxQueueAgeDays: 30,
maxQueueCount: 30,
beforeSend: async () => {
const ol = await net.isOnline();
return ol ? "send" : "queue";
}
}
});
Sentry.configureScope(async (scope) => {
const session = await readSessionData();
scope.setUser({
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
});
});
setSentryLogging(Sentry);
Sentry.init({
dsn: "https://5ff29bb5b3b64cd4bd4f4960ef1db2e3@o4504750197899264.ingest.sentry.io/4504750206746624",
debug: true,
transportOptions: {
maxQueueAgeDays: 30,
maxQueueCount: 30,
beforeSend: async () => {
const ol = await net.isOnline();
return ol ? "send" : "queue";
}
}
});
Sentry.configureScope(async (scope) => {
const session = await readSessionData();
scope.setUser({
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
});
});
setSentryLogging(Sentry);
}
init();
@ -56,148 +56,148 @@ setupTitlebar();
initializeHandlers({ notifyMainWindow });
function createWindow() {
const windowState = windowStateManager({
defaultWidth: 1000,
defaultHeight: 600
});
const windowState = windowStateManager({
defaultWidth: 1000,
defaultHeight: 600
});
const mainWindow = new BrowserWindow({
titleBarStyle: "hidden",
backgroundColor: "black",
autoHideMenuBar: true,
trafficLightPosition: {
x: 14,
y: 15
},
minHeight: 600,
minWidth: 1000,
webPreferences: {
contextIsolation: false,
nodeIntegration: true,
spellcheck: false,
webSecurity: false,
devTools: allowDebug,
preload: path.join(app.getAppPath(), "preload.cjs")
},
x: windowState.x,
y: windowState.y,
width: windowState.width,
height: windowState.height
});
const mainWindow = new BrowserWindow({
titleBarStyle: "hidden",
backgroundColor: "black",
autoHideMenuBar: true,
trafficLightPosition: {
x: 14,
y: 15
},
minHeight: 600,
minWidth: 1000,
webPreferences: {
contextIsolation: false,
nodeIntegration: true,
spellcheck: false,
webSecurity: false,
devTools: allowDebug,
preload: path.join(app.getAppPath(), "preload.cjs")
},
x: windowState.x,
y: windowState.y,
width: windowState.width,
height: windowState.height
});
windowState.manage(mainWindow);
mainWindow.webContents.openDevTools();
mainWindow.once("ready-to-show", () => {
mainWindow.show();
mainWindow.focus();
});
windowState.manage(mainWindow);
mainWindow.webContents.openDevTools();
mainWindow.once("ready-to-show", () => {
mainWindow.show();
mainWindow.focus();
});
mainWindow.on("close", () => {
windowState.saveState(mainWindow);
});
mainWindow.on("close", () => {
windowState.saveState(mainWindow);
});
mainWindow.webContents.on("did-finish-load", () => {
initializePushNotification(mainWindow);
});
mainWindow.webContents.on("did-finish-load", () => {
initializePushNotification(mainWindow);
});
attachTitlebarToWindow(mainWindow);
return mainWindow;
attachTitlebarToWindow(mainWindow);
return mainWindow;
}
contextMenu({
showLookUpSelection: false,
showSearchWithGoogle: false,
showCopyImage: false
showLookUpSelection: false,
showSearchWithGoogle: false,
showCopyImage: false
});
function loadVite(port) {
mainWindow?.loadURL(`http://localhost:${port}?is-vite=true`).catch((e) => {
console.log("Error loading URL, retrying", e);
setTimeout(() => {
loadVite(port);
}, 200);
});
mainWindow?.loadURL(`http://localhost:${port}?is-vite=true`).catch((e) => {
console.log("Error loading URL, retrying", e);
setTimeout(() => {
loadVite(port);
}, 200);
});
}
function createMainWindow() {
if (mainWindow) {
if (mainWindow.isMinimized()) mainWindow.restore();
mainWindow.focus();
} else {
mainWindow = createWindow();
}
if (mainWindow) {
if (mainWindow.isMinimized()) mainWindow.restore();
mainWindow.focus();
} else {
mainWindow = createWindow();
}
checkUpdater(mainWindow);
checkUpdater(mainWindow);
mainWindow.once("close", () => {
mainWindow = null;
});
mainWindow.once("close", () => {
mainWindow = null;
});
if (!app.isPackaged) {
// dev
loadVite(port);
} else {
serveURL(mainWindow);
}
if (!app.isPackaged) {
// dev
loadVite(port);
} else {
serveURL(mainWindow);
}
syncPackageTopicSubscriptions();
syncPackageTopicSubscriptions();
}
if (process.defaultApp) {
app.setAsDefaultProtocolClient("tea", process.execPath, [path.resolve(process.argv[1])]);
app.setAsDefaultProtocolClient("tea", process.execPath, [path.resolve(process.argv[1])]);
} else {
app.setAsDefaultProtocolClient("tea");
app.setAsDefaultProtocolClient("tea");
}
app.once("ready", createMainWindow);
app.on("activate", () => {
if (!mainWindow) {
createMainWindow();
}
if (!mainWindow) {
createMainWindow();
}
});
app.on("window-all-closed", () => {
// mac ux is just minimize them when closed unless forced quite CMD+Q
macWindowClosed = true;
if (process.platform !== "darwin") {
app.quit();
}
// mac ux is just minimize them when closed unless forced quite CMD+Q
macWindowClosed = true;
if (process.platform !== "darwin") {
app.quit();
}
});
// NOTE: this doesnt work in linux
// you have to loop through process.argv to figure out which url launched the app
app.on("open-url", (event, url) => {
// ie url: tea://packages/slug
event.preventDefault();
// ie url: tea://packages/slug
event.preventDefault();
const packagesPrefix = "/packages/";
const packagesPrefix = "/packages/";
let rawPath = url.replace("tea:/", "");
let rawPath = url.replace("tea:/", "");
const isPackage = url.includes(packagesPrefix);
if (isPackage) {
// /packages/github.com/pypa/twine -> /packages/github_com_pypa_twine
const packageSlug = nameToSlug(rawPath.replace(packagesPrefix, ""));
rawPath = [packagesPrefix, packageSlug].join("");
}
const isPackage = url.includes(packagesPrefix);
if (isPackage) {
// /packages/github.com/pypa/twine -> /packages/github_com_pypa_twine
const packageSlug = nameToSlug(rawPath.replace(packagesPrefix, ""));
rawPath = [packagesPrefix, packageSlug].join("");
}
setProtocolPath(rawPath);
setProtocolPath(rawPath);
if (mainWindow) {
if (mainWindow.isMinimized()) {
mainWindow.restore();
log.info("restored");
}
mainWindow.webContents.send("sync-path");
log.info("synced path", rawPath);
} else if (macWindowClosed) {
log.info("open new window");
createMainWindow();
}
if (mainWindow) {
if (mainWindow.isMinimized()) {
mainWindow.restore();
log.info("restored");
}
mainWindow.webContents.send("sync-path");
log.info("synced path", rawPath);
} else if (macWindowClosed) {
log.info("open new window");
createMainWindow();
}
});
function notifyMainWindow(channel: string, data: unknown) {
if (mainWindow) {
mainWindow.webContents.send(channel, data);
}
if (mainWindow) {
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");
export interface Session {
device_id?: string;
key?: string;
user?: any;
locale?: string;
device_id?: string;
key?: string;
user?: any;
locale?: string;
}
let sessionMemory: Session = { device_id: "", locale: "en" };
const initialized: Promise<Session> = new Promise((resolve, reject) => {
try {
log.info("initializing GUI session folder");
createInitialSessionFile().then((newSession) => {
resolve(newSession);
});
} catch (error) {
log.error(error);
reject(error);
}
try {
log.info("initializing GUI session folder");
createInitialSessionFile().then((newSession) => {
resolve(newSession);
});
} catch (error) {
log.error(error);
reject(error);
}
});
async function addEmptySessionFile(): Promise<Session> {
const locale = app.getLocale();
await mkdirp(sessionFolder);
const data = {
device_id: await getDeviceId(),
locale
};
await writeSessionData(data, true);
log.info("new session file created");
return data;
const locale = app.getLocale();
await mkdirp(sessionFolder);
const data = {
device_id: await getDeviceId(),
locale
};
await writeSessionData(data, true);
log.info("new session file created");
return data;
}
export async function createInitialSessionFile(): Promise<Session> {
// TODO: this looks nasty, refactor this
// the app is too dependent that this function succeeds
let session = {
...sessionMemory
};
const locale = app.getLocale();
// TODO: this looks nasty, refactor this
// the app is too dependent that this function succeeds
let session = {
...sessionMemory
};
const locale = app.getLocale();
try {
if (fs.existsSync(sessionFilePath)) {
log.info("session file exists!");
const sessionBuffer = await fs.readFileSync(sessionFilePath);
const sessionData = JSON.parse(sessionBuffer.toString()) as Session;
try {
if (fs.existsSync(sessionFilePath)) {
log.info("session file exists!");
const sessionBuffer = await fs.readFileSync(sessionFilePath);
const sessionData = JSON.parse(sessionBuffer.toString()) as Session;
if (!sessionData?.device_id) {
throw new Error("device_id is empty!");
} else {
session = sessionData;
session.locale = locale;
}
}
} catch (error) {
log.error(error);
}
if (!sessionData?.device_id) {
throw new Error("device_id is empty!");
} else {
session = sessionData;
session.locale = locale;
}
}
} catch (error) {
log.error(error);
}
if (!session?.device_id) {
try {
const newSession = await addEmptySessionFile();
if (newSession) {
session = newSession;
session.locale = locale;
}
} catch (error) {
log.error(error);
}
}
if (!session?.device_id) {
try {
const newSession = await addEmptySessionFile();
if (newSession) {
session = newSession;
session.locale = locale;
}
} catch (error) {
log.error(error);
}
}
sessionMemory = session;
sessionMemory = session;
return session;
return session;
}
let deviceIdRetryCount = 0;
async function getDeviceId() {
let deviceId = "";
try {
const req = await axios.get<{ deviceId: string }>("https://api.tea.xyz/v1/auth/registerDevice");
deviceId = req.data.deviceId;
} catch (error) {
log.error(error);
}
let deviceId = "";
try {
const req = await axios.get<{ deviceId: string }>("https://api.tea.xyz/v1/auth/registerDevice");
deviceId = req.data.deviceId;
} catch (error) {
log.error(error);
}
if (deviceIdRetryCount < 3 && !deviceId) {
deviceIdRetryCount++;
deviceId = await getDeviceId();
}
if (deviceIdRetryCount < 3 && !deviceId) {
deviceIdRetryCount++;
deviceId = await getDeviceId();
}
return deviceId;
return deviceId;
}
export async function readSessionData(): Promise<Session> {
log.info("read session data.");
const data = await initialized;
log.info(
"initialized session device_id:",
data?.device_id,
"developer_id:",
data?.user?.developer_id
);
if (sessionMemory?.device_id) {
log.debug("use session cache");
return sessionMemory;
}
log.info("read session data.");
const data = await initialized;
log.info(
"initialized session device_id:",
data?.device_id,
"developer_id:",
data?.user?.developer_id
);
if (sessionMemory?.device_id) {
log.debug("use session cache");
return sessionMemory;
}
try {
log.info("re-reading session data");
const locale = app.getLocale();
const sessionBuffer = await fs.readFileSync(sessionFilePath);
const session = JSON.parse(sessionBuffer.toString()) as Session;
if (!session?.device_id) throw new Error("device_id is empty!");
try {
log.info("re-reading session data");
const locale = app.getLocale();
const sessionBuffer = await fs.readFileSync(sessionFilePath);
const session = JSON.parse(sessionBuffer.toString()) as Session;
if (!session?.device_id) throw new Error("device_id is empty!");
session.locale = locale;
sessionMemory = session;
log.info("re-read session data done");
} catch (error) {
sessionMemory = await createInitialSessionFile();
log.error(error);
}
return sessionMemory;
session.locale = locale;
sessionMemory = session;
log.info("re-read session data done");
} catch (error) {
sessionMemory = await createInitialSessionFile();
log.error(error);
}
return sessionMemory;
}
export async function writeSessionData(data: Session, force?: boolean) {
try {
const existingData = force ? sessionMemory : await readSessionData();
sessionMemory = {
...existingData,
...data
};
try {
const existingData = force ? sessionMemory : await readSessionData();
sessionMemory = {
...existingData,
...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);
await mkdirp(sessionFolder);
log.info("writing session data:", sessionMemory); // rm this
await fs.writeFileSync(sessionFilePath, JSON.stringify(sessionMemory), {
encoding: "utf-8"
});
} catch (error) {
log.error(error);
}
log.info("creating:", sessionFolder);
await mkdirp(sessionFolder);
log.info("writing session data:", sessionMemory); // rm this
await fs.writeFileSync(sessionFilePath, JSON.stringify(sessionMemory), {
encoding: "utf-8"
});
} catch (error) {
log.error(error);
}
}

View file

@ -3,8 +3,8 @@ import log from "./logger";
import { BrowserWindow } from "electron";
type AutoUpdateStatus = {
status: "up-to-date" | "available" | "ready";
version?: string;
status: "up-to-date" | "available" | "ready";
version?: string;
};
autoUpdater.logger = log;
@ -18,59 +18,59 @@ let lastStatus: AutoUpdateStatus = { status: "up-to-date" };
export const getUpdater = () => autoUpdater;
export function checkUpdater(mainWindow: BrowserWindow): AppUpdater {
try {
window = mainWindow;
autoUpdater.checkForUpdatesAndNotify();
try {
window = mainWindow;
autoUpdater.checkForUpdatesAndNotify();
if (!initalized) {
initalized = true;
if (!initalized) {
initalized = true;
setInterval(() => {
autoUpdater.checkForUpdatesAndNotify();
}, 1000 * 60 * 30); // check for updates every 30 minutes
}
} catch (error) {
log.error(error);
}
setInterval(() => {
autoUpdater.checkForUpdatesAndNotify();
}, 1000 * 60 * 30); // check for updates every 30 minutes
}
} catch (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
// When the update store gets created as part of the window it will request the latest status.
export function getAutoUpdateStatus() {
return lastStatus;
return lastStatus;
}
function sendStatusToWindow(status: AutoUpdateStatus) {
lastStatus = status;
window?.webContents.send("app-update-status", status);
lastStatus = status;
window?.webContents.send("app-update-status", status);
}
autoUpdater.on("checking-for-update", () => {
log.info("checking for tea gui update");
log.info("checking for tea gui update");
});
autoUpdater.on("update-available", (info) => {
sendStatusToWindow({ status: "available" });
sendStatusToWindow({ status: "available" });
});
autoUpdater.on("update-not-available", () => {
log.info("no update for tea gui");
sendStatusToWindow({ status: "up-to-date" });
log.info("no update for tea gui");
sendStatusToWindow({ status: "up-to-date" });
});
autoUpdater.on("error", (err) => {
log.error("auto update:", err);
log.error("auto update:", err);
});
autoUpdater.on("download-progress", (progressObj) => {
let log_message = "Download speed: " + progressObj.bytesPerSecond;
log_message = log_message + " - Downloaded " + progressObj.percent + "%";
log_message = log_message + " (" + progressObj.transferred + "/" + progressObj.total + ")";
log.info("tea gui:", log_message);
let log_message = "Download speed: " + progressObj.bytesPerSecond;
log_message = log_message + " - Downloaded " + progressObj.percent + "%";
log_message = log_message + " (" + progressObj.transferred + "/" + progressObj.total + ")";
log.info("tea gui:", log_message);
});
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 async function installPackage(
full_name: string,
version: string,
notifyMainWindow: MainWindowNotifier
full_name: string,
version: string,
notifyMainWindow: MainWindowNotifier
) {
const teaVersion = await initializeTeaCli();
const progressNotifier = newInstallProgressNotifier(full_name, notifyMainWindow);
const teaVersion = await initializeTeaCli();
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 stderr = "";
let stdout = "";
let stderr = "";
await new Promise((resolve, reject) => {
// tea requires HOME to be set.
const opts = { env: { HOME: app.getPath("home"), NO_COLOR: "1" } };
await new Promise((resolve, reject) => {
// tea requires HOME to be set.
const opts = { env: { HOME: app.getPath("home"), NO_COLOR: "1" } };
const child = spawn(
`${destinationDirectory}/tea`,
["--env=false", "--sync", "--json", `+${qualifedPackage}`],
opts
);
const child = spawn(
`${destinationDirectory}/tea`,
["--env=false", "--sync", "--json", `+${qualifedPackage}`],
opts
);
child.stdout.on("data", (data) => {
stdout += data.toString().trim();
});
child.stdout.on("data", (data) => {
stdout += data.toString().trim();
});
child.stderr.on("data", (data) => {
try {
const msg = JSON.parse(data.toString().trim());
progressNotifier(msg);
} catch (err) {
//swallow it
}
child.stderr.on("data", (data) => {
try {
const msg = JSON.parse(data.toString().trim());
progressNotifier(msg);
} catch (err) {
//swallow it
}
stderr += data.toString().trim();
});
stderr += data.toString().trim();
});
child.on("exit", (code) => {
console.log("stdout:", stdout);
if (code !== 0) {
reject(new Error("tea exited with non-zero code: " + code));
} else {
resolve(null);
}
});
child.on("exit", (code) => {
console.log("stdout:", stdout);
if (code !== 0) {
reject(new Error("tea exited with non-zero code: " + code));
} else {
resolve(null);
}
});
child.on("error", () => {
reject(new Error(stderr));
});
});
child.on("error", () => {
reject(new Error(stderr));
});
});
}
// 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.
function newInstallProgressNotifier(full_name: string, notifyMainWindow: MainWindowNotifier) {
// the install progress is super spammy, only send every 10th update
let counter = 0;
// the install progress is super spammy, only send every 10th update
let counter = 0;
// the totall number of packages to install - this is set by the "resolved" message
let numberOfPackages = 1;
// the totall number of packages to install - this is set by the "resolved" message
let numberOfPackages = 1;
// the current package number - this is incremented by the "installed" or "downloaded" message
let currentPackageNumber = 0;
// the current package number - this is incremented by the "installed" or "downloaded" message
let currentPackageNumber = 0;
return function (msg: any) {
if (msg.status !== "downloading") {
log.info("cli:", msg);
}
return function (msg: any) {
if (msg.status !== "downloading") {
log.info("cli:", msg);
}
if (msg.status === "resolved") {
numberOfPackages = msg.pkgs?.length ?? 1;
log.info(`installing ${numberOfPackages} packages`);
} else if (msg.status === "downloading") {
counter++;
if (counter % 10 !== 0) return;
if (msg.status === "resolved") {
numberOfPackages = msg.pkgs?.length ?? 1;
log.info(`installing ${numberOfPackages} packages`);
} else if (msg.status === "downloading") {
counter++;
if (counter % 10 !== 0) return;
const { received = 0, "content-size": contentSize = 0 } = msg;
if (contentSize > 0) {
// how many total pacakges are completed
const overallProgress = (currentPackageNumber / numberOfPackages) * 100;
// how much of the current package is completed
const packageProgress = (received / contentSize) * 100;
// progress is the total packages completed plus the percentage of the current package
const progress = overallProgress + packageProgress / numberOfPackages;
notifyMainWindow("install-progress", { full_name, progress });
}
} else if (msg.status === "installed") {
currentPackageNumber++;
const progress = (currentPackageNumber / numberOfPackages) * 100;
notifyMainWindow("install-progress", { full_name, progress });
notifyPackageInstalled(msg.pkg, notifyMainWindow);
}
};
const { received = 0, "content-size": contentSize = 0 } = msg;
if (contentSize > 0) {
// how many total pacakges are completed
const overallProgress = (currentPackageNumber / numberOfPackages) * 100;
// how much of the current package is completed
const packageProgress = (received / contentSize) * 100;
// progress is the total packages completed plus the percentage of the current package
const progress = overallProgress + packageProgress / numberOfPackages;
notifyMainWindow("install-progress", { full_name, progress });
}
} else if (msg.status === "installed") {
currentPackageNumber++;
const progress = (currentPackageNumber / numberOfPackages) * 100;
notifyMainWindow("install-progress", { full_name, progress });
notifyPackageInstalled(msg.pkg, notifyMainWindow);
}
};
}
const notifyPackageInstalled = (rawPkg: string, notifyMainWindow: MainWindowNotifier) => {
try {
const [full_name, version] = rawPkg.split("=");
notifyMainWindow("pkg-installed", { full_name, version });
} catch (err) {
log.error("failed to notify package installed", err);
}
try {
const [full_name, version] = rawPkg.split("=");
notifyMainWindow("pkg-installed", { full_name, version });
} catch (err) {
log.error("failed to notify package installed", err);
}
};
export async function openPackageEntrypointInTerminal(pkg: string) {
let sh = `${cliBinPath} --sync --env=false +${pkg} `;
if (pkg == "github.com/AUTOMATIC1111/stable-diffusion-webui") {
sh += `~/.tea/${pkg}/v*/entrypoint.sh`;
} else {
sh += "sh";
}
let sh = `${cliBinPath} --sync --env=false +${pkg} `;
if (pkg == "github.com/AUTOMATIC1111/stable-diffusion-webui") {
sh += `~/.tea/${pkg}/v*/entrypoint.sh`;
} else {
sh += "sh";
}
const scriptPath = await createCommandScriptFile(sh);
const scriptPath = await createCommandScriptFile(sh);
try {
let stdout = "";
let stderr = "";
try {
let stdout = "";
let stderr = "";
await new Promise((resolve, reject) => {
const child = spawn("/usr/bin/osascript", [scriptPath]);
child.stdout.on("data", (data) => {
stdout += data.toString().trim();
});
child.stderr.on("data", (data) => {
stderr += data.toString().trim();
});
await new Promise((resolve, reject) => {
const child = spawn("/usr/bin/osascript", [scriptPath]);
child.stdout.on("data", (data) => {
stdout += data.toString().trim();
});
child.stderr.on("data", (data) => {
stderr += data.toString().trim();
});
child.on("exit", (code) => {
log.info("exit:", code, `\`${stdout}\``);
if (code == 0) {
resolve(stdout);
} else {
reject(new Error("failed to open terminal and run tea sh"));
}
});
child.on("exit", (code) => {
log.info("exit:", code, `\`${stdout}\``);
if (code == 0) {
resolve(stdout);
} else {
reject(new Error("failed to open terminal and run tea sh"));
}
});
child.on("error", () => {
reject(new Error(stderr));
});
});
} finally {
if (scriptPath) await fs.unlinkSync(scriptPath);
}
child.on("error", () => {
reject(new Error(stderr));
});
});
} finally {
if (scriptPath) await fs.unlinkSync(scriptPath);
}
}
const createCommandScriptFile = async (cmd: string): Promise<string> => {
const guiFolder = getGuiPath();
const tmpFilePath = path.join(guiFolder, `${+new Date()}.scpt`);
const command = `${cmd.replace(/"/g, '\\"')}`;
const script = `
const guiFolder = getGuiPath();
const tmpFilePath = path.join(guiFolder, `${+new Date()}.scpt`);
const command = `${cmd.replace(/"/g, '\\"')}`;
const script = `
tell application "Terminal"
activate
do script "${command}"
end tell
`.trim();
await fs.writeFileSync(tmpFilePath, script, "utf-8");
return tmpFilePath;
await fs.writeFileSync(tmpFilePath, script, "utf-8");
return tmpFilePath;
};
export async function asyncExec(cmd: string): Promise<string> {
return new Promise((resolve, reject) => {
exec(cmd, (err, stdout) => {
if (err) {
console.log("err:", err);
reject(err);
return;
}
console.log("stdout:", stdout);
resolve(stdout);
});
});
return new Promise((resolve, reject) => {
exec(cmd, (err, stdout) => {
if (err) {
console.log("err:", err);
reject(err);
return;
}
console.log("stdout:", stdout);
resolve(stdout);
});
});
}
export async function syncPantry() {
const teaVersion = await initializeTeaCli();
const teaVersion = await initializeTeaCli();
if (!teaVersion) throw new Error("no tea");
log.info("Syncing pantry");
await asyncExec(`cd '${destinationDirectory}' && ./tea -S`);
if (!teaVersion) throw new Error("no tea");
log.info("Syncing pantry");
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;
export async function initializeTeaCli(): Promise<string> {
if (initializePromise) {
return initializePromise;
}
if (initializePromise) {
return initializePromise;
}
log.info("Initializing tea cli");
initializePromise = initializeTeaCliInternal();
log.info("Initializing tea cli");
initializePromise = initializeTeaCliInternal();
initializePromise.catch((error) => {
log.info("Error initializing tea cli, resetting promise:", error);
initializePromise = null;
});
initializePromise.catch((error) => {
log.info("Error initializing tea cli, resetting promise:", error);
initializePromise = null;
});
return initializePromise;
return initializePromise;
}
async function initializeTeaCliInternal(): Promise<string> {
try {
let binCheck = "";
let needsUpdate = false;
try {
let binCheck = "";
let needsUpdate = false;
// Create the destination directory if it doesn't exist
if (!fs.existsSync(destinationDirectory)) {
fs.mkdirSync(destinationDirectory, { recursive: true });
}
// Create the destination directory if it doesn't exist
if (!fs.existsSync(destinationDirectory)) {
fs.mkdirSync(destinationDirectory, { recursive: true });
}
const curlCommand = `curl -L -o "${cliBinPath}" "${binaryUrl}"`;
const curlCommand = `curl -L -o "${cliBinPath}" "${binaryUrl}"`;
const exists = fs.existsSync(cliBinPath);
if (exists) {
log.info("binary tea already exists at", cliBinPath);
try {
binCheck = await asyncExec(`cd ${destinationDirectory} && ./tea --version`);
const teaVersion = binCheck.toString().split(" ")[1];
if (semverCompare(teaVersion, MINIMUM_TEA_VERSION) < 0) {
log.info("binary tea version is too old, updating");
needsUpdate = true;
}
} catch (error) {
// probably binary is not executable or no permission
log.error("Error checking tea binary version:", error);
needsUpdate = true;
await asyncExec(`cd ${destinationDirectory} && rm tea`);
}
}
const exists = fs.existsSync(cliBinPath);
if (exists) {
log.info("binary tea already exists at", cliBinPath);
try {
binCheck = await asyncExec(`cd ${destinationDirectory} && ./tea --version`);
const teaVersion = binCheck.toString().split(" ")[1];
if (semverCompare(teaVersion, MINIMUM_TEA_VERSION) < 0) {
log.info("binary tea version is too old, updating");
needsUpdate = true;
}
} catch (error) {
// probably binary is not executable or no permission
log.error("Error checking tea binary version:", error);
needsUpdate = true;
await asyncExec(`cd ${destinationDirectory} && rm tea`);
}
}
if (!exists || needsUpdate) {
try {
await asyncExec(curlCommand);
log.info("Binary downloaded and saved to", cliBinPath);
await asyncExec("chmod u+x " + cliBinPath);
log.info("Binary is now ready for use at", cliBinPath);
binCheck = await asyncExec(`cd ${destinationDirectory} && ./tea --version`);
} catch (error) {
log.error("Error setting-up tea binary:", error);
}
}
if (!exists || needsUpdate) {
try {
await asyncExec(curlCommand);
log.info("Binary downloaded and saved to", cliBinPath);
await asyncExec("chmod u+x " + cliBinPath);
log.info("Binary is now ready for use at", cliBinPath);
binCheck = await asyncExec(`cd ${destinationDirectory} && ./tea --version`);
} catch (error) {
log.error("Error setting-up tea binary:", error);
}
}
const version = binCheck.toString().split(" ")[1];
log.info("binary tea version:", version);
return semver.valid(version.trim()) ? version : "";
} catch (error) {
log.error(error);
return "";
}
const version = binCheck.toString().split(" ")[1];
log.info("binary tea version:", version);
return semver.valid(version.trim()) ? version : "";
} catch (error) {
log.error(error);
return "";
}
}
export default async function initialize(): Promise<string> {
const [version] = await Promise.all([initializeTeaCli(), createInitialSessionFile()]);
return version;
const [version] = await Promise.all([initializeTeaCli(), createInitialSessionFile()]);
return version;
}

View file

@ -15,194 +15,194 @@ import { nanoid } from "nanoid";
import { MainWindowNotifier } from "./types";
export type HandlerOptions = {
// A function to call back to the current main
notifyMainWindow: MainWindowNotifier;
// A function to call back to the current main
notifyMainWindow: MainWindowNotifier;
};
let teaProtocolPath = ""; // this should be empty string
export const setProtocolPath = (path: string) => {
teaProtocolPath = path;
teaProtocolPath = path;
};
export default function initializeHandlers({ notifyMainWindow }: HandlerOptions) {
ipcMain.handle("get-installed-packages", async () => {
try {
log.info("getting installed packages");
const pkgs = await getInstalledPackages();
log.info(`got installed packages: ${pkgs.length}`);
return pkgs;
} catch (error) {
log.error(error);
return [];
}
});
ipcMain.handle("get-installed-packages", async () => {
try {
log.info("getting installed packages");
const pkgs = await getInstalledPackages();
log.info(`got installed packages: ${pkgs.length}`);
return pkgs;
} catch (error) {
log.error(error);
return [];
}
});
ipcMain.handle("get-session", async () => {
try {
log.info("getting session");
const session = await readSessionData();
log.debug(session ? "found session data" : "no session data found");
return session;
} catch (error) {
log.error(error);
return {};
}
});
ipcMain.handle("get-session", async () => {
try {
log.info("getting session");
const session = await readSessionData();
log.debug(session ? "found session data" : "no session data found");
return session;
} catch (error) {
log.error(error);
return {};
}
});
ipcMain.handle("update-session", async (_, data) => {
try {
log.info("updating session data with", data); // rm this
await writeSessionData(data as Session);
} catch (error) {
log.error(error);
}
});
ipcMain.handle("update-session", async (_, data) => {
try {
log.info("updating session data with", data); // rm this
await writeSessionData(data as Session);
} catch (error) {
log.error(error);
}
});
ipcMain.handle("install-package", async (_, { full_name, version }) => {
try {
return await installPackage(full_name, version, notifyMainWindow);
} catch (error) {
log.error(error);
return error;
}
});
ipcMain.handle("install-package", async (_, { full_name, version }) => {
try {
return await installPackage(full_name, version, notifyMainWindow);
} catch (error) {
log.error(error);
return error;
}
});
ipcMain.handle("sync-pantry", async () => {
try {
return await syncPantry();
} catch (error) {
log.error(error);
return error;
}
});
ipcMain.handle("sync-pantry", async () => {
try {
return await syncPantry();
} catch (error) {
log.error(error);
return error;
}
});
ipcMain.handle("open-terminal", async (_, data) => {
const { pkg } = data as { pkg: string };
try {
// TODO: detect if mac or linux
// current openTerminal is only design for Mac
log.info("open tea entrypoint in terminal for pkg:", pkg);
await openPackageEntrypointInTerminal(pkg);
} catch (error) {
log.error(error);
}
});
ipcMain.handle("open-terminal", async (_, data) => {
const { pkg } = data as { pkg: string };
try {
// TODO: detect if mac or linux
// current openTerminal is only design for Mac
log.info("open tea entrypoint in terminal for pkg:", pkg);
await openPackageEntrypointInTerminal(pkg);
} catch (error) {
log.error(error);
}
});
ipcMain.handle("relaunch", async () => {
try {
log.info("relaunching app");
const autoUpdater = getUpdater();
await autoUpdater.quitAndInstall();
} catch (error) {
log.error(error);
}
});
ipcMain.handle("relaunch", async () => {
try {
log.info("relaunching app");
const autoUpdater = getUpdater();
await autoUpdater.quitAndInstall();
} catch (error) {
log.error(error);
}
});
ipcMain.handle("get-protocol-path", async () => {
const path = teaProtocolPath;
teaProtocolPath = "";
return path;
});
ipcMain.handle("get-protocol-path", async () => {
const path = teaProtocolPath;
teaProtocolPath = "";
return path;
});
ipcMain.handle("submit-logs", async () => {
try {
log.info("syncing logs");
const { device_id } = await readSessionData();
const logId = [device_id, nanoid()].join("---");
ipcMain.handle("submit-logs", async () => {
try {
log.info("syncing logs");
const { device_id } = await readSessionData();
const logId = [device_id, nanoid()].join("---");
// sync in background
syncLogsAt(logId)
.then(() => {
log.info("logs synced:", logId);
})
.catch((error) => {
log.error(error);
});
// sync in background
syncLogsAt(logId)
.then(() => {
log.info("logs synced:", logId);
})
.catch((error) => {
log.error(error);
});
return logId;
} catch (error) {
log.error(error);
return error.message;
}
});
return logId;
} catch (error) {
log.error(error);
return error.message;
}
});
ipcMain.handle("set-badge-count", async (_, { count }) => {
if (count) {
app.dock.setBadge(count.toString());
} else {
app.dock.setBadge("");
}
});
ipcMain.handle("set-badge-count", async (_, { count }) => {
if (count) {
app.dock.setBadge(count.toString());
} else {
app.dock.setBadge("");
}
});
ipcMain.handle(
"delete-package",
async (_, { fullName, version }: { fullName: string; version: string }) => {
try {
log.info("deleting package:", fullName);
await deletePackageFolder(fullName, version);
} catch (e) {
log.error(e);
return e;
}
}
);
ipcMain.handle(
"delete-package",
async (_, { fullName, version }: { fullName: string; version: string }) => {
try {
log.info("deleting package:", fullName);
await deletePackageFolder(fullName, version);
} catch (e) {
log.error(e);
return e;
}
}
);
ipcMain.handle("write-package-cache", async (_, data) => {
try {
await writePackageCache(data as Packages);
} catch (error) {
log.error(error);
}
});
ipcMain.handle("write-package-cache", async (_, data) => {
try {
await writePackageCache(data as Packages);
} catch (error) {
log.error(error);
}
});
ipcMain.handle("load-package-cache", async () => {
try {
return await loadPackageCache();
} catch (error) {
log.error(error);
return { version: "1", packages: {} };
}
});
ipcMain.handle("load-package-cache", async () => {
try {
return await loadPackageCache();
} catch (error) {
log.error(error);
return { version: "1", packages: {} };
}
});
ipcMain.handle("get-tea-version", async () => {
try {
log.info("installing tea cli");
const version = await initializeTeaCli();
if (!version) {
throw new Error("failed to install tea cli");
}
ipcMain.handle("get-tea-version", async () => {
try {
log.info("installing tea cli");
const version = await initializeTeaCli();
if (!version) {
throw new Error("failed to install tea cli");
}
return { version, message: "" };
} catch (error) {
log.error(error);
return { version: "", message: error.message };
}
});
return { version, message: "" };
} catch (error) {
log.error(error);
return { version: "", message: error.message };
}
});
ipcMain.handle("topbar-double-click", async (event: Electron.IpcMainInvokeEvent) => {
const mainWindow = BrowserWindow.fromWebContents(event.sender);
if (mainWindow) {
mainWindow.isMaximized() ? mainWindow.unmaximize() : mainWindow.maximize();
}
});
ipcMain.handle("topbar-double-click", async (event: Electron.IpcMainInvokeEvent) => {
const mainWindow = BrowserWindow.fromWebContents(event.sender);
if (mainWindow) {
mainWindow.isMaximized() ? mainWindow.unmaximize() : mainWindow.maximize();
}
});
ipcMain.handle("cache-image", async (_event, url) => {
try {
log.info("caching:", url);
const cachedImagePath = await cacheImage(url);
return cachedImagePath;
} catch (error) {
log.error("Failed to cache image:", error);
throw error;
}
});
ipcMain.handle("cache-image", async (_event, url) => {
try {
log.info("caching:", url);
const cachedImagePath = await cacheImage(url);
return cachedImagePath;
} catch (error) {
log.error("Failed to cache image:", error);
throw error;
}
});
ipcMain.handle("get-auto-update-status", async () => {
try {
log.info("getting auto update status");
return getAutoUpdateStatus();
} catch (error) {
log.error(error);
}
});
ipcMain.handle("get-auto-update-status", async () => {
try {
log.info("getting auto update status");
return getAutoUpdateStatus();
} catch (error) {
log.error(error);
}
});
}

View file

@ -1,12 +1,12 @@
import log from "electron-log";
export const setSentryLogging = (sentry: any) => {
const oldError = log.error;
const oldError = log.error;
log.error = (...params: any[]) => {
oldError(params);
sentry.captureException(params[0].message);
};
log.error = (...params: any[]) => {
oldError(params);
sentry.captureException(params[0].message);
};
};
// 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");
export async function writePackageCache(pkgs: Packages) {
try {
if (!pkgs || !Object.keys(pkgs.packages).length) {
return;
}
try {
if (!pkgs || !Object.keys(pkgs.packages).length) {
return;
}
log.info(`writing data for ${Object.keys(pkgs.packages).length} packages to ${pkgsFilePath}`);
await mkdirp(pkgsFolder);
fs.writeFileSync(pkgsFilePath, JSON.stringify(pkgs), {
encoding: "utf-8"
});
} catch (error) {
log.error(error);
}
log.info(`writing data for ${Object.keys(pkgs.packages).length} packages to ${pkgsFilePath}`);
await mkdirp(pkgsFolder);
fs.writeFileSync(pkgsFilePath, JSON.stringify(pkgs), {
encoding: "utf-8"
});
} catch (error) {
log.error(error);
}
}
export async function loadPackageCache(): Promise<Packages> {
try {
log.info(`loading package cache from ${pkgsFilePath}`);
const pkgData = fs.readFileSync(pkgsFilePath);
return JSON.parse(pkgData.toString()) as Packages;
} catch (err) {
if (err.code !== "ENOENT") {
log.error(err);
}
return { version: "1", packages: {} };
}
try {
log.info(`loading package cache from ${pkgsFilePath}`);
const pkgData = fs.readFileSync(pkgsFilePath);
return JSON.parse(pkgData.toString()) as Packages;
} catch (err) {
if (err.code !== "ENOENT") {
log.error(err);
}
return { version: "1", packages: {} };
}
}
export const nameToSlug = (name: string) => {
// github.com/Pypa/twine -> github_com_pypa_twine
const [nameOnly] = name.split("@");
const slug = nameOnly.replace(/[^\w\s]/gi, "_").toLocaleLowerCase();
return slug;
// github.com/Pypa/twine -> github_com_pypa_twine
const [nameOnly] = name.split("@");
const slug = nameOnly.replace(/[^\w\s]/gi, "_").toLocaleLowerCase();
return slug;
};

View file

@ -5,10 +5,10 @@ import log from "./logger";
import { Notification, BrowserWindow } from "electron";
import { nameToSlug } from "./package";
import {
getInstalledPackages,
getPackagesInstalledList,
updatePackageInstalledList,
getGuiPath
getInstalledPackages,
getPackagesInstalledList,
updatePackageInstalledList,
getGuiPath
} from "./tea-dir";
import { app } from "electron";
import { promisify } from "util";
@ -21,162 +21,162 @@ const readFile = promisify(fs.readFile);
const writeFile = promisify(fs.writeFile);
export default function initialize(mainWindow: BrowserWindow) {
if (config.PUSHY_APP_ID) {
Pushy.listen();
// Register device for push notifications
Pushy.register({ appId: config.PUSHY_APP_ID })
.then(async (push_token) => {
const { device_id } = await readSessionData();
log.info(
`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 });
})
.catch((err) => {
log.error(err);
// Display error dialog
// Pushy.alert(mainWindow, 'Pushy registration error: ' + err.message);
});
if (config.PUSHY_APP_ID) {
Pushy.listen();
// Register device for push notifications
Pushy.register({ appId: config.PUSHY_APP_ID })
.then(async (push_token) => {
const { device_id } = await readSessionData();
log.info(
`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 });
})
.catch((err) => {
log.error(err);
// Display error dialog
// Pushy.alert(mainWindow, 'Pushy registration error: ' + err.message);
});
// Listen for incoming notifications
Pushy.setNotificationListener(async (data: any) => {
try {
log.info("new notification received", data);
// Listen for incoming notifications
Pushy.setNotificationListener(async (data: any) => {
try {
log.info("new notification received", data);
const isDup = await wasReceivedBefore(data);
if (!isDup) {
new Notification({
title: "tea",
body: data?.message as string
}).show();
const isDup = await wasReceivedBefore(data);
if (!isDup) {
new Notification({
title: "tea",
body: data?.message as string
}).show();
const v = app.dock.getBadge();
if (!v) {
app.dock.setBadge("1");
} else {
app.dock.setBadge((parseInt(v) + 1).toString());
}
} else {
log.info("notification was already received before", data);
}
} catch (error) {
log.error("notification listener", error);
}
});
}
const v = app.dock.getBadge();
if (!v) {
app.dock.setBadge("1");
} else {
app.dock.setBadge((parseInt(v) + 1).toString());
}
} else {
log.info("notification was already received before", data);
}
} catch (error) {
log.error("notification listener", error);
}
});
}
}
export async function subscribeToPackageTopic(pkgFullname: string) {
try {
if (Pushy.isRegistered()) {
const slug = nameToSlug(pkgFullname);
try {
if (Pushy.isRegistered()) {
const slug = nameToSlug(pkgFullname);
// override rules for brewkit_mnt
if (slug.includes("brewkit_mnt")) return;
// override rules for brewkit_mnt
if (slug.includes("brewkit_mnt")) return;
const platformArch = getTopicArch();
const topic = `packages-${slug}_${platformArch}`;
const platformArch = getTopicArch();
const topic = `packages-${slug}_${platformArch}`;
await Pushy.subscribe(topic);
log.info("push: registered to pkg-topic: ", topic);
} else {
log.info("pushy is not registered");
}
} catch (error) {
log.error(error);
}
await Pushy.subscribe(topic);
log.info("push: registered to pkg-topic: ", topic);
} else {
log.info("pushy is not registered");
}
} catch (error) {
log.error(error);
}
}
export async function unsubscribeToPackageTopic(pkgFullname: string) {
try {
if (Pushy.isRegistered()) {
const slug = nameToSlug(pkgFullname);
const topic = `packages-${slug}`;
await Pushy.unsubscribe(topic);
log.info("push: unregistered from pkg-topic: ", topic);
} else {
log.info("pushy is not registered");
}
} catch (error) {
log.error(error);
}
try {
if (Pushy.isRegistered()) {
const slug = nameToSlug(pkgFullname);
const topic = `packages-${slug}`;
await Pushy.unsubscribe(topic);
log.info("push: unregistered from pkg-topic: ", topic);
} else {
log.info("pushy is not registered");
}
} catch (error) {
log.error(error);
}
}
export async function syncPackageTopicSubscriptions() {
try {
log.info("syncing package topic subscriptions");
const [installedPackages, lastInstalledList] = await Promise.all([
getInstalledPackages(),
getPackagesInstalledList()
]);
try {
log.info("syncing package topic subscriptions");
const [installedPackages, lastInstalledList] = await Promise.all([
getInstalledPackages(),
getPackagesInstalledList()
]);
const previouslyInstalledNames = lastInstalledList.map((pkg) => pkg.full_name);
const currentlyInstalledNames = installedPackages.map((pkg) => pkg.full_name);
const previouslyInstalledNames = lastInstalledList.map((pkg) => pkg.full_name);
const currentlyInstalledNames = installedPackages.map((pkg) => pkg.full_name);
const subscribedTo = currentlyInstalledNames.filter(
(pkg) => !previouslyInstalledNames.includes(pkg)
);
const unsubscribedFrom = previouslyInstalledNames.filter(
(pkg) => !currentlyInstalledNames.includes(pkg)
);
const subscribedTo = currentlyInstalledNames.filter(
(pkg) => !previouslyInstalledNames.includes(pkg)
);
const unsubscribedFrom = previouslyInstalledNames.filter(
(pkg) => !currentlyInstalledNames.includes(pkg)
);
for (const subscribe of subscribedTo) {
await subscribeToPackageTopic(subscribe);
}
for (const unsubscribe of unsubscribedFrom) {
await unsubscribeToPackageTopic(unsubscribe);
}
for (const subscribe of subscribedTo) {
await subscribeToPackageTopic(subscribe);
}
for (const unsubscribe of unsubscribedFrom) {
await unsubscribeToPackageTopic(unsubscribe);
}
await updatePackageInstalledList(installedPackages);
} catch (error) {
log.error(error);
}
await updatePackageInstalledList(installedPackages);
} catch (error) {
log.error(error);
}
}
enum PlatformArch {
DarwinAarch64 = "darwin_aarch64",
DarwinX86_64 = "darwin_x86-64",
LinuxAarch64 = "linux_aarch64",
LinuxX86_64 = "linux_x86-64"
DarwinAarch64 = "darwin_aarch64",
DarwinX86_64 = "darwin_x86-64",
LinuxAarch64 = "linux_aarch64",
LinuxX86_64 = "linux_x86-64"
}
export function getTopicArch() {
const arch = (process.arch as string) === "arm64" ? "aarch64" : "x86-64";
const platform = process.platform === "darwin" ? "darwin" : "linux";
return `${platform}_${arch}` as PlatformArch;
const arch = (process.arch as string) === "arm64" ? "aarch64" : "x86-64";
const platform = process.platform === "darwin" ? "darwin" : "linux";
return `${platform}_${arch}` as PlatformArch;
}
async function wasReceivedBefore({
url,
version
url,
version
}: {
url: string;
version: string;
url: string;
version: string;
}): Promise<boolean> {
if (!url || !version) return false;
if (!url || !version) return false;
let received = false;
const pkg = url.replace("tea://packages/", "");
const searchString = `${pkg}:::${version}`;
const notificationPath = path.join(getGuiPath(), "notifications");
try {
const fileContent = await readFile(notificationPath, "utf-8");
let received = false;
const pkg = url.replace("tea://packages/", "");
const searchString = `${pkg}:::${version}`;
const notificationPath = path.join(getGuiPath(), "notifications");
try {
const fileContent = await readFile(notificationPath, "utf-8");
if (fileContent.includes(searchString)) {
log.info("user has already been notified before of ", searchString);
received = true;
} else {
const appendString = fileContent ? `\n${searchString}` : searchString;
await writeFile(notificationPath, fileContent + appendString, "utf-8");
}
} catch (error) {
if (error.code === "ENOENT") {
// If the file does not exist, create the file and write the string
await writeFile(notificationPath, searchString, "utf-8");
log.info("notification file created with the ", searchString);
} else {
log.error("Error processing the file:", error);
}
}
return received;
if (fileContent.includes(searchString)) {
log.info("user has already been notified before of ", searchString);
received = true;
} else {
const appendString = fileContent ? `\n${searchString}` : searchString;
await writeFile(notificationPath, fileContent + appendString, "utf-8");
}
} catch (error) {
if (error.code === "ENOENT") {
// If the file does not exist, create the file and write the string
await writeFile(notificationPath, searchString, "utf-8");
log.info("notification file created with the ", searchString);
} else {
log.error("Error processing the file:", error);
}
}
return received;
}

View file

@ -10,195 +10,195 @@ import { mkdirp } from "mkdirp";
import fetch from "node-fetch";
type Dir = {
name: string;
path: string;
children?: Dir[];
name: string;
path: string;
children?: Dir[];
};
type ParsedVersion = { full_name: string; semVer: SemVer };
export const getTeaPath = () => {
const homePath = app.getPath("home");
const teaPath = path.join(homePath, "./.tea");
return teaPath;
const homePath = app.getPath("home");
const teaPath = path.join(homePath, "./.tea");
return teaPath;
};
const guiFolder = path.join(getTeaPath(), "tea.xyz/gui");
export const getGuiPath = () => {
return path.join(getTeaPath(), "tea.xyz/gui");
return path.join(getTeaPath(), "tea.xyz/gui");
};
export async function getInstalledPackages(): Promise<InstalledPackage[]> {
const pkgsPath = getTeaPath();
log.info("recursively reading:", pkgsPath);
const folders = await deepReadDir({
dir: pkgsPath,
continueDeeper: (name: string) => !semver.valid(name) && name !== ".tea",
filter: (name: string) => !!semver.valid(name) && name !== ".tea"
});
const pkgsPath = getTeaPath();
log.info("recursively reading:", pkgsPath);
const folders = await deepReadDir({
dir: pkgsPath,
continueDeeper: (name: string) => !semver.valid(name) && name !== ".tea",
filter: (name: string) => !!semver.valid(name) && name !== ".tea"
});
const bottles = folders
.map((p: string) => p.split(".tea/")[1])
.map(parseVersionFromPath)
.filter((v): v is ParsedVersion => !!v)
.sort((a, b) => semverCompare(b.semVer, a.semVer));
const bottles = folders
.map((p: string) => p.split(".tea/")[1])
.map(parseVersionFromPath)
.filter((v): v is ParsedVersion => !!v)
.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) => {
const pkg = pkgs.find((v) => v.full_name === bottle.full_name);
if (pkg) {
pkg.installed_versions.push(bottle.semVer.version);
} else {
pkgs.push({
full_name: bottle.full_name,
installed_versions: [bottle.semVer.version]
});
}
return pkgs;
}, []);
return bottles.reduce<InstalledPackage[]>((pkgs, bottle) => {
const pkg = pkgs.find((v) => v.full_name === bottle.full_name);
if (pkg) {
pkg.installed_versions.push(bottle.semVer.version);
} else {
pkgs.push({
full_name: bottle.full_name,
installed_versions: [bottle.semVer.version]
});
}
return pkgs;
}, []);
}
const parseVersionFromPath = (versionPath: string): ParsedVersion | null => {
try {
const path = versionPath.trim().split("/");
const version = path.pop();
return {
semVer: new SemVer(semver.clean(version || "") || ""),
full_name: path.join("/")
};
} catch (e) {
log.error("error parsing version from path: ", versionPath);
return null;
}
try {
const path = versionPath.trim().split("/");
const version = path.pop();
return {
semVer: new SemVer(semver.clean(version || "") || ""),
full_name: path.join("/")
};
} catch (e) {
log.error("error parsing version from path: ", versionPath);
return null;
}
};
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[] => {
log.info("getting installed bottle for ", packageDir);
const bottles: string[] = [];
log.info("getting installed bottle for ", packageDir);
const bottles: string[] = [];
const pkg = packageDir.path.split(".tea/")[1];
const version = pkg.split("/v")[1];
const pkg = packageDir.path.split(".tea/")[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) {
bottles.push(pkg);
} else if (packageDir?.children?.length) {
const childBottles = packageDir.children
.map(getPkgBottles)
.reduce((arr, bottles) => [...arr, ...bottles], []);
bottles.push(...childBottles);
}
if (version && isVersion) {
bottles.push(pkg);
} else if (packageDir?.children?.length) {
const childBottles = packageDir.children
.map(getPkgBottles)
.reduce((arr, bottles) => [...arr, ...bottles], []);
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);
return foundBottles;
log.info(`Found ${foundBottles.length} bottles from `, packageDir);
return foundBottles;
};
export const deepReadDir = async ({
dir,
continueDeeper,
filter
dir,
continueDeeper,
filter
}: {
dir: string;
continueDeeper?: (name: string) => boolean;
filter?: (name: string) => boolean;
dir: string;
continueDeeper?: (name: string) => boolean;
filter?: (name: string) => boolean;
}) => {
const arrayOfFiles: string[] = [];
try {
const files = fs.readdirSync(dir, { withFileTypes: true });
for (const f of files) {
const nextPath = path.join(dir, f.name);
const deeper = continueDeeper ? continueDeeper(f.name) : true;
if (f.isDirectory() && deeper) {
const nextFiles = await deepReadDir({ dir: nextPath, continueDeeper, filter });
arrayOfFiles.push(...nextFiles);
} else if (filter && filter(f.name)) {
arrayOfFiles.push(nextPath);
} else if (!filter) {
arrayOfFiles.push(nextPath);
}
}
} catch (e) {
log.error(e);
}
return arrayOfFiles;
const arrayOfFiles: string[] = [];
try {
const files = fs.readdirSync(dir, { withFileTypes: true });
for (const f of files) {
const nextPath = path.join(dir, f.name);
const deeper = continueDeeper ? continueDeeper(f.name) : true;
if (f.isDirectory() && deeper) {
const nextFiles = await deepReadDir({ dir: nextPath, continueDeeper, filter });
arrayOfFiles.push(...nextFiles);
} else if (filter && filter(f.name)) {
arrayOfFiles.push(nextPath);
} else if (!filter) {
arrayOfFiles.push(nextPath);
}
}
} catch (e) {
log.error(e);
}
return arrayOfFiles;
};
const listFilePath = path.join(getGuiPath(), "installed.json");
export const getPackagesInstalledList = async (): Promise<InstalledPackage[]> => {
let list: InstalledPackage[] = [];
try {
if (fs.existsSync(listFilePath)) {
log.info("gui/installed.json file exists!");
const listBuffer = await fs.readFileSync(listFilePath);
list = JSON.parse(listBuffer.toString()) as InstalledPackage[];
} else {
log.info("gui/installed.json does not exists!");
await mkdirp(guiFolder);
await updatePackageInstalledList([]);
}
} catch (error) {
log.error(error);
}
return list;
let list: InstalledPackage[] = [];
try {
if (fs.existsSync(listFilePath)) {
log.info("gui/installed.json file exists!");
const listBuffer = await fs.readFileSync(listFilePath);
list = JSON.parse(listBuffer.toString()) as InstalledPackage[];
} else {
log.info("gui/installed.json does not exists!");
await mkdirp(guiFolder);
await updatePackageInstalledList([]);
}
} catch (error) {
log.error(error);
}
return list;
};
export async function updatePackageInstalledList(list: InstalledPackage[]) {
try {
log.info("creating:", listFilePath);
await mkdirp(guiFolder);
await fs.writeFileSync(listFilePath, JSON.stringify(list), {
encoding: "utf-8"
});
} catch (error) {
log.error(error);
}
try {
log.info("creating:", listFilePath);
await mkdirp(guiFolder);
await fs.writeFileSync(listFilePath, JSON.stringify(list), {
encoding: "utf-8"
});
} catch (error) {
log.error(error);
}
}
export async function deletePackageFolder(fullName, version) {
try {
const foldPath = path.join(getTeaPath(), fullName, `v${version}`);
log.info("rm:", foldPath);
await fs.rmdirSync(foldPath, { recursive: true });
} catch (error) {
log.error(error);
}
try {
const foldPath = path.join(getTeaPath(), fullName, `v${version}`);
log.info("rm:", foldPath);
await fs.rmdirSync(foldPath, { recursive: true });
} catch (error) {
log.error(error);
}
}
async function downloadImage(url: string, imagePath: string): Promise<void> {
const response = await fetch(url);
await new Promise<void>((resolve, reject) => {
const fileStream = fs.createWriteStream(imagePath);
response.body.pipe(fileStream);
fileStream.on("finish", () => resolve());
fileStream.on("error", (error) => reject(error));
});
const response = await fetch(url);
await new Promise<void>((resolve, reject) => {
const fileStream = fs.createWriteStream(imagePath);
response.body.pipe(fileStream);
fileStream.on("finish", () => resolve());
fileStream.on("error", (error) => reject(error));
});
}
export async function cacheImage(url: string): Promise<string> {
const imageFolder = path.join(getGuiPath(), "cached_images");
const imageName = path.basename(url);
const imagePath = path.join(imageFolder, imageName);
const imageFolder = path.join(getGuiPath(), "cached_images");
const imageName = path.basename(url);
const imagePath = path.join(imageFolder, imageName);
await mkdirp(imageFolder);
await mkdirp(imageFolder);
if (!fs.existsSync(imagePath)) {
try {
await downloadImage(url, imagePath);
console.log("Image downloaded and cached:", imagePath);
} catch (error) {
console.error("Failed to download image:", error);
}
} else {
console.log("Image already cached:", imagePath);
}
if (!fs.existsSync(imagePath)) {
try {
await downloadImage(url, imagePath);
console.log("Image downloaded and cached:", imagePath);
} catch (error) {
console.error("Failed to download image:", error);
}
} else {
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 publicHeader = { Authorization: "public" };
export async function get<T>(urlPath: string) {
try {
log.info(`GET /v1/${urlPath}`);
try {
log.info(`GET /v1/${urlPath}`);
const session = await readSessionData();
const headers =
session?.device_id && session?.user
? await getHeaders(`GET/${urlPath}`, session)
: publicHeader;
const session = await readSessionData();
const headers =
session?.device_id && session?.user
? await getHeaders(`GET/${urlPath}`, session)
: publicHeader;
const url = new URL(path.join("v1", urlPath), base).toString();
// TODO: add headers
const req = await axios.request<T>({
method: "GET",
url,
headers
});
const url = new URL(path.join("v1", urlPath), base).toString();
// TODO: add headers
const req = await axios.request<T>({
method: "GET",
url,
headers
});
log.info("REQUEST:", urlPath, req.status);
log.info("REQUEST:", urlPath, req.status);
return req.data;
} catch (error) {
log.error(error);
return null;
}
return req.data;
} catch (error) {
log.error(error);
return null;
}
}
export async function post<T>(urlPath: string, data: { [key: string]: any }) {
try {
log.info(`POST /v1/${urlPath}`);
try {
log.info(`POST /v1/${urlPath}`);
const session = await readSessionData();
const headers =
session?.device_id && session?.user
? await getHeaders(`GET/${urlPath}`, session)
: publicHeader;
const session = await readSessionData();
const headers =
session?.device_id && session?.user
? await getHeaders(`GET/${urlPath}`, session)
: publicHeader;
const url = new URL(path.join("v1", urlPath), base).toString();
const req = await axios.request<T>({
method: "POST",
url,
headers,
data
});
const url = new URL(path.join("v1", urlPath), base).toString();
const req = await axios.request<T>({
method: "POST",
url,
headers,
data
});
log.info("REQUEST:", urlPath, req.status);
log.info("REQUEST:", urlPath, req.status);
return req.data;
} catch (error) {
log.error(error);
return null;
}
return req.data;
} catch (error) {
log.error(error);
return null;
}
}
async function getHeaders(path: string, session: Session) {
const unixMs = new Date().getTime();
const unixHexSecs = Math.round(unixMs / 1000).toString(16); // hex
const deviceId = session.device_id?.split("-")[0];
const preHash = [unixHexSecs, session.key, deviceId, path].join("");
const unixMs = new Date().getTime();
const unixHexSecs = Math.round(unixMs / 1000).toString(16); // hex
const deviceId = session.device_id?.split("-")[0];
const preHash = [unixHexSecs, session.key, deviceId, path].join("");
const Authorization = bcrypt.hashSync(preHash, 10);
const Authorization = bcrypt.hashSync(preHash, 10);
return {
Authorization,
["tea-ts"]: unixMs.toString(),
["tea-uid"]: session.user?.developer_id,
["tea-gui_id"]: session.device_id
};
return {
Authorization,
["tea-ts"]: unixMs.toString(),
["tea-uid"]: session.user?.developer_id,
["tea-gui_id"]: session.device_id
};
}
export async function syncLogsAt(prefix: string) {
const logDir = path.join(app.getPath("home"), "Library/Logs/tea");
// ['/Users/neil/Library/Logs/tea/main.log']
const logFiles = await deepReadDir({ dir: logDir });
const files = logFiles.map((p) => {
const paths = p.split("/");
return paths.pop();
});
const logDir = path.join(app.getPath("home"), "Library/Logs/tea");
// ['/Users/neil/Library/Logs/tea/main.log']
const logFiles = await deepReadDir({ dir: logDir });
const files = logFiles.map((p) => {
const paths = p.split("/");
return paths.pop();
});
const signedUrls = await post<{ [key: string]: string }>(`/gui/${prefix}/sync-log-files`, {
files
});
if (signedUrls) {
for (const key in signedUrls) {
const fileIndex = files.indexOf(key);
const filePath = logFiles[fileIndex];
if (filePath) {
const payload = createReadStream(filePath);
const response = await fetch(signedUrls[key], {
method: "PUT",
body: payload,
headers: {
"Content-Length": statSync(filePath).size.toString()
}
});
log.info("uploading log:", key, response.status);
}
}
}
const signedUrls = await post<{ [key: string]: string }>(`/gui/${prefix}/sync-log-files`, {
files
});
if (signedUrls) {
for (const key in signedUrls) {
const fileIndex = files.indexOf(key);
const filePath = logFiles[fileIndex];
if (filePath) {
const payload = createReadStream(filePath);
const response = await fetch(signedUrls[key], {
method: "PUT",
body: payload,
headers: {
"Content-Length": statSync(filePath).size.toString()
}
});
log.info("uploading log:", key, response.status);
}
}
}
}
export default get;

View file

@ -1,19 +1,19 @@
const isVite = () => {
try {
return window.location.href.includes("is-vite");
} catch (error) {
return false;
}
try {
return window.location.href.includes("is-vite");
} catch (error) {
return false;
}
};
if (!isVite()) {
const { init } = window.require("@sentry/electron/renderer");
const SvelteSentry = window.require("@sentry/svelte");
init(
{
dsn: "https://5ff29bb5b3b64cd4bd4f4960ef1db2e3@o4504750197899264.ingest.sentry.io/4504750206746624",
debug: true
},
SvelteSentry.init
);
const { init } = window.require("@sentry/electron/renderer");
const SvelteSentry = window.require("@sentry/svelte");
init(
{
dsn: "https://5ff29bb5b3b64cd4bd4f4960ef1db2e3@o4504750197899264.ingest.sentry.io/4504750206746624",
debug: true
},
SvelteSentry.init
);
}

View file

@ -9,41 +9,41 @@ const PROJECT_ROOT = join(PACKAGE_ROOT, "../..");
* @see https://vitejs.dev/config/
*/
const config = {
root: PACKAGE_ROOT,
envDir: PROJECT_ROOT,
resolve: {
alias: {
"/@/": join(PACKAGE_ROOT, "src") + "/"
}
},
build: {
ssr: true,
sourcemap: "inline",
outDir: "dist",
assetsDir: ".",
minify: process.env.MODE !== "development",
lib: {
entry: "electron.ts",
formats: ["cjs"]
},
rollupOptions: {
output: {
entryFileNames: "[name].cjs"
}
},
emptyOutDir: true,
reportCompressedSize: false
},
plugins: [
viteStaticCopy({
targets: [
{
src: "./preload.cjs",
dest: "."
}
]
})
]
root: PACKAGE_ROOT,
envDir: PROJECT_ROOT,
resolve: {
alias: {
"/@/": join(PACKAGE_ROOT, "src") + "/"
}
},
build: {
ssr: true,
sourcemap: "inline",
outDir: "dist",
assetsDir: ".",
minify: process.env.MODE !== "development",
lib: {
entry: "electron.ts",
formats: ["cjs"]
},
rollupOptions: {
output: {
entryFileNames: "[name].cjs"
}
},
emptyOutDir: true,
reportCompressedSize: false
},
plugins: [
viteStaticCopy({
targets: [
{
src: "./preload.cjs",
dest: "."
}
]
})
]
};
export default config;

View file

@ -1,121 +1,121 @@
{
"name": "tea",
"version": "0.0.45",
"private": true,
"description": "tea gui app",
"author": "tea.xyz",
"main": "electron/dist/electron.cjs",
"scripts": {
"prepare": "svelte-kit sync",
"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:svelte": "vite dev",
"dev:electron": "electron electron/dist/electron.cjs",
"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",
"pack": "electron-builder --dir --config electron-builder.config.cjs",
"predist": "node ./scripts/predist.cjs",
"dist": "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",
"electron": "concurrently --kill-others \"vite dev\" \"electron electron/dist/electron.cjs\"",
"olddev": "vite dev",
"build": "pnpm build:main && vite build && cp build/app.html build/index.html",
"preview": "vite preview",
"unit:test": "vitest",
"coverage": "vitest run --coverage",
"test": "playwright test",
"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",
"lint": "prettier --plugin-search-dir . --check . && eslint .",
"format": "prettier --plugin-search-dir . --write ."
},
"devDependencies": {
"@electron/notarize": "^1.2.3",
"@playwright/experimental-ct-svelte": "^1.29.2",
"@playwright/test": "1.25.0",
"@sveltejs/adapter-auto": "^1.0.0",
"@sveltejs/adapter-node": "^1.0.0-next.101",
"@sveltejs/adapter-static": "^1.0.0-next.48",
"@sveltejs/kit": "^1.0.0-next.562",
"@tea/ui": "workspace:*",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/svelte": "^3.2.2",
"@types/bcryptjs": "^2.4.2",
"@types/js-yaml": "^4.0.5",
"@types/mixpanel-browser": "^2.38.1",
"@types/testing-library__jest-dom": "^5.14.5",
"@typescript-eslint/eslint-plugin": "^5.27.0",
"@typescript-eslint/parser": "^5.27.0",
"autoprefixer": "^10.4.13",
"concurrently": "^7.6.0",
"cross-env": "^7.0.3",
"electron": "22.1.0",
"electron-builder": "^23.6.0",
"electron-reloader": "^1.2.3",
"eslint": "^8.16.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-svelte3": "^4.0.0",
"jsdom": "^21.0.0",
"postcss": "^8.4.19",
"prettier": "^2.7.1",
"prettier-plugin-svelte": "^2.7.0",
"prettier-plugin-tailwindcss": "^0.2.0",
"svelte": "^3.55.1",
"svelte-check": "^2.8.0",
"svelte-preprocess": "^5.0.1",
"svelte2tsx": "^0.5.20",
"tailwindcss": "^3.2.4",
"tslib": "^2.3.1",
"typescript": "^4.7.4",
"vite": "^4.1.1",
"vitest": "^0.28.3"
},
"type": "module",
"dependencies": {
"@crowdin/ota-client": "^0.7.0",
"@electron/asar": "^3.2.3",
"@sentry/browser": "^7.49.0",
"@sentry/electron": "^4.4.0",
"@sentry/svelte": "^7.47.0",
"@types/electron": "^1.6.10",
"@types/mousetrap": "^1.6.11",
"@vitest/coverage-c8": "^0.27.1",
"axios": "^1.3.2",
"bcryptjs": "^2.4.3",
"buffer": "^6.0.3",
"custom-electron-titlebar": "4.2.0-beta.0",
"dayjs": "^1.11.7",
"electron-context-menu": "^3.6.1",
"electron-log": "^4.4.8",
"electron-serve": "^1.1.0",
"electron-updater": "^5.3.0",
"electron-vite": "^1.0.18",
"electron-window-state": "^5.0.3",
"fuse.js": "^6.6.2",
"js-yaml": "^4.1.0",
"lodash": "^4.17.21",
"lorem-ipsum": "^2.0.8",
"mixpanel-browser": "^2.45.0",
"mkdirp": "^2.1.3",
"moment": "^2.29.4",
"mousetrap": "^1.6.5",
"pushy-electron": "^1.0.11",
"renderer": "link:@types/electron/renderer",
"semver": "^7.3.8",
"svelte-infinite-scroll": "^2.0.1",
"svelte-markdown": "^0.2.3",
"svelte-watch-resize": "^1.0.3",
"sveltekit-i18n": "^2.2.2",
"upath": "^2.0.1",
"vite-plugin-static-copy": "^0.13.1",
"yaml": "^2.2.1"
},
"pnpm": {
"onlyBuiltDependencies": [
"@tea/ui"
]
},
"homepage": "https://tea.xyz",
"repository": "https://github.com/teaxyz/gui.git"
"name": "tea",
"version": "0.0.45",
"private": true,
"description": "tea gui app",
"author": "tea.xyz",
"main": "electron/dist/electron.cjs",
"scripts": {
"prepare": "svelte-kit sync",
"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:svelte": "vite dev",
"dev:electron": "electron electron/dist/electron.cjs",
"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",
"pack": "electron-builder --dir --config electron-builder.config.cjs",
"predist": "node ./scripts/predist.cjs",
"dist": "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",
"electron": "concurrently --kill-others \"vite dev\" \"electron electron/dist/electron.cjs\"",
"olddev": "vite dev",
"build": "pnpm build:main && vite build && cp build/app.html build/index.html",
"preview": "vite preview",
"unit:test": "vitest",
"coverage": "vitest run --coverage",
"test": "playwright test",
"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",
"lint": "prettier --check . && eslint .",
"format": "prettier --write ."
},
"devDependencies": {
"@electron/notarize": "^1.2.3",
"@playwright/experimental-ct-svelte": "^1.29.2",
"@playwright/test": "1.25.0",
"@sveltejs/adapter-auto": "^1.0.0",
"@sveltejs/adapter-node": "^1.0.0-next.101",
"@sveltejs/adapter-static": "^1.0.0-next.48",
"@sveltejs/kit": "^1.0.0-next.562",
"@tea/ui": "workspace:*",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/svelte": "^3.2.2",
"@types/bcryptjs": "^2.4.2",
"@types/js-yaml": "^4.0.5",
"@types/mixpanel-browser": "^2.38.1",
"@types/testing-library__jest-dom": "^5.14.5",
"@typescript-eslint/eslint-plugin": "^5.27.0",
"@typescript-eslint/parser": "^5.27.0",
"autoprefixer": "^10.4.13",
"concurrently": "^7.6.0",
"cross-env": "^7.0.3",
"electron": "22.1.0",
"electron-builder": "^23.6.0",
"electron-reloader": "^1.2.3",
"eslint": "^8.16.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-svelte3": "^4.0.0",
"jsdom": "^21.0.0",
"postcss": "^8.4.19",
"prettier": "^2.7.1",
"prettier-plugin-svelte": "^2.7.0",
"prettier-plugin-tailwindcss": "^0.2.0",
"svelte": "^3.55.1",
"svelte-check": "^2.8.0",
"svelte-preprocess": "^5.0.1",
"svelte2tsx": "^0.5.20",
"tailwindcss": "^3.2.4",
"tslib": "^2.3.1",
"typescript": "^4.7.4",
"vite": "^4.1.1",
"vitest": "^0.28.3"
},
"type": "module",
"dependencies": {
"@crowdin/ota-client": "^0.7.0",
"@electron/asar": "^3.2.3",
"@sentry/browser": "^7.49.0",
"@sentry/electron": "^4.4.0",
"@sentry/svelte": "^7.47.0",
"@types/electron": "^1.6.10",
"@types/mousetrap": "^1.6.11",
"@vitest/coverage-c8": "^0.27.1",
"axios": "^1.3.2",
"bcryptjs": "^2.4.3",
"buffer": "^6.0.3",
"custom-electron-titlebar": "4.2.0-beta.0",
"dayjs": "^1.11.7",
"electron-context-menu": "^3.6.1",
"electron-log": "^4.4.8",
"electron-serve": "^1.1.0",
"electron-updater": "^5.3.0",
"electron-vite": "^1.0.18",
"electron-window-state": "^5.0.3",
"fuse.js": "^6.6.2",
"js-yaml": "^4.1.0",
"lodash": "^4.17.21",
"lorem-ipsum": "^2.0.8",
"mixpanel-browser": "^2.45.0",
"mkdirp": "^2.1.3",
"moment": "^2.29.4",
"mousetrap": "^1.6.5",
"pushy-electron": "^1.0.11",
"renderer": "link:@types/electron/renderer",
"semver": "^7.3.8",
"svelte-infinite-scroll": "^2.0.1",
"svelte-markdown": "^0.2.3",
"svelte-watch-resize": "^1.0.3",
"sveltekit-i18n": "^2.2.2",
"upath": "^2.0.1",
"vite-plugin-static-copy": "^0.13.1",
"yaml": "^2.2.1"
},
"pnpm": {
"onlyBuiltDependencies": [
"@tea/ui"
]
},
"homepage": "https://tea.xyz",
"repository": "https://github.com/teaxyz/gui.git"
}

View file

@ -1,10 +1,10 @@
import type { PlaywrightTestConfig } from "@playwright/test";
const config: PlaywrightTestConfig = {
webServer: {
command: "npm run build && npm run preview",
port: 4173
}
webServer: {
command: "npm run build && npm run preview",
port: 4173
}
};
export default config;

View file

@ -1,12 +1,12 @@
const { theme, plugins } = require("@tea/ui/tailwind.config.cjs");
module.exports = {
plugins: {
tailwindcss: {
content: ["./src/**/*.{html,svelte,ts,js}", "../ui/src/**/*.{html,svelte,ts,js}"],
theme,
plugins: [...plugins]
},
autoprefixer: {}
}
plugins: {
tailwindcss: {
content: ["./src/**/*.{html,svelte,ts,js}", "../ui/src/**/*.{html,svelte,ts,js}"],
theme,
plugins: [...plugins]
},
autoprefixer: {}
}
};

View file

@ -10,52 +10,52 @@ const hash = "cf849610ca66250f0954379ct4t";
const client = new otaClient.default(hash);
async function main() {
const configPath = path.join(__dirname, "../electron/config.json");
const config = {
PUSHY_APP_ID: process.env.PUSHY_APP_ID || ""
};
await fs.writeFileSync(configPath, JSON.stringify(config, null, 2), "utf-8");
const configPath = path.join(__dirname, "../electron/config.json");
const config = {
PUSHY_APP_ID: process.env.PUSHY_APP_ID || ""
};
await fs.writeFileSync(configPath, JSON.stringify(config, null, 2), "utf-8");
console.log("getting latest translation!");
if (!process.env.SYNC_I18N) return;
console.log("getting latest translation!");
if (!process.env.SYNC_I18N) return;
const [languagesList, translationsRaw] = await Promise.all([
client.getLanguageObjects(),
client.getStrings()
]);
const [languagesList, translationsRaw] = await Promise.all([
client.getLanguageObjects(),
client.getStrings()
]);
const lang = languagesList.reduce((map, lang) => {
map[lang.id] = lang.name;
return map;
}, {});
const lang = languagesList.reduce((map, lang) => {
map[lang.id] = lang.name;
return map;
}, {});
const translations = languagesList.reduce((map, langRaw) => {
map[langRaw.id] = {
lang
};
const translation = translationsRaw[langRaw.id];
for (const k in translation) {
const key = [langRaw.id, k].join(".");
_.set(map, key, translation[k]);
}
return map;
}, {});
const translations = languagesList.reduce((map, langRaw) => {
map[langRaw.id] = {
lang
};
const translation = translationsRaw[langRaw.id];
for (const k in translation) {
const key = [langRaw.id, k].join(".");
_.set(map, key, translation[k]);
}
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;
await fs.writeFileSync(
translationsPath,
JSON.stringify(
{
...translations,
...defaultEnTranslation
},
null,
2
),
"utf-8"
);
defaultEnTranslation.en.lang = translations.en.lang;
await fs.writeFileSync(
translationsPath,
JSON.stringify(
{
...translations,
...defaultEnTranslation
},
null,
2
),
"utf-8"
);
}
main();

View file

@ -7,8 +7,8 @@ const token = process.env.CROWDIN_API_TOKEN;
const projectId = 570715;
const fileId = 7;
const headers = {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json"
Authorization: `Bearer ${token}`,
"Content-Type": "application/json"
};
const englishRaw = translations["en"];
@ -16,85 +16,85 @@ const englishRaw = translations["en"];
delete englishRaw.lang;
function flattenObject(o, prefix = "", result = {}, keepNull = true) {
if (_.isString(o) || _.isNumber(o) || _.isBoolean(o) || (keepNull && _.isNull(o))) {
result[prefix] = o;
return result;
}
if (_.isString(o) || _.isNumber(o) || _.isBoolean(o) || (keepNull && _.isNull(o))) {
result[prefix] = o;
return result;
}
if (_.isArray(o) || _.isPlainObject(o)) {
for (let i in o) {
let pref = prefix;
if (_.isArray(o)) {
pref = pref + `[${i}]`;
} else {
if (_.isEmpty(prefix)) {
pref = i;
} else {
pref = prefix + "." + i;
}
}
flattenObject(o[i], pref, result, keepNull);
}
return result;
}
return result;
if (_.isArray(o) || _.isPlainObject(o)) {
for (let i in o) {
let pref = prefix;
if (_.isArray(o)) {
pref = pref + `[${i}]`;
} else {
if (_.isEmpty(prefix)) {
pref = i;
} else {
pref = prefix + "." + i;
}
}
flattenObject(o[i], pref, result, keepNull);
}
return result;
}
return result;
}
const flattenedEnglish = flattenObject(englishRaw);
const getStrings = async () => {
const { data } = await axios({
method: "GET",
url: `https://api.crowdin.com/api/v2/projects/${projectId}/strings?limit=500`,
headers
});
return data.data;
const { data } = await axios({
method: "GET",
url: `https://api.crowdin.com/api/v2/projects/${projectId}/strings?limit=500`,
headers
});
return data.data;
};
async function main() {
const data = await getStrings();
for (const key in flattenedEnglish) {
const found = data.find((data) => data.data.identifier === `"${key}"`);
if (found && found.data.text !== flattenedEnglish[key]) {
console.log("update!", key, found.data.text, flattenedEnglish[key]);
await updateString(found.data.id, flattenedEnglish[key]);
} else if (!found) {
// insert add
await createString(key, flattenedEnglish[key]);
}
}
const data = await getStrings();
for (const key in flattenedEnglish) {
const found = data.find((data) => data.data.identifier === `"${key}"`);
if (found && found.data.text !== flattenedEnglish[key]) {
console.log("update!", key, found.data.text, flattenedEnglish[key]);
await updateString(found.data.id, flattenedEnglish[key]);
} else if (!found) {
// insert add
await createString(key, flattenedEnglish[key]);
}
}
}
async function createString(key, text) {
await axios({
method: "POST",
url: `https://api.crowdin.com/api/v2/projects/${projectId}/strings`,
headers,
data: {
text,
identifier: `"${key}"`,
fileId,
context: ` -> ${key}`,
isHidden: false,
maxLength: 0,
labelIds: []
}
});
await axios({
method: "POST",
url: `https://api.crowdin.com/api/v2/projects/${projectId}/strings`,
headers,
data: {
text,
identifier: `"${key}"`,
fileId,
context: ` -> ${key}`,
isHidden: false,
maxLength: 0,
labelIds: []
}
});
}
async function updateString(stringId, value) {
const d = await axios({
method: "PATCH",
url: `https://api.crowdin.com/api/v2/projects/${projectId}/strings/${stringId}`,
headers,
data: [
{
value,
op: "replace",
path: "/text"
}
]
});
const d = await axios({
method: "PATCH",
url: `https://api.crowdin.com/api/v2/projects/${projectId}/strings/${stringId}`,
headers,
data: [
{
value,
op: "replace",
path: "/text"
}
]
});
}
main();

View file

@ -3,26 +3,26 @@
@tailwind utilities;
@font-face {
font-family: "mona-sans";
src: url("/fonts/mona-sans-bold.woff2");
font-family: "mona-sans";
src: url("/fonts/mona-sans-bold.woff2");
}
@font-face {
font-family: "inter";
src: url("/fonts/inter-regular.woff2");
font-family: "inter";
src: url("/fonts/inter-regular.woff2");
}
html {
background-color: #1a1a1a;
color: #fff;
user-select: none;
cursor: default;
background-color: #1a1a1a;
color: #fff;
user-select: none;
cursor: default;
}
@layer base {
html {
font-family: sono, sans-serif;
}
html {
font-family: sono, sans-serif;
}
}
.text-primary,
@ -33,9 +33,9 @@ h4,
h5,
h6,
.click-copy {
font-family: "mona-sans";
font-family: "mona-sans";
}
.pk-version {
font-family: "inter";
font-family: "inter";
}

View file

@ -2,17 +2,17 @@
// for information about these interfaces
// and what to do when importing types
declare namespace App {
// interface Locals {}
// interface PageData {}
// interface Error {}
// interface Platform {}
// interface Locals {}
// interface PageData {}
// interface Error {}
// interface Platform {}
}
// Declare custom event handlers here to make typscript happy.
declare namespace svelte.JSX {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
interface HTMLAttributes<T> {
onclick_outside?: () => void;
onleave_delay?: () => void;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
interface HTMLAttributes<T> {
onclick_outside?: () => void;
onleave_delay?: () => void;
}
}

View file

@ -1,19 +1,19 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width" />
%sveltekit.head%
<style>
html {
overflow: hidden;
width: 100vw;
height: 100vh;
}
</style>
</head>
<body>
<div>%sveltekit.body%</div>
</body>
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width" />
%sveltekit.head%
<style>
html {
overflow: hidden;
width: 100vw;
height: 100vh;
}
</style>
</head>
<body>
<div>%sveltekit.body%</div>
</body>
</html>

View file

@ -1,6 +1,6 @@
<script lang="ts">
import '$appcss';
import Placeholder from '$components/placeholder/placeholder.svelte';
import "$appcss";
import Placeholder from "$components/placeholder/placeholder.svelte";
</script>
<Placeholder label="Badges" />
<Placeholder label="Badges" />

View file

@ -1,5 +1,5 @@
<script lang="ts">
import '$appcss';
import "$appcss";
</script>
<section class="border-gray h-56 border bg-black" />

View file

@ -1,78 +1,78 @@
<script lang="ts">
import "$appcss";
// import { t } from '$libs/translations';
import { SideMenuOptions } from "$libs/types";
import Preloader from "@tea/ui/Preloader/Preloader.svelte";
import Package from "$components/packages/package.svelte";
import { packagesStore } from "$libs/stores";
import "$appcss";
// import { t } from '$libs/translations';
import { SideMenuOptions } from "$libs/types";
import Preloader from "@tea/ui/Preloader/Preloader.svelte";
import Package from "$components/packages/package.svelte";
import { packagesStore } from "$libs/stores";
const { packageList: allPackages } = packagesStore;
export let packageFilter: SideMenuOptions = SideMenuOptions.discover;
const { packageList: allPackages } = packagesStore;
export let packageFilter: SideMenuOptions = SideMenuOptions.discover;
export let scrollY = 0;
export let scrollY = 0;
const onScroll = (e: Event) => {
const target = e.target as HTMLInputElement;
scrollY = target.scrollTop || 0;
};
const onScroll = (e: Event) => {
const target = e.target as HTMLInputElement;
scrollY = target.scrollTop || 0;
};
$: packages = $allPackages
.filter((p) => p.categories.includes(SideMenuOptions.discover))
.sort((a, b) => {
return a.manual_sorting - b.manual_sorting;
});
console.log("test", packages);
$: packages = $allPackages
.filter((p) => p.categories.includes(SideMenuOptions.discover))
.sort((a, b) => {
return a.manual_sorting - b.manual_sorting;
});
console.log("test", packages);
</script>
<div class="relative h-full w-full">
<ul class="flex flex-col items-stretch" on:scroll={onScroll}>
{#if packages.length > 0}
{#each packages as pkg, idx}
<div class="z-1 p-1">
<Package tab={packageFilter} {pkg} layout={idx % 2 === 0 ? "left" : "right"} />
</div>
{/each}
{:else}
{#each Array(9) as _}
<section class="card p-1 h-{238}">
<div class="border-gray h-full w-full border">
<Preloader />
</div>
</section>
{/each}
{/if}
</ul>
<ul class="flex flex-col items-stretch" on:scroll={onScroll}>
{#if packages.length > 0}
{#each packages as pkg, idx}
<div class="z-1 p-1">
<Package tab={packageFilter} {pkg} layout={idx % 2 === 0 ? "left" : "right"} />
</div>
{/each}
{:else}
{#each Array(9) as _}
<section class="card p-1 h-{238}">
<div class="border-gray h-full w-full border">
<Preloader />
</div>
</section>
{/each}
{/if}
</ul>
</div>
<style>
ul {
margin-top: 0px;
padding-top: 80px;
padding-bottom: 8px;
height: calc(100vh - 49px);
overflow-y: scroll;
overflow-x: hidden;
padding-right: 4px;
}
ul {
margin-top: 0px;
padding-top: 80px;
padding-bottom: 8px;
height: calc(100vh - 49px);
overflow-y: scroll;
overflow-x: hidden;
padding-right: 4px;
}
/* width */
::-webkit-scrollbar {
width: 6px;
}
/* width */
::-webkit-scrollbar {
width: 6px;
}
/* Track */
::-webkit-scrollbar-track {
background: #272626;
}
/* Track */
::-webkit-scrollbar-track {
background: #272626;
}
/* Handle */
::-webkit-scrollbar-thumb {
background: #949494;
border-radius: 4px;
}
/* Handle */
::-webkit-scrollbar-thumb {
background: #949494;
border-radius: 4px;
}
/* Handle on hover */
::-webkit-scrollbar-thumb:hover {
background: white;
}
/* Handle on hover */
::-webkit-scrollbar-thumb:hover {
background: white;
}
</style>

View file

@ -1,25 +1,25 @@
<script lang="ts">
import '$appcss';
import { t } from '$libs/translations';
import type { AirtablePost } from '@tea/ui/types';
import Posts from '@tea/ui/posts/posts.svelte';
import PanelHeader from '@tea/ui/panel-header/panel-header.svelte';
import Preloader from '@tea/ui/Preloader/Preloader.svelte';
import { postsStore } from '$libs/stores';
import "$appcss";
import { t } from "$libs/translations";
import type { AirtablePost } from "@tea/ui/types";
import Posts from "@tea/ui/posts/posts.svelte";
import PanelHeader from "@tea/ui/panel-header/panel-header.svelte";
import Preloader from "@tea/ui/Preloader/Preloader.svelte";
import { postsStore } from "$libs/stores";
export let title = 'Workshops';
export let ctaLabel = 'View all';
export let title = "Workshops";
export let ctaLabel = "View all";
let courses: AirtablePost[] = [];
let courses: AirtablePost[] = [];
postsStore.subscribeByTag('course', (posts) => (courses = posts));
postsStore.subscribeByTag("course", (posts) => (courses = posts));
</script>
<PanelHeader {title} {ctaLabel} ctaLink="/" />
{#if courses.length}
<Posts posts={courses} linkTarget="_blank" />
<Posts posts={courses} linkTarget="_blank" />
{:else}
<section class="border-gray h-64 border bg-black p-4">
<Preloader />
</section>
<section class="border-gray h-64 border bg-black p-4">
<Preloader />
</section>
{/if}

View file

@ -1,32 +1,32 @@
<script lang="ts">
import '$appcss';
import { t } from '$libs/translations';
import { postsStore } from '$libs/stores';
import type { Course } from '$libs/types';
import "$appcss";
import { t } from "$libs/translations";
import { postsStore } from "$libs/stores";
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) => {
courses = posts.map((post) => {
return {
title: post.title,
sub_title: post.sub_title,
banner_image_url: post.thumb_image_url,
link: post.link
} as Course;
});
});
postsStore.subscribeByTag("featured_course", (posts) => {
courses = posts.map((post) => {
return {
title: post.title,
sub_title: post.sub_title,
banner_image_url: post.thumb_image_url,
link: post.link
} as Course;
});
});
</script>
<Gallery
title={$t("documentation.featured-courses-title").toUpperCase()}
items={courses.map((course) => ({
title: course.title,
subTitle: course.sub_title,
imageUrl: course.banner_image_url,
link: course.link
}))}
linkTarget="_blank"
title={$t("documentation.featured-courses-title").toUpperCase()}
items={courses.map((course) => ({
title: course.title,
subTitle: course.sub_title,
imageUrl: course.banner_image_url,
link: course.link
}))}
linkTarget="_blank"
/>

View file

@ -1,33 +1,33 @@
<script lang="ts">
import '$appcss';
import { onMount } from 'svelte';
import type { Package } from '@tea/ui/types';
import "$appcss";
import { onMount } from "svelte";
import type { Package } from "@tea/ui/types";
import Gallery from '@tea/ui/gallery/gallery.svelte';
import {
featuredPackages as featuredPackagesStore,
initializeFeaturedPackages
} from '$libs/stores';
import Gallery from "@tea/ui/gallery/gallery.svelte";
import {
featuredPackages as featuredPackagesStore,
initializeFeaturedPackages
} from "$libs/stores";
let featuredPackages: Package[] = [];
let featuredPackages: Package[] = [];
featuredPackagesStore.subscribe((v) => {
featuredPackages = v;
});
featuredPackagesStore.subscribe((v) => {
featuredPackages = v;
});
onMount(() => {
if (!featuredPackages.length) {
initializeFeaturedPackages();
}
});
onMount(() => {
if (!featuredPackages.length) {
initializeFeaturedPackages();
}
});
</script>
<Gallery
title="FEATURED PACKAGES"
items={featuredPackages.map((pkg) => ({
title: pkg.full_name,
subTitle: pkg.maintainer || '',
imageUrl: pkg.thumb_image_url,
link: `/packages/${pkg.slug}`
}))}
title="FEATURED PACKAGES"
items={featuredPackages.map((pkg) => ({
title: pkg.full_name,
subTitle: pkg.maintainer || "",
imageUrl: pkg.thumb_image_url,
link: `/packages/${pkg.slug}`
}))}
/>

View file

@ -1,75 +1,75 @@
<script lang="ts">
import { t } from '$libs/translations';
import Button from '@tea/ui/button/button.svelte';
import * as pub from '$env/static/public';
import { t } from "$libs/translations";
import Button from "@tea/ui/button/button.svelte";
import * as pub from "$env/static/public";
</script>
<footer class="relative h-auto w-full bg-black">
<section class="p-4 px-16 py-16">
<h3 class="text-primary mb-5 text-2xl">{$t("footer.quick-links-title").toUpperCase()}</h3>
<menu class="flex gap-4">
<div class="border-gray flex-grow border border-l-0 border-r-0">
<a href="/">
<Button>
<div class="text-primary flex justify-between hover:text-black">
<div class="uppercase">{$t("footer.about-tea-store").toUpperCase()}</div>
<div>&#8594</div>
</div>
</Button>
</a>
</div>
<div class="border-gray flex-grow border border-l-0 border-r-0">
<a href="/">
<Button>
<div class="text-primary flex justify-between hover:text-black">
<div class="uppercase">{$t("footer.report-a-problem").toUpperCase()}</div>
<div>&#8594</div>
</div>
</Button>
</a>
</div>
<div class="border-gray flex-grow border border-l-0 border-r-0">
<a href="https://tea.xyz" target="_blank" rel="noreferrer">
<Button>
<div class="text-primary flex justify-between hover:text-black">
<div class="uppercase">{$t("footer.visit-website").toUpperCase()}</div>
<div>&#8594</div>
</div>
</Button>
</a>
</div>
</menu>
</section>
<section class="p-4 px-16 py-16">
<h3 class="text-primary mb-5 text-2xl">{$t("footer.quick-links-title").toUpperCase()}</h3>
<menu class="flex gap-4">
<div class="border-gray flex-grow border border-l-0 border-r-0">
<a href="/">
<Button>
<div class="text-primary flex justify-between hover:text-black">
<div class="uppercase">{$t("footer.about-tea-store").toUpperCase()}</div>
<div>&#8594</div>
</div>
</Button>
</a>
</div>
<div class="border-gray flex-grow border border-l-0 border-r-0">
<a href="/">
<Button>
<div class="text-primary flex justify-between hover:text-black">
<div class="uppercase">{$t("footer.report-a-problem").toUpperCase()}</div>
<div>&#8594</div>
</div>
</Button>
</a>
</div>
<div class="border-gray flex-grow border border-l-0 border-r-0">
<a href="https://tea.xyz" target="_blank" rel="noreferrer">
<Button>
<div class="text-primary flex justify-between hover:text-black">
<div class="uppercase">{$t("footer.visit-website").toUpperCase()}</div>
<div>&#8594</div>
</div>
</Button>
</a>
</div>
</menu>
</section>
<section class="border-gray h-16 border border-r-0 p-4 px-16 flex justify-between">
<div class="text-gray flex gap-4 text-xs">
<a
href="https://tea.xyz/terms-of-use/"
target="_blank"
rel="noreferrer"
class="hover:text-white"
>
{$t("footer.terms-services").toUpperCase()}
</a>
<a
href="https://tea.xyz/privacy-policy/"
target="_blank"
rel="noreferrer"
class="hover:text-white"
>
{$t("footer.privacy-policy").toUpperCase()}
</a>
</div>
{#if pub.PUBLIC_VERSION}
<div class="text-gray flex gap-4 text-xs">
<span>v{pub.PUBLIC_VERSION}</span>
</div>
{/if}
</section>
<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">
<a
href="https://tea.xyz/terms-of-use/"
target="_blank"
rel="noreferrer"
class="hover:text-white"
>
{$t("footer.terms-services").toUpperCase()}
</a>
<a
href="https://tea.xyz/privacy-policy/"
target="_blank"
rel="noreferrer"
class="hover:text-white"
>
{$t("footer.privacy-policy").toUpperCase()}
</a>
</div>
{#if pub.PUBLIC_VERSION}
<div class="text-gray flex gap-4 text-xs">
<span>v{pub.PUBLIC_VERSION}</span>
</div>
{/if}
</section>
</footer>
<style>
h3 {
color: #00ffd0;
}
h3 {
color: #00ffd0;
}
</style>

View file

@ -1,45 +1,45 @@
<script lang="ts">
import '$appcss';
import ArticleCard from '@tea/ui/article-card/article-card.svelte';
import "$appcss";
import ArticleCard from "@tea/ui/article-card/article-card.svelte";
const doStuff = () => {
console.log('do stuff!');
};
const doStuff = () => {
console.log("do stuff!");
};
</script>
<header class="border-gray text-primary border bg-black p-4">GETTING STARTED WITH TEA</header>
<section class="grid grid-cols-3 bg-black">
<div class="border-gray border p-4">
<ArticleCard
content={{
title: 'installing tea',
copy: "It's time to take your first sip! Click below to visit our tea-cli documentation page.",
img_url: '/images/bored-ape.png',
cta_label: 'Get Started',
link: '/cli'
}}
/>
</div>
<div class="border-gray border p-4">
<ArticleCard
content={{
title: 'authenticating',
copy: 'Using tea without authenticating is like playing a video game without the DLC. Join us today!',
img_url: '/images/bored-ape.png',
cta_label: 'Get Started',
link: ''
}}
onClick={doStuff}
/>
</div>
<div class="border-gray border p-4">
<ArticleCard
content={{
title: 'give us a star',
copy: 'Revolutions are built on the will of the people. Show your support for a more equitable internet.',
img_url: '/images/bored-ape.png',
cta_label: 'Get Started'
}}
/>
</div>
<div class="border-gray border p-4">
<ArticleCard
content={{
title: "installing tea",
copy: "It's time to take your first sip! Click below to visit our tea-cli documentation page.",
img_url: "/images/bored-ape.png",
cta_label: "Get Started",
link: "/cli"
}}
/>
</div>
<div class="border-gray border p-4">
<ArticleCard
content={{
title: "authenticating",
copy: "Using tea without authenticating is like playing a video game without the DLC. Join us today!",
img_url: "/images/bored-ape.png",
cta_label: "Get Started",
link: ""
}}
onClick={doStuff}
/>
</div>
<div class="border-gray border p-4">
<ArticleCard
content={{
title: "give us a star",
copy: "Revolutions are built on the will of the people. Show your support for a more equitable internet.",
img_url: "/images/bored-ape.png",
cta_label: "Get Started"
}}
/>
</div>
</section>

View file

@ -1,22 +1,22 @@
<script lang="ts">
import '$appcss';
import { t } from '$libs/translations';
import { postsStore } from '$libs/stores';
import type { AirtablePost } from '@tea/ui/types';
import Posts from '@tea/ui/posts/posts.svelte';
import PanelHeader from '@tea/ui/panel-header/panel-header.svelte';
import Preloader from '@tea/ui/Preloader/Preloader.svelte';
import "$appcss";
import { t } from "$libs/translations";
import { postsStore } from "$libs/stores";
import type { AirtablePost } from "@tea/ui/types";
import Posts from "@tea/ui/posts/posts.svelte";
import PanelHeader from "@tea/ui/panel-header/panel-header.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>
<PanelHeader title="OPEN-SOURCE NEWS" ctaLabel="Read more articles" ctaLink="/" />
{#if news.length}
<Posts posts={news} linkTarget="_blank" />
<Posts posts={news} linkTarget="_blank" />
{:else}
<section class="border-gray h-64 border bg-black p-4">
<Preloader />
</section>
<section class="border-gray h-64 border bg-black p-4">
<Preloader />
</section>
{/if}

View file

@ -1,21 +1,23 @@
<script lang="ts">
import '$appcss';
import { t } from "$libs/translations";
import { notificationStore } from '$libs/stores';
import Notification from "@tea/ui/notification/notification.svelte";
import "$appcss";
import { t } from "$libs/translations";
import { notificationStore } from "$libs/stores";
import Notification from "@tea/ui/notification/notification.svelte";
</script>
<div class="w-full flex flex-col gap-1">
<div class="flex w-full flex-col gap-1">
{#each $notificationStore as notification}
<Notification
notification={{
...notification,
// 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);
}}
/>
{/each}
</div>
</div>

View file

@ -1,175 +1,174 @@
<script lang="ts">
import "$appcss";
import "@tea/ui/icons/icons.css";
import { t } from "$libs/translations";
import Button from "@tea/ui/button/button.svelte";
import ButtonIcon from "@tea/ui/button-icon/button-icon.svelte";
import ToolTip from "@tea/ui/tool-tip/tool-tip.svelte";
import semverCompare from "semver/functions/compare";
import ProgressCircle from "@tea/ui/progress-circle/progress-circle.svelte";
import "$appcss";
import "@tea/ui/icons/icons.css";
import { t } from "$libs/translations";
import Button from "@tea/ui/button/button.svelte";
import ButtonIcon from "@tea/ui/button-icon/button-icon.svelte";
import ToolTip from "@tea/ui/tool-tip/tool-tip.svelte";
import semverCompare from "semver/functions/compare";
import ProgressCircle from "@tea/ui/progress-circle/progress-circle.svelte";
import type { GUIPackage } from "$libs/types";
import { packagesStore } from "$libs/stores";
import { openPackageEntrypointInTerminal, shellOpenExternal } from "@native";
import { findAvailableVersions, findRecentInstalledVersion } from "$libs/packages/pkg-utils";
import { trimGithubSlug } from "$libs/github";
import PackageImage from "../package-card/bg-image.svelte";
import PackageVersionSelector from "$components/package-install-button/package-version-selector.svelte";
import type { GUIPackage } from "$libs/types";
import { packagesStore } from "$libs/stores";
import { openPackageEntrypointInTerminal, shellOpenExternal } from "@native";
import { findAvailableVersions, findRecentInstalledVersion } from "$libs/packages/pkg-utils";
import { trimGithubSlug } from "$libs/github";
import PackageImage from "../package-card/bg-image.svelte";
import PackageVersionSelector from "$components/package-install-button/package-version-selector.svelte";
import { isPackageInstalled } from "$libs/native-mock";
export let pkg: GUIPackage;
let installing = false;
let pruning = false;
export let pkg: GUIPackage;
let installing = false;
let pruning = false;
const install = async (version: string) => {
installing = true;
await packagesStore.installPkg(pkg, version);
installing = false;
};
const install = async (version: string) => {
installing = true;
await packagesStore.installPkg(pkg, version);
installing = false;
};
const prune = async () => {
pruning = true;
const versions = (pkg?.installed_versions || []).sort((a, b) => semverCompare(b, a));
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 () => {
pruning = true;
const versions = (pkg?.installed_versions || []).sort((a, b) => semverCompare(b, a));
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;
};
let copied = false;
const copyPackagePantryLink = async () => {
const pantryLink = `https://tea.xyz/+${pkg.full_name}`.toLowerCase();
await navigator.clipboard.writeText(pantryLink);
copied = true;
};
let copied = false;
const copyPackagePantryLink = async () => {
const pantryLink = `https://tea.xyz/+${pkg.full_name}`.toLowerCase();
await navigator.clipboard.writeText(pantryLink);
copied = true;
};
</script>
<section class="mt-4 bg-black">
<header class="flex">
<figure class="grow-1 relative w-1/3">
<PackageImage class="min-h-[300px] w-full overflow-hidden" {pkg} layout="none" />
{#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 right-0 top-1/2 m-auto -mt-12 h-24 w-24">
<ProgressCircle value={pkg.install_progress_percentage} />
</div>
</div>
{/if}
</figure>
<article class="w-2/3 p-4 pt-8">
<div class="align-center flex items-center gap-2">
<h3 class="text-primary text-3xl">{pkg.full_name}</h3>
<ButtonIcon
icon="pencil"
helpText="edit package"
on:click={() =>
shellOpenExternal(
`https://github.com/teaxyz/pantry/blob/main/projects/${pkg.full_name}/package.yml`
)}
/>
<ButtonIcon icon="link" helpText="share package" on:click={copyPackagePantryLink} />
{#if copied}
<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
>
<header class="flex">
<figure class="grow-1 relative w-1/3">
<PackageImage class="min-h-[300px] w-full overflow-hidden" {pkg} layout="none" />
{#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 right-0 top-1/2 m-auto -mt-12 h-24 w-24">
<ProgressCircle value={pkg.install_progress_percentage} />
</div>
</div>
{/if}
</figure>
<article class="w-2/3 p-4 pt-8">
<div class="align-center flex items-center gap-2">
<h3 class="text-primary text-3xl">{pkg.full_name}</h3>
<ButtonIcon
icon="pencil"
helpText="edit package"
on:click={() =>
shellOpenExternal(
`https://github.com/teaxyz/pantry/blob/main/projects/${pkg.full_name}/package.yml`
)}
/>
<ButtonIcon icon="link" helpText="share package" on:click={copyPackagePantryLink} />
{#if copied}
<p class="text-green">copied!</p>
{/if}
</menu>
</article>
</header>
</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] 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>

View file

@ -1,172 +1,172 @@
<script lang="ts">
import type { GUIPackage } from "$libs/types";
import { onMount } from "svelte";
import { packagesStore } from "$libs/stores";
import type { GUIPackage } from "$libs/types";
import { onMount } from "svelte";
import { packagesStore } from "$libs/stores";
let clazz = "";
export { clazz as class };
let clazz = "";
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";
$: loadedImg = "";
let loaded = false;
let lastProcessedPkg: GUIPackage | null = null;
const defaultImgUrl = "/images/default-thumb.jpg";
$: loadedImg = "";
let loaded = false;
let lastProcessedPkg: GUIPackage | null = null;
const loadImage = async (url: string): Promise<string> => {
const image = new Image();
image.src = url;
return new Promise((resolve, reject) => {
image.onload = () => {
loadedImg = url;
setTimeout(() => {
loaded = true;
}, 300);
resolve(url);
};
image.onerror = () => {
reject(new Error(`file/url does not exist ${url}`));
};
});
};
const loadImage = async (url: string): Promise<string> => {
const image = new Image();
image.src = url;
return new Promise((resolve, reject) => {
image.onload = () => {
loadedImg = url;
setTimeout(() => {
loaded = true;
}, 300);
resolve(url);
};
image.onerror = () => {
reject(new Error(`file/url does not exist ${url}`));
};
});
};
const recachePkg = async () => {
const url = await packagesStore.cachePkgImage(pkg);
loadImage(url);
};
const recachePkg = async () => {
const url = await packagesStore.cachePkgImage(pkg);
loadImage(url);
};
const getCache = async () => {
if (pkg.cached_image_url) {
loadImage(pkg.cached_image_url).catch(() => {
if (pkg.thumb_image_url) {
loadImage(pkg.thumb_image_url);
recachePkg();
}
});
} else if (pkg.thumb_image_url) {
recachePkg();
}
}
const getCache = async () => {
if (pkg.cached_image_url) {
loadImage(pkg.cached_image_url).catch(() => {
if (pkg.thumb_image_url) {
loadImage(pkg.thumb_image_url);
recachePkg();
}
});
} else if (pkg.thumb_image_url) {
recachePkg();
}
};
$: {
if (pkg && pkg?.slug !== lastProcessedPkg?.slug) {
loaded = false;
loadedImg = "";
lastProcessedPkg = pkg;
getCache();
}
}
$: {
if (pkg && pkg?.slug !== lastProcessedPkg?.slug) {
loaded = false;
loadedImg = "";
lastProcessedPkg = pkg;
getCache();
}
}
</script>
<section class="bg-black {clazz} {layout}">
<i class="logo icon-tea-logo-iconasset-1 text-gray animate-pulse text-3xl {layout}" />
<div
class="bg-center opacity-0 transition-all duration-500"
class:opacity-100={loaded}
style="background-image: url({loadedImg})"
>
<!-- dup image: save processing power instead of computing the blur across all the HTML layers -->
{#if layout !== "none"}
<aside
class="blur-sm {layout} opacity-0 transition-all duration-500"
class:opacity-100={loaded}
>
<figure class="bg-center" style="background-image: url({loadedImg})" />
</aside>
{/if}
</div>
<i class="logo icon-tea-logo-iconasset-1 text-gray animate-pulse text-3xl {layout}" />
<div
class="bg-center opacity-0 transition-all duration-500"
class:opacity-100={loaded}
style="background-image: url({loadedImg})"
>
<!-- dup image: save processing power instead of computing the blur across all the HTML layers -->
{#if layout !== "none"}
<aside
class="blur-sm {layout} opacity-0 transition-all duration-500"
class:opacity-100={loaded}
>
<figure class="bg-center" style="background-image: url({loadedImg})" />
</aside>
{/if}
</div>
</section>
<style>
section {
width: 100%;
height: 100%;
}
section {
width: 100%;
height: 100%;
}
.logo {
position: absolute;
width: 30px;
height: 30px;
margin-left: -15px;
}
.logo.none {
left: 50%;
top: 50%;
}
.logo.bottom {
left: 50%;
top: 30%;
}
.logo.right {
left: 22%;
top: 50%;
margin-top: -15px;
}
.logo.left {
left: 70%;
top: 50%;
margin-top: -15px;
}
.logo {
position: absolute;
width: 30px;
height: 30px;
margin-left: -15px;
}
.logo.none {
left: 50%;
top: 50%;
}
.logo.bottom {
left: 50%;
top: 30%;
}
.logo.right {
left: 22%;
top: 50%;
margin-top: -15px;
}
.logo.left {
left: 70%;
top: 50%;
margin-top: -15px;
}
div {
position: absolute;
left: 0px;
bottom: 0px;
width: 100%;
height: 100%;
background-size: cover;
box-sizing: border-box;
background-repeat: no-repeat;
}
aside {
position: absolute;
bottom: 0px;
width: 100%;
overflow: hidden;
}
aside.bottom {
left: 0px;
height: 50%;
}
div {
position: absolute;
left: 0px;
bottom: 0px;
width: 100%;
height: 100%;
background-size: cover;
box-sizing: border-box;
background-repeat: no-repeat;
}
aside {
position: absolute;
bottom: 0px;
width: 100%;
overflow: hidden;
}
aside.bottom {
left: 0px;
height: 50%;
}
aside.left {
left: 0px;
height: 100%;
width: 60%;
}
aside.left {
left: 0px;
height: 100%;
width: 60%;
}
aside.right {
height: 100%;
right: 0px;
width: 60%;
}
aside.right {
height: 100%;
right: 0px;
width: 60%;
}
figure {
position: absolute;
bottom: 0px;
width: 100%;
height: 338px;
background-size: cover;
background-repeat: no-repeat;
}
figure {
position: absolute;
bottom: 0px;
width: 100%;
height: 338px;
background-size: cover;
background-repeat: no-repeat;
}
aside.bottom figure {
left: 0px;
}
aside.bottom figure {
left: 0px;
}
aside.right figure {
height: 100%;
/* the overlay is 60% of the image, so we need to oversize the background image back to 100% */
width: 166.6666666%;
right: 0px;
}
aside.right figure {
height: 100%;
/* the overlay is 60% of the image, so we need to oversize the background image back to 100% */
width: 166.6666666%;
right: 0px;
}
aside.left figure {
height: 100%;
/* the overlay is 60% of the image, so we need to oversize the background image back to 100% */
width: 166.66666666%;
left: 0px;
}
aside.left figure {
height: 100%;
/* the overlay is 60% of the image, so we need to oversize the background image back to 100% */
width: 166.66666666%;
left: 0px;
}
</style>

View file

@ -1,219 +1,219 @@
<script lang="ts">
import "../../app.css";
import ProgressCircle from "@tea/ui/progress-circle/progress-circle.svelte";
import { PackageStates, type GUIPackage } from "$libs/types";
import { findRecentInstalledVersion } from "$libs/packages/pkg-utils";
import BgImage from "./bg-image.svelte";
import PackageInstallButton from "$components/package-install-button/package-install-button.svelte";
import PackageInstalledBadge from "$components/package-install-button/package-installed-badge.svelte";
import "../../app.css";
import ProgressCircle from "@tea/ui/progress-circle/progress-circle.svelte";
import { PackageStates, type GUIPackage } from "$libs/types";
import { findRecentInstalledVersion } from "$libs/packages/pkg-utils";
import BgImage from "./bg-image.svelte";
import PackageInstallButton from "$components/package-install-button/package-install-button.svelte";
import PackageInstalledBadge from "$components/package-install-button/package-installed-badge.svelte";
export let pkg: GUIPackage;
export let link: string;
export let progessLoading = 0;
export let pkg: GUIPackage;
export let link: string;
export let progessLoading = 0;
export let layout: "bottom" | "right" | "left" = "bottom";
export let layout: "bottom" | "right" | "left" = "bottom";
export let onClickCTA = async () => {
console.log("do nothing");
};
export let onClickCTA = async () => {
console.log("do nothing");
};
const fixPackageName = (title: string) => {
return title.replace("-", "\u2011");
};
const fixPackageName = (title: string) => {
return title.replace("-", "\u2011");
};
// Using this instead of css :active because there is a button inside of a button
let isActive = false;
const activate = () => (isActive = true);
const deactivate = () => (isActive = false);
// Using this instead of css :active because there is a button inside of a button
let isActive = false;
const activate = () => (isActive = true);
const deactivate = () => (isActive = false);
const preventPropagation = (evt: MouseEvent) => evt.stopPropagation();
const preventPropagation = (evt: MouseEvent) => evt.stopPropagation();
</script>
<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}>
<div class="package-card-content absolute h-full w-full flex-col justify-between">
<div class="hint-container">
<div class="hint">
<div class="line-clamp-1 text-xs">view more details</div>
<div class="hint-icon"><i class="icon-upward-arrow" /></div>
</div>
</div>
<div class="content-container absolute bottom-0 w-full {layout}">
<article class="card-thumb-label relative">
{#if layout === "bottom"}
<h3 class="text-bold font-mona line-clamp-1 text-2xl font-bold text-white">
{fixPackageName(pkg.name)}
</h3>
<p class="line-clamp-2 h-[32px] text-xs font-thin lowercase">{pkg.desc ?? ""}</p>
{:else}
<h3 class="text-bold font-mona line-clamp-1 mb-4 text-3xl font-bold text-white">
{fixPackageName(pkg.name)}
</h3>
<p class="line-clamp-10 h-[160px] text-xs font-thin lowercase">{pkg.desc ?? ""}</p>
{/if}
</article>
<div class="relative mt-3.5 w-full">
<div class="install-button {layout}" on:mousedown={preventPropagation}>
{#if pkg.state === PackageStates.INSTALLED}
<PackageInstalledBadge version={pkg.version} />
{:else}
<PackageInstallButton
{pkg}
onClick={(evt) => {
// prevent default to prevent the link that this button is inside of from being followed
evt?.preventDefault();
onClickCTA();
}}
/>
{/if}
</div>
</div>
<div class="relative mt-1.5 h-[10px] leading-[10px]">
{#if pkg.state === "NEEDS_UPDATE"}
<span class="text-[10px]">
<span class="opacity-70">you have</span>
v{findRecentInstalledVersion(pkg)}
</span>
{/if}
</div>
</div>
</div>
</a>
<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="hint-container">
<div class="hint">
<div class="line-clamp-1 text-xs">view more details</div>
<div class="hint-icon"><i class="icon-upward-arrow" /></div>
</div>
</div>
<div class="content-container absolute bottom-0 w-full {layout}">
<article class="card-thumb-label relative">
{#if layout === "bottom"}
<h3 class="text-bold font-mona line-clamp-1 text-2xl font-bold text-white">
{fixPackageName(pkg.name)}
</h3>
<p class="line-clamp-2 h-[32px] text-xs font-thin lowercase">{pkg.desc ?? ""}</p>
{:else}
<h3 class="text-bold font-mona line-clamp-1 mb-4 text-3xl font-bold text-white">
{fixPackageName(pkg.name)}
</h3>
<p class="line-clamp-10 h-[160px] text-xs font-thin lowercase">{pkg.desc ?? ""}</p>
{/if}
</article>
<div class="relative mt-3.5 w-full">
<div class="install-button {layout}" on:mousedown={preventPropagation}>
{#if pkg.state === PackageStates.INSTALLED}
<PackageInstalledBadge version={pkg.version} />
{:else}
<PackageInstallButton
{pkg}
onClick={(evt) => {
// prevent default to prevent the link that this button is inside of from being followed
evt?.preventDefault();
onClickCTA();
}}
/>
{/if}
</div>
</div>
<div class="relative mt-1.5 h-[10px] leading-[10px]">
{#if pkg.state === "NEEDS_UPDATE"}
<span class="text-[10px]">
<span class="opacity-70">you have</span>
v{findRecentInstalledVersion(pkg)}
</span>
{/if}
</div>
</div>
</div>
</a>
{#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 right-0 top-1/2 m-auto -mt-12 h-24 w-24">
<ProgressCircle value={progessLoading} />
</div>
</div>
{/if}
{#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 right-0 top-1/2 m-auto -mt-12 h-24 w-24">
<ProgressCircle value={progessLoading} />
</div>
</div>
{/if}
</section>
<style>
section {
transition: all 0.3s;
width: 100%;
height: 340px;
background-size: cover;
box-sizing: border-box;
}
section {
transition: all 0.3s;
width: 100%;
height: 340px;
background-size: cover;
box-sizing: border-box;
}
section.active::before {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(26, 26, 26, 0.7);
z-index: 2;
content: "";
pointer-events: none;
}
section.active::before {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(26, 26, 26, 0.7);
z-index: 2;
content: "";
pointer-events: none;
}
section.package-card:active {
border-color: #8000ff;
box-shadow: 0px 0px 0px 2px rgba(128, 0, 255, 0.5);
}
section.package-card:active {
border-color: #8000ff;
box-shadow: 0px 0px 0px 2px rgba(128, 0, 255, 0.5);
}
.content-container {
height: 50%;
background: linear-gradient(180deg, rgba(26, 26, 26, 0.3) 0%, rgba(26, 26, 26, 0.75) 72.92%);
display: flex;
flex-direction: column;
padding: 28px 14px;
justify-content: center;
}
.content-container {
height: 50%;
background: linear-gradient(180deg, rgba(26, 26, 26, 0.3) 0%, rgba(26, 26, 26, 0.75) 72.92%);
display: flex;
flex-direction: column;
padding: 28px 14px;
justify-content: center;
}
.content-container.bottom {
left: 0px;
}
.content-container.bottom {
left: 0px;
}
.content-container.left {
height: 100%;
width: 60%;
left: 0px;
padding: 28px 28px;
}
.content-container.left {
height: 100%;
width: 60%;
left: 0px;
padding: 28px 28px;
}
.content-container.right {
height: 100%;
width: 60%;
right: 0px;
padding: 28px 28px;
}
.content-container.right {
height: 100%;
width: 60%;
right: 0px;
padding: 28px 28px;
}
.hint-container {
position: absolute;
top: 0px;
right: 0px;
display: flex;
justify-content: flex-end;
z-index: 1;
}
.hint-container {
position: absolute;
top: 0px;
right: 0px;
display: flex;
justify-content: flex-end;
z-index: 1;
}
.hint {
min-width: 240px;
padding-left: 30%;
height: 24px;
display: flex;
align-items: center;
justify-content: flex-end;
column-gap: 0.5rem;
background: linear-gradient(270deg, #e1e1e1 66.29%, rgba(225, 225, 225, 0) 100%);
color: #1a1a1a;
visibility: hidden;
}
.hint {
min-width: 240px;
padding-left: 30%;
height: 24px;
display: flex;
align-items: center;
justify-content: flex-end;
column-gap: 0.5rem;
background: linear-gradient(270deg, #e1e1e1 66.29%, rgba(225, 225, 225, 0) 100%);
color: #1a1a1a;
visibility: hidden;
}
.hint-icon {
background: #8000ff;
height: 24px;
width: 24px;
display: flex;
justify-content: center;
align-items: center;
color: #e1e1e1;
}
.hint-icon {
background: #8000ff;
height: 24px;
width: 24px;
display: flex;
justify-content: center;
align-items: center;
color: #e1e1e1;
}
.package-card.right {
min-width: 550px;
}
.package-card.right {
min-width: 550px;
}
.package-card.left {
min-width: 550px;
}
.package-card.left {
min-width: 550px;
}
.package-card:hover .hint {
visibility: visible;
}
.package-card:hover .hint {
visibility: visible;
}
.card-thumb-label {
text-align: left;
width: 100%;
}
.card-thumb-label {
text-align: left;
width: 100%;
}
.card-thumb-label p {
color: white;
}
.card-thumb-label p {
color: white;
}
.install-button {
width: 160px;
}
.install-button {
width: 160px;
}
.install-button.bottom {
min-width: 100%;
}
.install-button.bottom {
min-width: 100%;
}
@media screen and (min-width: 650px) {
.install-button.bottom {
min-width: 60%;
}
}
@media screen and (min-width: 650px) {
.install-button.bottom {
min-width: 60%;
}
}
@media screen and (min-width: 1000px) {
.install-button.bottom {
min-width: 50%;
}
}
@media screen and (min-width: 1000px) {
.install-button.bottom {
min-width: 50%;
}
}
</style>

View file

@ -1,8 +1,8 @@
<script lang="ts">
import type { Package } from '@tea/ui/types';
export let pkg: Package;
import type { Package } from "@tea/ui/types";
export let pkg: Package;
</script>
<section class="h-64 w-full">
<h1>{pkg.full_name}</h1>
<h1>{pkg.full_name}</h1>
</section>

View file

@ -1,105 +1,105 @@
<script lang="ts">
import { PackageStates, type GUIPackage } from "$libs/types";
import Button from "@tea/ui/button/button.svelte";
import { t } from "$libs/translations";
import { PackageStates, type GUIPackage } from "$libs/types";
import Button from "@tea/ui/button/button.svelte";
import { t } from "$libs/translations";
export let buttonSize: "small" | "large" = "small";
export let buttonSize: "small" | "large" = "small";
export let pkg: GUIPackage;
export let onClick = (evt?: MouseEvent) => {
console.log("do nothing");
};
export let pkg: GUIPackage;
export let onClick = (evt?: MouseEvent) => {
console.log("do nothing");
};
const getColor = (state: PackageStates): "primary" | "secondary" | "black" => {
if (state === PackageStates.INSTALLED) {
return "black";
}
if (state === PackageStates.AVAILABLE || state === PackageStates.INSTALLING) {
return "secondary";
}
return "primary";
};
const getColor = (state: PackageStates): "primary" | "secondary" | "black" => {
if (state === PackageStates.INSTALLED) {
return "black";
}
if (state === PackageStates.AVAILABLE || state === PackageStates.INSTALLING) {
return "secondary";
}
return "primary";
};
const isActive = (state: PackageStates): boolean => {
return state === PackageStates.INSTALLING || state === PackageStates.UPDATING;
};
const isActive = (state: PackageStates): boolean => {
return state === PackageStates.INSTALLING || state === PackageStates.UPDATING;
};
const badgeClass: Record<PackageStates, string> = {
[PackageStates.AVAILABLE]: "install-badge",
[PackageStates.INSTALLING]: "install-badge",
[PackageStates.NEEDS_UPDATE]: "update-badge",
[PackageStates.UPDATING]: "update-badge",
[PackageStates.INSTALLED]: "installed-badge",
};
const badgeClass: Record<PackageStates, string> = {
[PackageStates.AVAILABLE]: "install-badge",
[PackageStates.INSTALLING]: "install-badge",
[PackageStates.NEEDS_UPDATE]: "update-badge",
[PackageStates.UPDATING]: "update-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>
<Button
class="w-full border p-0 text-xs text-white {buttonSize === 'small' ? 'h-8' : 'h-10'}"
type="plain"
color={getColor(pkg.state)}
active={isActive(pkg.state)}
{onClick}
class="w-full border p-0 text-xs text-white {buttonSize === 'small' ? 'h-8' : 'h-10'}"
type="plain"
color={getColor(pkg.state)}
active={isActive(pkg.state)}
{onClick}
>
<div class="version-button h-full">
<div class="flex h-full flex-col justify-center p-2">
{#if hasVersionSelectorDropdown}
<div class="flex items-center justify-between gap-x-2">
<div class="flex items-center gap-x-2">
<div>{ctaLabel}</div>
<div class="version-label {badgeClass[pkg.state]}">{pkg.version}</div>
</div>
<i class="icon-downward-arrow flex" />
</div>
{:else}
<div class="flex items-center justify-center gap-x-2">
<div>{ctaLabel}</div>
<div class="version-label {badgeClass[pkg.state]}">{pkg.version}</div>
</div>
{/if}
</div>
<!-- This slot holds the drop down menu and it inside of the button so that the
<div class="version-button h-full">
<div class="flex h-full flex-col justify-center p-2">
{#if hasVersionSelectorDropdown}
<div class="flex items-center justify-between gap-x-2">
<div class="flex items-center gap-x-2">
<div>{ctaLabel}</div>
<div class="version-label {badgeClass[pkg.state]}">{pkg.version}</div>
</div>
<i class="icon-downward-arrow flex" />
</div>
{:else}
<div class="flex items-center justify-center gap-x-2">
<div>{ctaLabel}</div>
<div class="version-label {badgeClass[pkg.state]}">{pkg.version}</div>
</div>
{/if}
</div>
<!-- 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-->
<slot name="selector" />
</div>
<slot name="selector" />
</div>
</Button>
<style>
.version-label {
font-size: 10px;
line-height: 12px;
padding: 0 4px;
border-radius: 2px;
}
.version-label {
font-size: 10px;
line-height: 12px;
padding: 0 4px;
border-radius: 2px;
}
.install-badge {
background-color: #dcb8ff;
color: #8000ff;
}
.install-badge {
background-color: #dcb8ff;
color: #8000ff;
}
.update-badge {
background-color: #04957a;
color: #00ffd0;
}
.update-badge {
background-color: #04957a;
color: #00ffd0;
}
.installed-badge {
background-color: white;
color: #1a1a1a;
}
.installed-badge {
background-color: white;
color: #1a1a1a;
}
.version-button:hover .install-badge {
background-color: white;
}
.version-button:hover .install-badge {
background-color: white;
}
.version-button:hover .update-badge {
background-color: #1a1a1a;
}
.version-button:hover .update-badge {
background-color: #1a1a1a;
}
.version-button:hover .installed-badge {
background-color: #1a1a1a;
color: white;
}
.version-button:hover .installed-badge {
background-color: #1a1a1a;
color: white;
}
</style>

View file

@ -1,32 +1,32 @@
<script lang="ts">
export let version = "1.0.0";
export let version = "1.0.0";
</script>
<div class="container relative h-full">
<div class="content flex items-center justify-center gap-2 p-2">
<i class="icon-check-circle-o flex text-sm text-[#00ffd0]" />
<div class="text-xs">INSTALLED</div>
<div class="rounded-sm bg-white px-1 text-[10px] leading-[12px] text-black">
v{version}
</div>
</div>
<div class="content flex items-center justify-center gap-2 p-2">
<i class="icon-check-circle-o flex text-sm text-[#00ffd0]" />
<div class="text-xs">INSTALLED</div>
<div class="rounded-sm bg-white px-1 text-[10px] leading-[12px] text-black">
v{version}
</div>
</div>
</div>
<style>
.content {
position: relative;
z-index: 2;
}
.content {
position: relative;
z-index: 2;
}
.container::before {
content: "";
position: absolute;
mix-blend-mode: overlay;
top: 0;
right: 0;
left: 0;
bottom: 0;
background: #dedede;
z-index: 0;
}
.container::before {
content: "";
position: absolute;
mix-blend-mode: overlay;
top: 0;
right: 0;
left: 0;
bottom: 0;
background: #dedede;
z-index: 0;
}
</style>

View file

@ -1,147 +1,147 @@
<script lang="ts">
import { PackageStates, type GUIPackage } from "$libs/types";
import clickOutside from "@tea/ui/lib/clickOutside";
import PackageStateButton from "./package-install-button.svelte";
import { PackageStates, type GUIPackage } from "$libs/types";
import clickOutside from "@tea/ui/lib/clickOutside";
import PackageStateButton from "./package-install-button.svelte";
export let buttonSize: "small" | "large" = "small";
export let pkg: GUIPackage;
export let availableVersions: string[] = [];
export let buttonSize: "small" | "large" = "small";
export let pkg: GUIPackage;
export let availableVersions: string[] = [];
export let onClick = async (_version: string) => {
console.log("do nothing");
};
export let onClick = async (_version: string) => {
console.log("do nothing");
};
$: isOpened = false;
$: isOpened = false;
const toggleOpen = (evt?: MouseEvent) => {
evt?.preventDefault();
const toggleOpen = (evt?: MouseEvent) => {
evt?.preventDefault();
if ([PackageStates.INSTALLING, PackageStates.UPDATING].includes(pkg.state)) {
return;
}
isOpened = !isOpened;
};
if ([PackageStates.INSTALLING, PackageStates.UPDATING].includes(pkg.state)) {
return;
}
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) => {
if (isInstalled(version)) {
return;
}
const handleClick = (evt: MouseEvent, version: string) => {
if (isInstalled(version)) {
return;
}
isOpened = false;
if (version) {
onClick(version);
}
};
isOpened = false;
if (version) {
onClick(version);
}
};
const handleClickOutside = () => (isOpened = false);
const handleClickOutside = () => (isOpened = false);
</script>
<div class="dropdown z-10" use:clickOutside on:click_outside={handleClickOutside}>
<PackageStateButton {buttonSize} {pkg} onClick={toggleOpen}>
<div slot="selector" class="pt-2">
<div class="version-list" class:visible={isOpened}>
{#each availableVersions as version, idx}
{#if idx !== 0}<hr class="divider" />{/if}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
class="version-item flex items-center justify-start gap-x-1 text-xs"
class:installable-version={!installedVersions.includes(version)}
on:click={(evt) => handleClick(evt, version)}
>
<div class:installed-text={installedVersions.includes(version)}>v{version}</div>
{#if idx === 0}
<div class="latest-version">(latest)</div>
{/if}
{#if installedVersions.includes(version)}
<div class="flex grow justify-end">
<i class="installed-text icon-check-circle flex" />
</div>
{/if}
</div>
{/each}
</div>
</div>
</PackageStateButton>
<PackageStateButton {buttonSize} {pkg} onClick={toggleOpen}>
<div slot="selector" class="pt-2">
<div class="version-list" class:visible={isOpened}>
{#each availableVersions as version, idx}
{#if idx !== 0}<hr class="divider" />{/if}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
class="version-item flex items-center justify-start gap-x-1 text-xs"
class:installable-version={!installedVersions.includes(version)}
on:click={(evt) => handleClick(evt, version)}
>
<div class:installed-text={installedVersions.includes(version)}>v{version}</div>
{#if idx === 0}
<div class="latest-version">(latest)</div>
{/if}
{#if installedVersions.includes(version)}
<div class="flex grow justify-end">
<i class="installed-text icon-check-circle flex" />
</div>
{/if}
</div>
{/each}
</div>
</div>
</PackageStateButton>
</div>
<style>
.version-list {
display: none;
position: absolute;
width: 100%;
color: white;
background-color: #1a1a1a;
border: 0.5px solid #949494;
box-shadow: 0px 0px 8px rgba(0, 0, 0, 0.5);
border-radius: 2px;
max-height: 160px;
overflow-y: auto;
overflow-x: hidden;
}
.version-list {
display: none;
position: absolute;
width: 100%;
color: white;
background-color: #1a1a1a;
border: 0.5px solid #949494;
box-shadow: 0px 0px 8px rgba(0, 0, 0, 0.5);
border-radius: 2px;
max-height: 160px;
overflow-y: auto;
overflow-x: hidden;
}
.version-item {
margin: 4px 6px;
padding: 4px 6px;
height: auto;
width: auto;
white-space: nowrap;
}
.version-item {
margin: 4px 6px;
padding: 4px 6px;
height: auto;
width: auto;
white-space: nowrap;
}
.installable-version {
cursor: pointer;
}
.installable-version {
cursor: pointer;
}
.installable-version:hover {
outline: 1px solid #949494;
background-color: rgba(148, 148, 148, 0.35);
}
.installable-version:hover {
outline: 1px solid #949494;
background-color: rgba(148, 148, 148, 0.35);
}
.dropdown {
position: relative;
display: inline-block;
width: 100%;
}
.dropdown {
position: relative;
display: inline-block;
width: 100%;
}
.divider {
border: 1px solid #272626;
margin-left: 8px;
margin-right: 8px;
}
.divider {
border: 1px solid #272626;
margin-left: 8px;
margin-right: 8px;
}
.installed-text {
color: #00ffd0;
}
.installed-text {
color: #00ffd0;
}
.latest-version {
color: #af5fff;
}
.latest-version {
color: #af5fff;
}
.visible {
display: block;
}
.visible {
display: block;
}
/* width */
::-webkit-scrollbar {
width: 6px;
}
/* width */
::-webkit-scrollbar {
width: 6px;
}
/* Track */
::-webkit-scrollbar-track {
background: #272626;
}
/* Track */
::-webkit-scrollbar-track {
background: #272626;
}
/* Handle */
::-webkit-scrollbar-thumb {
background: #949494;
border-radius: 4px;
}
/* Handle */
::-webkit-scrollbar-thumb {
background: #949494;
border-radius: 4px;
}
/* Handle on hover */
::-webkit-scrollbar-thumb:hover {
background: white;
}
/* Handle on hover */
::-webkit-scrollbar-thumb:hover {
background: white;
}
</style>

View file

@ -1,56 +1,56 @@
<script lang="ts">
import '$appcss';
import { afterUpdate } from 'svelte';
import ReviewCard from '@tea/ui/review-card/review-card.svelte';
import "$appcss";
import { afterUpdate } from "svelte";
import ReviewCard from "@tea/ui/review-card/review-card.svelte";
import type { Review } from '@tea/ui/types';
export let reviews: Review[];
import type { Review } from "@tea/ui/types";
export let reviews: Review[];
export let showLimit = 9;
let showMore = false;
export let showLimit = 9;
let showMore = false;
const getColReviews = (n: number) => {
const showReviews = reviews.filter((_item, i) => (i - n) % 3 === 0);
return showMore ? showReviews : showReviews.slice(0, showLimit / 3);
};
const getColReviews = (n: number) => {
const showReviews = reviews.filter((_item, i) => (i - n) % 3 === 0);
return showMore ? showReviews : showReviews.slice(0, showLimit / 3);
};
let col1: Review[] = [];
let col2: Review[] = [];
let col3: Review[] = [];
let col1: Review[] = [];
let col2: Review[] = [];
let col3: Review[] = [];
afterUpdate(() => {
col1 = getColReviews(0);
col2 = getColReviews(1);
col3 = getColReviews(2);
});
afterUpdate(() => {
col1 = getColReviews(0);
col2 = getColReviews(1);
col3 = getColReviews(2);
});
// TODO: problem with reviews with differing heights
// ideally they should work like metro-ui to not have extreme height diff between columns
// TODO: problem with reviews with differing heights
// ideally they should work like metro-ui to not have extreme height diff between columns
</script>
<header class="border-gray text-primary border bg-black p-4">REVIEWS ({reviews.length})</header>
<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">
{#each col1 as review}
<ReviewCard {review} />
<div class="mt-4" />
{/each}
</div>
<div class="border-gray w-1/3 border-0 border-l-2 border-b-2 p-4">
{#each col2 as review}
<ReviewCard {review} />
<div class="mt-4" />
{/each}
</div>
<div class="border-gray w-1/3 border-0 border-x-2 border-b-2 p-4">
{#each col3 as review}
<ReviewCard {review} />
<div class="mt-4" />
{/each}
</div>
<div class="border-gray w-1/3 border-0 border-l-2 border-b-2 p-4">
{#each col1 as review}
<ReviewCard {review} />
<div class="mt-4" />
{/each}
</div>
<div class="border-gray w-1/3 border-0 border-l-2 border-b-2 p-4">
{#each col2 as review}
<ReviewCard {review} />
<div class="mt-4" />
{/each}
</div>
<div class="border-gray w-1/3 border-0 border-x-2 border-b-2 p-4">
{#each col3 as review}
<ReviewCard {review} />
<div class="mt-4" />
{/each}
</div>
</section>
{#if showLimit <= reviews.length && showMore === false}
<footer class="border-gray border bg-black p-4">
<button on:click={() => (showMore = true)}>SHOW MORE</button>
</footer>
<footer class="border-gray border bg-black p-4">
<button on:click={() => (showMore = true)}>SHOW MORE</button>
</footer>
{/if}

View file

@ -1,14 +1,16 @@
<script>
import Button from "@tea/ui/button/button.svelte";
import { goto } from "$app/navigation";
import { goto } from "$app/navigation";
import { SideMenuOptions } from "$libs/types";
</script>
<div class="flex w-full h-3/4 justify-center items-center flex-col">
<div class="text-2xl text-[#e1e1e1] bg-[#252424] px-9 py-3">
You dont have anything installed
<div class="flex h-3/4 w-full flex-col items-center justify-center">
<div class="bg-[#252424] px-9 py-3 text-2xl text-[#e1e1e1]">
You dont have anything installed
</div>
<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>

View file

@ -1,5 +1,3 @@
<div class="flex w-full h-3/4 justify-center items-center">
<div class="text-2xl text-[#e1e1e1] bg-[#252424] px-9 py-3">
Youre all up to date 👍
</div>
<div class="flex h-3/4 w-full items-center justify-center">
<div class="bg-[#252424] px-9 py-3 text-2xl text-[#e1e1e1]">Youre all up to date 👍</div>
</div>

View file

@ -1,33 +1,33 @@
<script lang="ts">
import "$appcss";
import "$appcss";
import { PackageStates, type GUIPackage } from "$libs/types";
import { packagesStore } from "$libs/stores";
import { onMount } from "svelte";
import PackageCard from "$components/package-card/package-card.svelte";
import { PackageStates, type GUIPackage } from "$libs/types";
import { packagesStore } from "$libs/stores";
import { onMount } from "svelte";
import PackageCard from "$components/package-card/package-card.svelte";
export let tab = "all";
export let pkg: GUIPackage;
export let layout: "bottom" | "left" | "right" = "bottom";
export let tab = "all";
export let pkg: GUIPackage;
export let layout: "bottom" | "left" | "right" = "bottom";
onMount(() => {
packagesStore.fetchPackageBottles(pkg.full_name);
});
onMount(() => {
packagesStore.fetchPackageBottles(pkg.full_name);
});
</script>
<PackageCard
{pkg}
{layout}
link="/packages/{pkg.slug}?tab={tab}"
progessLoading={pkg.install_progress_percentage}
onClickCTA={async () => {
if (
[PackageStates.INSTALLED, PackageStates.INSTALLING, PackageStates.UPDATING].includes(
pkg.state
)
) {
return;
}
packagesStore.installPkg(pkg);
}}
{pkg}
{layout}
link="/packages/{pkg.slug}?tab={tab}"
progessLoading={pkg.install_progress_percentage}
onClickCTA={async () => {
if (
[PackageStates.INSTALLED, PackageStates.INSTALLING, PackageStates.UPDATING].includes(
pkg.state
)
) {
return;
}
packagesStore.installPkg(pkg);
}}
/>

View file

@ -1,159 +1,159 @@
<script lang="ts">
import "$appcss";
import { watchResize } from "svelte-watch-resize";
import InfiniteScroll from "svelte-infinite-scroll";
// import { t } from '$libs/translations';
import type { GUIPackage } from "$libs/types";
import moment from "moment";
import { PackageStates, SideMenuOptions } from "$libs/types";
import Preloader from "@tea/ui/Preloader/Preloader.svelte";
import Package from "./package.svelte";
import NoInstalls from "./no-installs.svelte";
import NoUpdates from "./no-updates.svelte";
import { packagesStore } from "$libs/stores";
import "$appcss";
import { watchResize } from "svelte-watch-resize";
import InfiniteScroll from "svelte-infinite-scroll";
// import { t } from '$libs/translations';
import type { GUIPackage } from "$libs/types";
import moment from "moment";
import { PackageStates, SideMenuOptions } from "$libs/types";
import Preloader from "@tea/ui/Preloader/Preloader.svelte";
import Package from "./package.svelte";
import NoInstalls from "./no-installs.svelte";
import NoUpdates from "./no-updates.svelte";
import { packagesStore } from "$libs/stores";
const { packageList: allPackages } = packagesStore;
export let packageFilter: SideMenuOptions = SideMenuOptions.all;
const { packageList: allPackages } = packagesStore;
export let packageFilter: SideMenuOptions = SideMenuOptions.all;
export let sortBy: "popularity" | "most recent" = "most recent";
export let sortDirection: "asc" | "desc" = "desc";
export let sortBy: "popularity" | "most recent" = "most recent";
export let sortDirection: "asc" | "desc" = "desc";
export let scrollY = 0;
export let scrollY = 0;
let loadMore = 9;
let limit = loadMore + 9;
let loadMore = 9;
let limit = loadMore + 9;
// TODO: figure out a better type strategy here so that this breaks if SideMenuOptions is updated
const pkgFilters: { [key: string]: (pkg: GUIPackage) => boolean } = {
[SideMenuOptions.all]: (_pkg: GUIPackage) => true,
[SideMenuOptions.installed]: (pkg: GUIPackage) => {
return [
PackageStates.INSTALLED,
PackageStates.INSTALLING,
PackageStates.NEEDS_UPDATE,
PackageStates.UPDATING
].includes(pkg.state);
},
[SideMenuOptions.installed_updates_available]: (pkg: GUIPackage) => {
return [PackageStates.UPDATING, PackageStates.NEEDS_UPDATE].includes(pkg.state);
},
[SideMenuOptions.recently_updated]: (pkg: GUIPackage) => {
return moment(pkg.last_modified).isAfter(moment().subtract(30, "days"));
},
[SideMenuOptions.new_packages]: (pkg: GUIPackage) => {
return moment(pkg.created).isAfter(moment().subtract(30, "days"));
},
[SideMenuOptions.popular]: (pkg: GUIPackage) =>
pkg.categories.includes(SideMenuOptions.popular),
[SideMenuOptions.featured]: (pkg: GUIPackage) =>
pkg.categories.includes(SideMenuOptions.featured),
[SideMenuOptions.essentials]: (pkg: GUIPackage) =>
pkg.categories.includes(SideMenuOptions.essentials),
[SideMenuOptions.starstruck]: (pkg: GUIPackage) =>
pkg.categories.includes(SideMenuOptions.starstruck),
[SideMenuOptions.made_by_tea]: (pkg: GUIPackage) => pkg.full_name.includes("tea.xyz")
};
// TODO: figure out a better type strategy here so that this breaks if SideMenuOptions is updated
const pkgFilters: { [key: string]: (pkg: GUIPackage) => boolean } = {
[SideMenuOptions.all]: (_pkg: GUIPackage) => true,
[SideMenuOptions.installed]: (pkg: GUIPackage) => {
return [
PackageStates.INSTALLED,
PackageStates.INSTALLING,
PackageStates.NEEDS_UPDATE,
PackageStates.UPDATING
].includes(pkg.state);
},
[SideMenuOptions.installed_updates_available]: (pkg: GUIPackage) => {
return [PackageStates.UPDATING, PackageStates.NEEDS_UPDATE].includes(pkg.state);
},
[SideMenuOptions.recently_updated]: (pkg: GUIPackage) => {
return moment(pkg.last_modified).isAfter(moment().subtract(30, "days"));
},
[SideMenuOptions.new_packages]: (pkg: GUIPackage) => {
return moment(pkg.created).isAfter(moment().subtract(30, "days"));
},
[SideMenuOptions.popular]: (pkg: GUIPackage) =>
pkg.categories.includes(SideMenuOptions.popular),
[SideMenuOptions.featured]: (pkg: GUIPackage) =>
pkg.categories.includes(SideMenuOptions.featured),
[SideMenuOptions.essentials]: (pkg: GUIPackage) =>
pkg.categories.includes(SideMenuOptions.essentials),
[SideMenuOptions.starstruck]: (pkg: GUIPackage) =>
pkg.categories.includes(SideMenuOptions.starstruck),
[SideMenuOptions.made_by_tea]: (pkg: GUIPackage) => pkg.full_name.includes("tea.xyz")
};
const onScroll = (e: Event) => {
const target = e.target as HTMLInputElement;
scrollY = target.scrollTop || 0;
};
const onScroll = (e: Event) => {
const target = e.target as HTMLInputElement;
scrollY = target.scrollTop || 0;
};
$: packages = $allPackages.filter(pkgFilters[packageFilter] || pkgFilters.all).sort((a, b) => {
if (sortBy === "popularity") {
const aPop = +a.dl_count + a.installs;
const bPop = +b.dl_count + b.installs;
return sortDirection === "asc" ? aPop - bPop : bPop - aPop;
} else {
// most recent
const aDate = new Date(a.last_modified);
const bDate = new Date(b.last_modified);
return sortDirection === "asc" ? +aDate - +bDate : +bDate - +aDate;
}
});
$: packages = $allPackages.filter(pkgFilters[packageFilter] || pkgFilters.all).sort((a, b) => {
if (sortBy === "popularity") {
const aPop = +a.dl_count + a.installs;
const bPop = +b.dl_count + b.installs;
return sortDirection === "asc" ? aPop - bPop : bPop - aPop;
} else {
// most recent
const aDate = new Date(a.last_modified);
const bDate = new Date(b.last_modified);
return sortDirection === "asc" ? +aDate - +bDate : +bDate - +aDate;
}
});
const onResize = (node: HTMLElement) => {
const assumedCardHeight = 250;
const cardRows = Math.floor(packages.length / 3);
const minCardRows = Math.floor(node.scrollHeight / assumedCardHeight);
if (cardRows < minCardRows) {
const addLimit = 3 * (minCardRows - cardRows);
limit += addLimit;
}
};
const onResize = (node: HTMLElement) => {
const assumedCardHeight = 250;
const cardRows = Math.floor(packages.length / 3);
const minCardRows = Math.floor(node.scrollHeight / assumedCardHeight);
if (cardRows < minCardRows) {
const addLimit = 3 * (minCardRows - cardRows);
limit += addLimit;
}
};
</script>
<div class="relative h-full w-full">
<ul class="flex flex-wrap content-start bg-black" use:watchResize={onResize} on:scroll={onScroll}>
{#if packages.length > 0}
{#each packages as pkg, index}
{#if index < limit}
<div class="card z-1 p-1" class:animate-puls={pkg.state === PackageStates.INSTALLING}>
<Package tab={packageFilter} {pkg} layout="bottom"/>
</div>
{/if}
{/each}
{:else if packageFilter === SideMenuOptions.installed}
<NoInstalls />
{:else if packageFilter === SideMenuOptions.installed_updates_available}
<NoUpdates />
{:else}
{#each Array(9) as _}
<section class="card p-1 h-{238}">
<div class="border-gray h-full w-full border">
<Preloader />
</div>
</section>
{/each}
{/if}
<InfiniteScroll threshold={100} on:loadMore={() => (limit += loadMore)} />
</ul>
<ul class="flex flex-wrap content-start bg-black" use:watchResize={onResize} on:scroll={onScroll}>
{#if packages.length > 0}
{#each packages as pkg, index}
{#if index < limit}
<div class="card z-1 p-1" class:animate-puls={pkg.state === PackageStates.INSTALLING}>
<Package tab={packageFilter} {pkg} layout="bottom" />
</div>
{/if}
{/each}
{:else if packageFilter === SideMenuOptions.installed}
<NoInstalls />
{:else if packageFilter === SideMenuOptions.installed_updates_available}
<NoUpdates />
{:else}
{#each Array(9) as _}
<section class="card p-1 h-{238}">
<div class="border-gray h-full w-full border">
<Preloader />
</div>
</section>
{/each}
{/if}
<InfiniteScroll threshold={100} on:loadMore={() => (limit += loadMore)} />
</ul>
</div>
<style>
ul {
margin-top: 0px;
padding-top: 80px;
height: calc(100vh - 49px);
overflow-y: scroll;
overflow-x: hidden;
padding-right: 4px;
}
ul {
margin-top: 0px;
padding-top: 80px;
height: calc(100vh - 49px);
overflow-y: scroll;
overflow-x: hidden;
padding-right: 4px;
}
/* width */
::-webkit-scrollbar {
width: 6px;
}
/* width */
::-webkit-scrollbar {
width: 6px;
}
/* Track */
::-webkit-scrollbar-track {
background: #272626;
}
/* Track */
::-webkit-scrollbar-track {
background: #272626;
}
/* Handle */
::-webkit-scrollbar-thumb {
background: #949494;
border-radius: 4px;
}
/* Handle */
::-webkit-scrollbar-thumb {
background: #949494;
border-radius: 4px;
}
/* Handle on hover */
::-webkit-scrollbar-thumb:hover {
background: white;
}
/* Handle on hover */
::-webkit-scrollbar-thumb:hover {
background: white;
}
.card {
width: 100%;
}
.card {
width: 100%;
}
@media screen and (min-width: 650px) {
.card {
width: 50%;
}
}
@media screen and (min-width: 650px) {
.card {
width: 50%;
}
}
@media screen and (min-width: 1000px) {
.card {
width: 33.333333%;
}
}
@media screen and (min-width: 1000px) {
.card {
width: 33.333333%;
}
}
</style>

View file

@ -1,14 +1,14 @@
<script lang="ts">
export let coverUrl = '';
let clazz = '';
export { clazz as class };
export let coverUrl = "";
let clazz = "";
export { clazz as class };
</script>
<figure class="relative mb-8 h-32 w-full uppercase {clazz}">
{#if coverUrl}
<img src={coverUrl} class="absolute z-0 h-32 w-full object-cover" alt="cover" />
{/if}
<div class="text-primary absolute bottom-0 text-6xl leading-[32px]">
<slot />
</div>
{#if coverUrl}
<img src={coverUrl} class="absolute z-0 h-32 w-full object-cover" alt="cover" />
{/if}
<div class="text-primary absolute bottom-0 text-6xl leading-[32px]">
<slot />
</div>
</figure>

View file

@ -1,24 +1,24 @@
<script lang="ts">
export let label = '';
export let label = "";
</script>
<section class="bg-gray p-8">
<header>{label}</header>
<slot />
<header>{label}</header>
<slot />
</section>
<style>
section {
position: relative;
min-height: 240px;
height: 100%;
width: 100%;
min-width: 100%;
/* background-color: #ccc; */
display: flex;
}
header {
color: rgb(50, 48, 48);
font-size: 3em;
}
section {
position: relative;
min-height: 240px;
height: 100%;
width: 100%;
min-width: 100%;
/* background-color: #ccc; */
display: flex;
}
header {
color: rgb(50, 48, 48);
font-size: 3em;
}
</style>

View file

@ -1,35 +1,35 @@
<script lang="ts">
import '$appcss';
import { shellOpenExternal } from '@native';
const openGithub = () => {
shellOpenExternal("https://github.com/teaxyz");
};
import "$appcss";
import { shellOpenExternal } from "@native";
const openGithub = () => {
shellOpenExternal("https://github.com/teaxyz");
};
</script>
<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>
<div class="listbox-item border-gray border-b p-6">
<a href="/cli">
<p>Install Tea</p>
</a>
</div>
<div class="listbox-item border-gray border-b p-6">
<div>
<p>Authenticate</p>
</div>
</div>
<div class="listbox-item p-6">
<button on:click={openGithub}>Give tea a star</button>
</div>
<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">
<a href="/cli">
<p>Install Tea</p>
</a>
</div>
<div class="listbox-item border-gray border-b p-6">
<div>
<p>Authenticate</p>
</div>
</div>
<div class="listbox-item p-6">
<button on:click={openGithub}>Give tea a star</button>
</div>
</div>
<style>
.card {
border: 2px solid #949494;
background-color: #1a1a1a;
}
.card {
border: 2px solid #949494;
background-color: #1a1a1a;
}
.listbox-item {
height: 75px;
}
.listbox-item {
height: 75px;
}
</style>

View file

@ -1,33 +1,33 @@
<script lang="ts">
import '$appcss';
import { authStore } from '$libs/stores';
import "$appcss";
import { authStore } from "$libs/stores";
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 { 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
</script>
{#if $user}
<section class="border-gray border-2 bg-black p-2">
<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" />
<div class="flex w-4/5 items-center p-5">
<div class="w-1/2 pl-5">
<p class="text-gray uppercase">Authenticated with GitHub</p>
<p />
<p class="text-primary text-4xl">@{$user.login}</p>
</div>
<div class="border-gray h-full border-l" />
<div class="w-1/2 pl-10">
<p class="text-gray uppercase leading-loose">
Country: <span>$user?.country}</span><br />Wallet:
{#if $user.wallet}
<span>{$user.wallet}</span>
{:else}
<a class="text-green underline" href="/">Connect Now</a>
{/if}
</p>
</div>
</div>
</div>
</section>
<section class="border-gray border-2 bg-black p-2">
<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" />
<div class="flex w-4/5 items-center p-5">
<div class="w-1/2 pl-5">
<p class="text-gray uppercase">Authenticated with GitHub</p>
<p />
<p class="text-primary text-4xl">@{$user.login}</p>
</div>
<div class="border-gray h-full border-l" />
<div class="w-1/2 pl-10">
<p class="text-gray uppercase leading-loose">
Country: <span>$user?.country}</span><br />Wallet:
{#if $user.wallet}
<span>{$user.wallet}</span>
{:else}
<a class="text-green underline" href="/">Connect Now</a>
{/if}
</p>
</div>
</div>
</div>
</section>
{/if}

View file

@ -20,6 +20,18 @@
};
</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>
svg {
fill: var(--progress-fill, transparent);
@ -48,14 +60,3 @@
transform: translate(-50%, -50%);
}
</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">
import '$appcss';
import { t } from '$libs/translations';
import "$appcss";
import { t } from "$libs/translations";
type SortOption = "popularity" | "most recent";
export let onSort: (opt: SortOption, dir: "asc" | "desc") => void;
type SortOption = "popularity" | "most recent";
export let onSort: (opt: SortOption, dir: 'asc' | 'desc') => void;
let sortBy: SortOption = "popularity";
let sortDirection: "asc" | "desc" = "desc";
let sortBy: SortOption = "popularity";
let sortDirection: 'asc' | 'desc' = 'desc';
const sortOptions: SortOption[] = ["popularity", "most recent"];
const sortOptions: SortOption[] = ["popularity", "most recent"];
const optionLabels = {
[sortOptions[0]]: $t("sorting.popularity"),
[sortOptions[1]]: $t("sorting.most-recent")
};
const optionLabels = {
[sortOptions[0]]: $t("sorting.popularity"),
[sortOptions[1]]: $t("sorting.most-recent")
}
const setSortBy = (opt: SortOption) => {
sortBy = opt;
if (onSort) {
onSort(sortBy, sortDirection);
}
};
const setSortDir = (opt: SortOption, dir: 'asc' | 'desc') => {
sortDirection = dir;
setSortBy(opt);
};
const setSortBy = (opt: SortOption) => {
sortBy = opt;
if (onSort) {
onSort(sortBy, sortDirection);
}
};
const setSortDir = (opt: SortOption, dir: "asc" | "desc") => {
sortDirection = dir;
setSortBy(opt);
};
</script>
<section class="sorting-container bg-black text-gray">
<div class="dropdown">
<div class="dropdown-title">{$t("sorting.label")}</div>
<ul class="dropdown-content column flex">
{#each sortOptions as option}
<li class="flex items-center">
<button
class="sort-btn"
class:active={sortBy === option}
on:click={() => setSortBy(option)}
>
{optionLabels[option]}
</button>
<div class="direction-arrows">
<button
on:click={() => setSortDir(option, 'asc')}
class={sortBy === option && sortDirection === 'asc' ? 'active' : ''}>&uarr;</button
>
<button
on:click={() => setSortDir(option, 'desc')}
class={sortBy === option && sortDirection === 'desc' ? 'active' : ''}>&darr;</button
>
</div>
</li>
{/each}
</ul>
</div>
<section class="sorting-container text-gray bg-black">
<div class="dropdown">
<div class="dropdown-title">{$t("sorting.label")}</div>
<ul class="dropdown-content column flex">
{#each sortOptions as option}
<li class="flex items-center">
<button
class="sort-btn"
class:active={sortBy === option}
on:click={() => setSortBy(option)}
>
{optionLabels[option]}
</button>
<div class="direction-arrows">
<button
on:click={() => setSortDir(option, "asc")}
class={sortBy === option && sortDirection === "asc" ? "active" : ""}>&uarr;</button
>
<button
on:click={() => setSortDir(option, "desc")}
class={sortBy === option && sortDirection === "desc" ? "active" : ""}>&darr;</button
>
</div>
</li>
{/each}
</ul>
</div>
</section>
<style>
.direction-arrows {
float: right;
}
.direction-arrows button {
opacity: 0.3;
}
.direction-arrows button.active {
opacity: 1;
}
.direction-arrows {
float: right;
}
.direction-arrows button {
opacity: 0.3;
}
.direction-arrows button.active {
opacity: 1;
}
.sorting-container {
display: inline-block;
text-decoration: none;
max-width: 240px;
width: 100%;
height: 100%;
min-height: 30px;
transition: 0.1s linear;
}
.sorting-container {
display: inline-block;
text-decoration: none;
max-width: 240px;
width: 100%;
height: 100%;
min-height: 30px;
transition: 0.1s linear;
}
.dropdown {
width: 100%;
height: auto;
position: relative;
display: inline-block;
cursor: pointer;
}
.dropdown {
width: 100%;
height: auto;
position: relative;
display: inline-block;
cursor: pointer;
}
.dropdown-content {
display: none;
position: absolute;
background-color: #1a1a1a;
width: 100%;
box-shadow: 0px 8px 16px 0px rgba(0, 0, 0, 0.2);
z-index: 1;
color: white;
list-style: none;
padding: 0px;
}
.dropdown-content {
display: none;
position: absolute;
background-color: #1a1a1a;
width: 100%;
box-shadow: 0px 8px 16px 0px rgba(0, 0, 0, 0.2);
z-index: 1;
color: white;
list-style: none;
padding: 0px;
}
.dropdown-content li {
position: relative;
padding: 0px 10px;
height: 32px;
width: 100%;
line-height: 36px;
}
.dropdown-content li {
position: relative;
padding: 0px 10px;
height: 32px;
width: 100%;
line-height: 36px;
}
.dropdown-content li .sort-btn {
height: 100%;
width: calc(100% - 40px);
opacity: 0.6;
}
.dropdown-content li .sort-btn {
height: 100%;
width: calc(100% - 40px);
opacity: 0.6;
}
.dropdown-content li .sort-btn.active {
font-weight: bold;
opacity: 1;
}
.dropdown-content li .sort-btn.active {
font-weight: bold;
opacity: 1;
}
.dropdown-content li .direction-arrows {
position: absolute;
right: 10px;
top: 0px;
}
.dropdown-content li .direction-arrows {
position: absolute;
right: 10px;
top: 0px;
}
.dropdown-content li:hover {
background: #00ffd0;
color: black;
}
.dropdown:hover .dropdown-content {
display: block;
}
.dropdown-content li:hover {
background: #00ffd0;
color: black;
}
.dropdown:hover .dropdown-content {
display: block;
}
.dropdown-title {
height: 40px;
line-height: 40px;
padding-left: 10px;
}
.dropdown-title {
height: 40px;
line-height: 40px;
padding-left: 10px;
}
</style>

View file

@ -3,13 +3,17 @@
import { shellOpenExternal } from "@native";
</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-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">
<Button type="plain" color="blue" onClick={() => shellOpenExternal("https://github.com/teaxyz/pantry")}>
<span class="text-sm px-12">VISIT PANTRY</span>
<Button
type="plain"
color="blue"
onClick={() => shellOpenExternal("https://github.com/teaxyz/pantry")}
>
<span class="px-12 text-sm">VISIT PANTRY</span>
</Button>
</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>

View file

@ -1,62 +1,62 @@
<script lang="ts">
import type { GUIPackage } from "$libs/types";
import { packagesStore } from "$libs/stores";
import { onMount } from "svelte";
import type { GUIPackage } from "$libs/types";
import { packagesStore } from "$libs/stores";
import { onMount } from "svelte";
import ImgLoader from "@tea/ui/img-loader/img-loader.svelte";
import { goto } from "$app/navigation";
import PackageInstallButton from "$components/package-install-button/package-install-button.svelte";
export let pkg: GUIPackage; // Fuse package search result probably not updated
export let onClick: () => Promise<void>;
export let onClose: () => void;
import ImgLoader from "@tea/ui/img-loader/img-loader.svelte";
import { goto } from "$app/navigation";
import PackageInstallButton from "$components/package-install-button/package-install-button.svelte";
export let pkg: GUIPackage; // Fuse package search result probably not updated
export let onClick: () => Promise<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(() => {
packagesStore.fetchPackageBottles(pkg.full_name);
});
onMount(() => {
packagesStore.fetchPackageBottles(pkg.full_name);
});
const gotoPackagePage = () => {
goto(`/packages/${pkg.slug}?tab=all`);
onClose();
};
const gotoPackagePage = () => {
goto(`/packages/${pkg.slug}?tab=all`);
onClose();
};
</script>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<figure class="border-gray flex flex-row gap-2 border p-2">
<ImgLoader
on:click={() => gotoPackagePage()}
class="pkg-image h-16 w-16 object-cover"
src={!pkg.thumb_image_url.includes("https://tea.xyz")
? "/images/default-thumb.jpg"
: pkg.thumb_image_url}
alt={pkg.name}
/>
<header class="flex-grow" on:click={() => gotoPackagePage()}>
<h1>{pkg.full_name}</h1>
<p class="line-clamp-2 text-xs">{pkg.desc}</p>
</header>
<aside>
<div>
{#if updatedPkg}
<PackageInstallButton pkg={updatedPkg} {onClick} />
{/if}
</div>
</aside>
<ImgLoader
on:click={() => gotoPackagePage()}
class="pkg-image h-16 w-16 object-cover"
src={!pkg.thumb_image_url.includes("https://tea.xyz")
? "/images/default-thumb.jpg"
: pkg.thumb_image_url}
alt={pkg.name}
/>
<header class="flex-grow" on:click={() => gotoPackagePage()}>
<h1>{pkg.full_name}</h1>
<p class="line-clamp-2 text-xs">{pkg.desc}</p>
</header>
<aside>
<div>
{#if updatedPkg}
<PackageInstallButton pkg={updatedPkg} {onClick} />
{/if}
</div>
</aside>
</figure>
<style>
figure:hover {
background-color: #252525;
}
figure:hover {
background-color: #252525;
}
aside {
display: flex;
flex-direction: column;
justify-content: center;
min-width: 140px;
margin-right: 22px;
}
aside {
display: flex;
flex-direction: column;
justify-content: center;
min-width: 140px;
margin-right: 22px;
}
</style>

View file

@ -1,111 +1,111 @@
<script lang="ts">
import { packagesStore, searchStore } from "$libs/stores";
import type { GUIPackage } from "$libs/types";
import SearchInput from "@tea/ui/search-input/search-input.svelte";
import { t } from "$libs/translations";
import Preloader from "@tea/ui/Preloader/Preloader.svelte";
import Package from "$components/packages/package.svelte";
import { PackageStates } from "$libs/types";
import PackageResult from "./package-search-result.svelte";
import Mousetrap from "mousetrap";
// import Posts from '@tea/ui/posts/posts.svelte';
import { packagesStore, searchStore } from "$libs/stores";
import type { GUIPackage } from "$libs/types";
import SearchInput from "@tea/ui/search-input/search-input.svelte";
import { t } from "$libs/translations";
import Preloader from "@tea/ui/Preloader/Preloader.svelte";
import Package from "$components/packages/package.svelte";
import { PackageStates } from "$libs/types";
import PackageResult from "./package-search-result.svelte";
import Mousetrap from "mousetrap";
// import Posts from '@tea/ui/posts/posts.svelte';
import { installPackage } from "@native";
import { onMount } from "svelte";
import { installPackage } from "@native";
import { onMount } from "svelte";
import NoSearchResults from "./no-search-results.svelte";
const { searching, packagesSearch } = searchStore;
// import type { AirtablePost } from '@tea/ui/types';
let term: string;
// let articles: AirtablePost[] = []; // news, blogs, etc
// let workshops: AirtablePost[] = []; // workshops, course
let loading = true;
const { searching, packagesSearch } = searchStore;
// import type { AirtablePost } from '@tea/ui/types';
let term: string;
// let articles: AirtablePost[] = []; // news, blogs, etc
// let workshops: AirtablePost[] = []; // workshops, course
let loading = true;
// searchStore.packagesSearch.subscribe((pkgs) => {
// packages = pkgs;
// });
// searchStore.postsSearch.subscribe((posts) => {
// let partialArticles: AirtablePost[] = [];
// let partialWorkshops: AirtablePost[] = [];
// for (let post of posts) {
// if (post.tags.includes('news')) {
// partialArticles.push(post);
// }
// if (post.tags.includes('course') || post.tags.includes('featured_course')) {
// partialWorkshops.push(post);
// }
// }
// searchStore.packagesSearch.subscribe((pkgs) => {
// packages = pkgs;
// });
// searchStore.postsSearch.subscribe((posts) => {
// let partialArticles: AirtablePost[] = [];
// let partialWorkshops: AirtablePost[] = [];
// for (let post of posts) {
// if (post.tags.includes('news')) {
// partialArticles.push(post);
// }
// if (post.tags.includes('course') || post.tags.includes('featured_course')) {
// partialWorkshops.push(post);
// }
// }
// articles = partialArticles;
// workshops = partialWorkshops;
// });
// articles = partialArticles;
// workshops = partialWorkshops;
// });
searchStore.searching.subscribe((v) => (loading = v));
searchStore.searching.subscribe((v) => (loading = v));
const onClose = () => {
term = "";
searchStore.searching.set(false);
};
const onClose = () => {
term = "";
searchStore.searching.set(false);
};
</script>
{#if $searching === true}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div id="bg-close" class="z-40" on:click={onClose} />
<section class="z-50">
<header class="border-gray flex border border-x-0 border-t-0 bg-black">
<div class="relative w-full">
<SearchInput
class="h-9 w-full rounded-sm"
size="small"
autofocus={true}
placeholder={$t("store-search-placeholder")}
onSearch={(search) => {
term = search;
searchStore.search(search);
}}
/>
<div class="absolute top-1 right-4 flex items-center gap-1 pt-[1px] opacity-50">
<span class="mr-1 text-xs">clear</span>
<kbd class=" bg-gray flex items-center rounded-sm px-2 pt-[1px] text-white">
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div id="bg-close" class="z-40" on:click={onClose} />
<section class="z-50">
<header class="border-gray flex border border-x-0 border-t-0 bg-black">
<div class="relative w-full">
<SearchInput
class="h-9 w-full rounded-sm"
size="small"
autofocus={true}
placeholder={$t("store-search-placeholder")}
onSearch={(search) => {
term = search;
searchStore.search(search);
}}
/>
<div class="absolute top-1 right-4 flex items-center gap-1 pt-[1px] opacity-50">
<span class="mr-1 text-xs">clear</span>
<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 -->
<span class="" style="font-family: system-ui, -apple-system, sans-serif">⌘⇧⌫</span>
</kbd>
</div>
</div>
<button class="mr-2" on:click={onClose}>&#x2715</button>
</header>
{#if term}
<div class="z-20 bg-black">
{#if $packagesSearch.length > 0}
<header class="text-gray p-4 text-lg">
packages ({$packagesSearch.length})
</header>
<ul class="flex flex-col gap-2 p-2">
{#each $packagesSearch as pkg}
<div class={pkg.state === PackageStates.INSTALLING ? "animate-pulse" : ""}>
<PackageResult
{pkg}
{onClose}
onClick={async () => {
if (
[
PackageStates.INSTALLED,
PackageStates.INSTALLING,
PackageStates.UPDATING
].includes(pkg.state)
) {
return;
}
packagesStore.installPkg(pkg);
}}
/>
</div>
{/each}
</ul>
{:else}
<NoSearchResults />
{/if}
<!-- <header class="text-primary p-4 text-lg">
<span class="" style="font-family: system-ui, -apple-system, sans-serif">⌘⇧⌫</span>
</kbd>
</div>
</div>
<button class="mr-2" on:click={onClose}>&#x2715</button>
</header>
{#if term}
<div class="z-20 bg-black">
{#if $packagesSearch.length > 0}
<header class="text-gray p-4 text-lg">
packages ({$packagesSearch.length})
</header>
<ul class="flex flex-col gap-2 p-2">
{#each $packagesSearch as pkg}
<div class={pkg.state === PackageStates.INSTALLING ? "animate-pulse" : ""}>
<PackageResult
{pkg}
{onClose}
onClick={async () => {
if (
[
PackageStates.INSTALLED,
PackageStates.INSTALLING,
PackageStates.UPDATING
].includes(pkg.state)
) {
return;
}
packagesStore.installPkg(pkg);
}}
/>
</div>
{/each}
</ul>
{:else}
<NoSearchResults />
{/if}
<!-- <header class="text-primary p-4 text-lg">
Top Article Results ({articles.length})
</header>
{#if articles.length}
@ -125,67 +125,67 @@
<Preloader />
</section>
{/if} -->
</div>
{:else}
<div class="flex h-full w-full flex-col justify-center bg-black">
<p class="text-gray text-center">start typing to search</p>
</div>
{/if}
</section>
</div>
{:else}
<div class="flex h-full w-full flex-col justify-center bg-black">
<p class="text-gray text-center">start typing to search</p>
</div>
{/if}
</section>
{/if}
<style>
#bg-close {
position: fixed;
width: calc(100vw - 2px);
height: calc(100vh - 2px);
top: 1px;
left: 1px;
background: rgba(0, 0, 0, 0.7);
border-radius: 12px;
}
section {
position: fixed;
top: 50px;
left: 50px;
right: 50px;
bottom: 50px;
background: rgba(0, 0, 0, 0.7);
transition: opacity 0.3s ease-in-out;
opacity: 1;
overflow: hidden;
height: auto;
border: gray 1px solid;
border-radius: 12px;
}
#bg-close {
position: fixed;
width: calc(100vw - 2px);
height: calc(100vh - 2px);
top: 1px;
left: 1px;
background: rgba(0, 0, 0, 0.7);
border-radius: 12px;
}
section {
position: fixed;
top: 50px;
left: 50px;
right: 50px;
bottom: 50px;
background: rgba(0, 0, 0, 0.7);
transition: opacity 0.3s ease-in-out;
opacity: 1;
overflow: hidden;
height: auto;
border: gray 1px solid;
border-radius: 12px;
}
section > div {
position: relative;
margin-top: 2px;
height: calc(100% - 40px);
width: 100%;
transition: height 0.6s ease-in-out;
overflow-y: scroll;
}
section > div {
position: relative;
margin-top: 2px;
height: calc(100% - 40px);
width: 100%;
transition: height 0.6s ease-in-out;
overflow-y: scroll;
}
/* width */
::-webkit-scrollbar {
width: 6px;
}
/* width */
::-webkit-scrollbar {
width: 6px;
}
/* Track */
::-webkit-scrollbar-track {
background: #272626;
}
/* Track */
::-webkit-scrollbar-track {
background: #272626;
}
/* Handle */
::-webkit-scrollbar-thumb {
background: #949494;
border-radius: 4px;
}
/* Handle */
::-webkit-scrollbar-thumb {
background: #949494;
border-radius: 4px;
}
/* Handle on hover */
::-webkit-scrollbar-thumb:hover {
background: white;
}
/* Handle on hover */
::-webkit-scrollbar-thumb:hover {
background: white;
}
</style>

View file

@ -1,16 +1,16 @@
<script lang="ts">
import { t, locales, locale } from "$libs/translations";
import SelectExpand from "@tea/ui/select-expand/select-expand.svelte";
import { t, locales, locale } from "$libs/translations";
import SelectExpand from "@tea/ui/select-expand/select-expand.svelte";
const label = "language";
const label = "language";
</script>
<SelectExpand
{label}
bind:value={$locale}
options={$locales.map((value) => ({
label: $t(`lang.${value}`),
value,
selected: value === $locale
}))}
{label}
bind:value={$locale}
options={$locales.map((value) => ({
label: $t(`lang.${value}`),
value,
selected: value === $locale
}))}
/>

View file

@ -1,83 +1,83 @@
<script lang="ts">
import { shellOpenExternal, submitLogs } from "@native";
import mouseLeaveDelay from "@tea/ui/lib/mouse-leave-delay";
import UpdateButton from "./update-button.svelte";
import { appUpdateStore } from "$libs/stores";
const { updateStatus } = appUpdateStore;
import { shellOpenExternal, submitLogs } from "@native";
import mouseLeaveDelay from "@tea/ui/lib/mouse-leave-delay";
import UpdateButton from "./update-button.svelte";
import { appUpdateStore } from "$libs/stores";
const { updateStatus } = appUpdateStore;
const hidePopup = () => {
isOpen = false;
};
const hidePopup = () => {
isOpen = false;
};
const preventDoubleClick = (evt: MouseEvent) => evt.stopPropagation();
const preventDoubleClick = (evt: MouseEvent) => evt.stopPropagation();
$: isOpen = false;
$: isOpen = false;
</script>
<div
class="relative"
use:mouseLeaveDelay={2000}
on:leave_delay={() => hidePopup()}
on:dblclick={preventDoubleClick}
class="relative"
use:mouseLeaveDelay={2000}
on:leave_delay={() => hidePopup()}
on:dblclick={preventDoubleClick}
>
<button
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"}
on:click={() => (isOpen = !isOpen)}
<button
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"}
on:click={() => (isOpen = !isOpen)}
title="settings"
>
<div class="icon-gear text-l text-gray flex group-hover:text-black" />
</button>
>
<div class="icon-gear text-l text-gray flex group-hover:text-black" />
</button>
<nav
class="menu border-gray absolute w-full border bg-black p-2 text-xs transition-all"
class:invisible={!isOpen}
class:visible={isOpen}
>
<!-- TODO: what is this supposed to do? -->
<!-- <button
<nav
class="menu border-gray absolute w-full border bg-black p-2 text-xs transition-all"
class:invisible={!isOpen}
class:visible={isOpen}
>
<!-- TODO: what is this supposed to do? -->
<!-- <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"
>
language
</button>
<hr /> -->
<button
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")}
>
docs
</button>
<hr />
<UpdateButton />
</nav>
<button
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")}
>
docs
</button>
<hr />
<UpdateButton />
</nav>
</div>
<style>
hr {
border: 1px solid #272626;
margin: 1px 0;
}
hr {
border: 1px solid #272626;
margin: 1px 0;
}
.menu {
top: calc(100% + 4px);
left: calc(50% - 80px);
width: 160px;
}
.menu {
top: calc(100% + 4px);
left: calc(50% - 80px);
width: 160px;
}
.circle-badge::after {
content: "1";
position: absolute;
width: 14px;
height: 14px;
display: flex;
justify-content: center;
align-items: center;
color: white;
border-radius: 50%;
background: #ff4100;
font-size: 10px;
top: -7px;
right: -7px;
z-index: 1;
}
.circle-badge::after {
content: "1";
position: absolute;
width: 14px;
height: 14px;
display: flex;
justify-content: center;
align-items: center;
color: white;
border-radius: 50%;
background: #ff4100;
font-size: 10px;
top: -7px;
right: -7px;
z-index: 1;
}
</style>

View file

@ -1,50 +1,50 @@
<script lang="ts">
import Spinner from "@tea/ui/spinner/spinner.svelte";
import { relaunch } from "@native";
import { appUpdateStore } from "$libs/stores";
import Spinner from "@tea/ui/spinner/spinner.svelte";
import { relaunch } from "@native";
import { appUpdateStore } from "$libs/stores";
const { updateStatus } = appUpdateStore;
const { updateStatus } = appUpdateStore;
</script>
{#if $updateStatus.status === "up-to-date"}
<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"
>
<div>up to date</div>
<i class="installed-text icon-check-circle-o flex text-[#00ffd0]" />
</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"
>
<div>up to date</div>
<i class="installed-text icon-check-circle-o flex text-[#00ffd0]" />
</div>
{:else if $updateStatus.status === "available"}
<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"
>
<div>fetching update</div>
<Spinner />
</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"
>
<div>fetching update</div>
<Spinner />
</div>
{:else if $updateStatus.status === "ready"}
<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"
on:click={relaunch}
>
<div class="flex items-center">
<div class="circle-badge mr-2">1</div>
<div>update</div>
</div>
<div class="rounded-sm bg-white px-2 text-[10px] leading-[12px] text-black">
v{$updateStatus.version}
</div>
</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"
on:click={relaunch}
>
<div class="flex items-center">
<div class="circle-badge mr-2">1</div>
<div>update</div>
</div>
<div class="rounded-sm bg-white px-2 text-[10px] leading-[12px] text-black">
v{$updateStatus.version}
</div>
</button>
{/if}
<style>
.circle-badge {
display: flex;
justify-content: center;
align-items: center;
color: white;
background: #ff4100;
font-size: 10px;
width: 14px;
height: 14px;
border-radius: 50%;
}
.circle-badge {
display: flex;
justify-content: center;
align-items: center;
color: white;
background: #ff4100;
font-size: 10px;
width: 14px;
height: 14px;
border-radius: 50%;
}
</style>

View file

@ -1,31 +1,31 @@
<script lang="ts">
import "$appcss";
export let icon: string;
export let label: string;
import "$appcss";
export let icon: string;
export let label: string;
export let active = false;
export let active = false;
</script>
<button
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:active
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:active
>
<i class="icon-{icon} mt-1" />
<div class="text-sm font-thin">
{label}
</div>
<slot />
<i class="icon-{icon} mt-1" />
<div class="text-sm font-thin">
{label}
</div>
<slot />
</button>
<style>
button {
box-sizing: border-box;
height: 37px;
}
button {
box-sizing: border-box;
height: 37px;
}
button.active {
background: rgba(148, 148, 148, 0.5);
border: rgba(148, 148, 148, 1) 1px solid;
}
button.active {
background: rgba(148, 148, 148, 0.5);
border: rgba(148, 148, 148, 1) 1px solid;
}
</style>

View file

@ -1,105 +1,105 @@
<script lang="ts">
import "$appcss";
import { PackageStates, SideMenuOptions } from "$libs/types";
import { packagesStore } from "$libs/stores";
import MenuButton from "./menu-button.svelte";
import { t } from "$libs/translations";
import { goto } from "$app/navigation";
const { packageList } = packagesStore;
import "$appcss";
import { PackageStates, SideMenuOptions } from "$libs/types";
import { packagesStore } from "$libs/stores";
import MenuButton from "./menu-button.svelte";
import { t } from "$libs/translations";
import { goto } from "$app/navigation";
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>
<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">
<MenuButton
label={$t("tags.discover").toLowerCase()}
icon="map"
active={activeOption === SideMenuOptions.discover}
on:click={() => goto(`/?tab=${SideMenuOptions.discover}`)}
/>
<hr />
<MenuButton
label={$t("side-menu-title.all").toLowerCase()}
icon="grid"
active={activeOption === SideMenuOptions.all}
on:click={() => goto(`/?tab=${SideMenuOptions.all}`)}
/>
<hr />
<MenuButton
label="installed"
icon="tea-checkmark"
active={activeOption === SideMenuOptions.installed}
on:click={() => goto(`/?tab=${SideMenuOptions.installed}`)}
/>
<hr />
<MenuButton
label={$t("tags.installed_updates_available").toLowerCase()}
icon="update"
active={activeOption === SideMenuOptions.installed_updates_available}
on:click={() => goto(`/?tab=${SideMenuOptions.installed_updates_available}`)}
>
{#if needsUpdateCount > 0}
<div class="update-count-badge">{needsUpdateCount}</div>
{/if}
</MenuButton>
<hr />
<MenuButton
label={$t("tags.new_packages").toLowerCase()}
icon="birthday-cake"
active={activeOption === SideMenuOptions.new_packages}
on:click={() => goto(`/?tab=${SideMenuOptions.new_packages}`)}
/>
<!-- <hr />
<ul class="flex flex-col gap-1 pt-4 pl-1">
<MenuButton
label={$t("tags.discover").toLowerCase()}
icon="map"
active={activeOption === SideMenuOptions.discover}
on:click={() => goto(`/?tab=${SideMenuOptions.discover}`)}
/>
<hr />
<MenuButton
label={$t("side-menu-title.all").toLowerCase()}
icon="grid"
active={activeOption === SideMenuOptions.all}
on:click={() => goto(`/?tab=${SideMenuOptions.all}`)}
/>
<hr />
<MenuButton
label="installed"
icon="tea-checkmark"
active={activeOption === SideMenuOptions.installed}
on:click={() => goto(`/?tab=${SideMenuOptions.installed}`)}
/>
<hr />
<MenuButton
label={$t("tags.installed_updates_available").toLowerCase()}
icon="update"
active={activeOption === SideMenuOptions.installed_updates_available}
on:click={() => goto(`/?tab=${SideMenuOptions.installed_updates_available}`)}
>
{#if needsUpdateCount > 0}
<div class="update-count-badge">{needsUpdateCount}</div>
{/if}
</MenuButton>
<hr />
<MenuButton
label={$t("tags.new_packages").toLowerCase()}
icon="birthday-cake"
active={activeOption === SideMenuOptions.new_packages}
on:click={() => goto(`/?tab=${SideMenuOptions.new_packages}`)}
/>
<!-- <hr />
<MenuButton
label={$t("tags.popular").toLowerCase()}
icon="bar-chart"
active={activeOption === SideMenuOptions.popular}
on:click={() => goto(`/?tab=${SideMenuOptions.popular}`)}
/> -->
<hr />
<MenuButton
label={$t("tags.recently_updated").toLowerCase()}
icon="back-in-time"
active={activeOption === SideMenuOptions.recently_updated}
on:click={() => goto(`/?tab=${SideMenuOptions.recently_updated}`)}
/>
<hr />
<MenuButton
label={$t("tags.made_by_tea").toLowerCase()}
icon="tea-logo-iconasset-1"
active={activeOption === SideMenuOptions.made_by_tea}
on:click={() => goto(`/?tab=${SideMenuOptions.made_by_tea}`)}
/>
</ul>
<hr />
<MenuButton
label={$t("tags.recently_updated").toLowerCase()}
icon="back-in-time"
active={activeOption === SideMenuOptions.recently_updated}
on:click={() => goto(`/?tab=${SideMenuOptions.recently_updated}`)}
/>
<hr />
<MenuButton
label={$t("tags.made_by_tea").toLowerCase()}
icon="tea-logo-iconasset-1"
active={activeOption === SideMenuOptions.made_by_tea}
on:click={() => goto(`/?tab=${SideMenuOptions.made_by_tea}`)}
/>
</ul>
</aside>
<style>
aside {
position: absolute;
left: 0px;
top: 0px;
height: calc(100vh - 48px); /* win.height - title-bar.height */
width: 210px;
box-sizing: border-box;
}
aside {
position: absolute;
left: 0px;
top: 0px;
height: calc(100vh - 48px); /* win.height - title-bar.height */
width: 210px;
box-sizing: border-box;
}
hr {
border-top: 1px solid #272626;
}
hr {
border-top: 1px solid #272626;
}
.update-count-badge {
display: flex;
justify-content: center;
align-items: center;
color: white;
background: #ff4100;
font-size: 11px;
width: 22px;
height: 22px;
border-radius: 50%;
margin-left: 4px;
}
.update-count-badge {
display: flex;
justify-content: center;
align-items: center;
color: white;
background: #ff4100;
font-size: 11px;
width: 22px;
height: 22px;
border-radius: 50%;
margin-left: 4px;
}
</style>

View file

@ -1,40 +1,40 @@
<script lang="ts">
import "$appcss";
import type { GUIPackage } from "$libs/types";
import type { Package } from "@tea/ui/types";
import { PackageStates } from "$libs/types";
import Preloader from "@tea/ui/Preloader/Preloader.svelte";
import PackageCard from "$components/packages/package.svelte";
import { onMount } from "svelte";
import { packagesStore } from "$libs/stores";
import "$appcss";
import type { GUIPackage } from "$libs/types";
import type { Package } from "@tea/ui/types";
import { PackageStates } from "$libs/types";
import Preloader from "@tea/ui/Preloader/Preloader.svelte";
import PackageCard from "$components/packages/package.svelte";
import { onMount } from "svelte";
import { packagesStore } from "$libs/stores";
export let pkg: Package;
export let pkg: Package;
let packages: GUIPackage[] = [];
let packages: GUIPackage[] = [];
onMount(async () => {
if (!packages.length) {
const matches = await packagesStore.search(pkg.desc, 4);
packages = matches.filter((mp) => mp.full_name !== pkg.full_name).slice(0, 3);
}
});
onMount(async () => {
if (!packages.length) {
const matches = await packagesStore.search(pkg.desc, 4);
packages = matches.filter((mp) => mp.full_name !== pkg.full_name).slice(0, 3);
}
});
</script>
<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>
<ul class="grid grid-cols-3 bg-black">
{#if packages.length > 0}
{#each packages as pkg}
<div class={pkg.state === PackageStates.INSTALLING ? "animate-pulse" : ""}>
<PackageCard {pkg} />
</div>
{/each}
{:else}
{#each Array(9) as _}
<section class="h-50 border-gray border p-4">
<Preloader />
</section>
{/each}
{/if}
{#if packages.length > 0}
{#each packages as pkg}
<div class={pkg.state === PackageStates.INSTALLING ? "animate-pulse" : ""}>
<PackageCard {pkg} />
</div>
{/each}
{:else}
{#each Array(9) as _}
<section class="h-50 border-gray border p-4">
<Preloader />
</section>
{/each}
{/if}
</ul>

View file

@ -1,84 +1,84 @@
<script lang="ts">
import { authStore, navStore } from "$libs/stores";
import { getSession } from "@native";
import { baseUrl } from "$libs/v1-client";
import { shellOpenExternal } from "@native";
import mouseLeaveDelay from "@tea/ui/lib/mouse-leave-delay";
const { user } = authStore;
import { authStore, navStore } from "$libs/stores";
import { getSession } from "@native";
import { baseUrl } from "$libs/v1-client";
import { shellOpenExternal } from "@native";
import mouseLeaveDelay from "@tea/ui/lib/mouse-leave-delay";
const { user } = authStore;
$: authenticating = false;
$: authenticating = false;
$: isLogoutOpen = false;
$: isLogoutOpen = false;
const openGithub = async () => {
if (!authenticating) {
authenticating = true;
try {
const session = await getSession();
const openGithub = async () => {
if (!authenticating) {
authenticating = true;
try {
const session = await getSession();
if (session && session.device_id) {
shellOpenExternal(`${baseUrl}/auth/user?device_id=${session.device_id}`);
authStore.pollSession();
} else {
throw new Error("possible no internet connection");
}
} catch (error) {
console.error(error);
} finally {
authenticating = false;
}
}
isLogoutOpen = false;
};
if (session && session.device_id) {
shellOpenExternal(`${baseUrl}/auth/user?device_id=${session.device_id}`);
authStore.pollSession();
} else {
throw new Error("possible no internet connection");
}
} catch (error) {
console.error(error);
} finally {
authenticating = false;
}
}
isLogoutOpen = false;
};
const logout = () => authStore.clearSession();
const logout = () => authStore.clearSession();
const preventDoubleClick = (evt: MouseEvent) => evt.stopPropagation();
const preventDoubleClick = (evt: MouseEvent) => evt.stopPropagation();
</script>
{#if $user}
<div class="relative" use:mouseLeaveDelay={2000} on:leave_delay={() => (isLogoutOpen = false)}>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<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
<div class="relative" use:mouseLeaveDelay={2000} on:leave_delay={() => (isLogoutOpen = false)}>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<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
hover:bg-[#e1e1e1] hover:text-black"
on:click={() => (isLogoutOpen = !isLogoutOpen)}
on:dblclick={preventDoubleClick}
>
<div class="text-gray line-clamp-1 mr-1 group-hover:text-black">@{$user?.login}</div>
<img
id="avatar"
class="flex rounded-sm"
src={$user?.avatar_url || "/images/bored-ape.png"}
alt="profile"
/>
</section>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<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
on:click={() => (isLogoutOpen = !isLogoutOpen)}
on:dblclick={preventDoubleClick}
>
<div class="text-gray line-clamp-1 mr-1 group-hover:text-black">@{$user?.login}</div>
<img
id="avatar"
class="flex rounded-sm"
src={$user?.avatar_url || "/images/bored-ape.png"}
alt="profile"
/>
</section>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<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
hover:bg-[#e1e1e1] hover:text-black"
class:invisible={!isLogoutOpen}
class:visible={isLogoutOpen}
on:click={logout}
>
log out
</div>
</div>
class:invisible={!isLogoutOpen}
class:visible={isLogoutOpen}
on:click={logout}
>
log out
</div>
</div>
{:else}
<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:animate-pulse={authenticating}
on:click={openGithub}
on:dblclick={preventDoubleClick}
>
log in
</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:animate-pulse={authenticating}
on:click={openGithub}
on:dblclick={preventDoubleClick}
>
log in
</button>
{/if}
<style>
#avatar {
padding: 1px;
height: 26px !important;
width: 26px !important;
}
#avatar {
padding: 1px;
height: 26px !important;
width: 26px !important;
}
</style>

View file

@ -1,18 +1,18 @@
<script lang="ts">
import { shellOpenExternal, submitLogs } from "@native";
import LoginButton from "./login-button.svelte";
import ButtonIcon from "@tea/ui/button-icon/button-icon.svelte";
import SettingsMenu from "$components/settings-menu/settings-menu.svelte";
import { shellOpenExternal, submitLogs } from "@native";
import LoginButton from "./login-button.svelte";
import ButtonIcon from "@tea/ui/button-icon/button-icon.svelte";
import SettingsMenu from "$components/settings-menu/settings-menu.svelte";
const submitBugReport = async () => {
const logId = await submitLogs();
const bugFormUrl = `https://airtable.com/shravDxWeNwwpPkFV?prefill_log_id=${logId}&hide_log_id=true`;
shellOpenExternal(bugFormUrl);
};
const submitBugReport = async () => {
const logId = await submitLogs();
const bugFormUrl = `https://airtable.com/shravDxWeNwwpPkFV?prefill_log_id=${logId}&hide_log_id=true`;
shellOpenExternal(bugFormUrl);
};
</script>
<div class="mr-1 flex h-full items-center justify-end gap-2 p-2">
<ButtonIcon icon="bug" helpText="report feedback" on:click={() => submitBugReport()} />
<SettingsMenu />
<LoginButton />
<ButtonIcon icon="bug" helpText="report feedback" on:click={() => submitBugReport()} />
<SettingsMenu />
<LoginButton />
</div>

View file

@ -1,96 +1,102 @@
<script lang="ts">
import { searchStore } from "$libs/stores";
import SearchInput from "@tea/ui/search-input/search-input.svelte";
import { navStore } from "$libs/stores";
import { t } from "$libs/translations";
import { searchStore } from "$libs/stores";
import SearchInput from "@tea/ui/search-input/search-input.svelte";
import { navStore } from "$libs/stores";
import { t } from "$libs/translations";
import TopBarMenu from "./top-bar-menu.svelte";
import { topbarDoubleClick } from "$libs/native-electron";
import TopBarMenu from "./top-bar-menu.svelte";
import { topbarDoubleClick } from "$libs/native-electron";
let { nextPath, prevPath } = navStore;
let { nextPath, prevPath } = navStore;
</script>
<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"
style="-webkit-app-region: drag"
on:dblclick={topbarDoubleClick}
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"
on:dblclick={topbarDoubleClick}
>
<ul class="text-gray flex h-10 gap-1 pl-20 align-middle items-center leading-10">
<a href="/?tab=all">
<div class="home-btn w-12 text-center text-2xl">
<i class="icon-tea-logo-iconasset-1" />
</div>
</a>
<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"
><i class="icon-arrow-left" /></button
>
<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"
><i class="icon-arrow-right" /></button
>
</ul>
<div class="relative w-1/3 px-2">
<SearchInput
class="border-gray h-9 w-full rounded-sm border"
size="small"
placeholder={$t("store-search-placeholder")}
onFocus={() => {
searchStore.searching.set(true);
}}
readonly={true}
/>
<ul class="text-gray flex h-10 items-center gap-1 pl-20 align-middle leading-10">
<a href="/?tab=all">
<div class="home-btn w-12 text-center text-2xl">
<i class="icon-tea-logo-iconasset-1" />
</div>
</a>
<p class="text-gray px-2">beta</p>
<button
on:click={navStore.back}
class:active={$prevPath}
class="hover:bg-gray h-[28px] rounded-sm px-2 pt-1 text-xs opacity-50 transition-all hover:text-black"
title="go back"><i class="icon-arrow-left" /></button
>
<button
on:click={navStore.next}
class:active={$nextPath}
class="hover:bg-gray h-[28px] rounded-sm px-2 pt-1 text-xs opacity-50 transition-all hover:text-black"
title="go forward"><i class="icon-arrow-right" /></button
>
</ul>
<div class="relative w-1/3 px-2">
<SearchInput
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
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"
>
<span class="text-lg"></span>
<span class="text-xs" style="font-size: smaller">K</span>
</kbd>
</div>
<TopBarMenu />
<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"
style="letter-spacing: 0.5pt"
>
<span class="text-lg"></span>
<span class="text-xs" style="font-size: smaller">K</span>
</kbd>
</div>
<TopBarMenu />
</header>
<style>
@tailwind base;
@tailwind components;
@tailwind utilities;
@tailwind base;
@tailwind components;
@tailwind utilities;
header {
background: rgba(26, 26, 26, 0.9);
backdrop-filter: blur(2px);
box-sizing: border-box;
}
.home-btn {
height: 46px;
width: 46px;
line-height: 46px;
padding-left: 3px;
background-size: cover;
background-position: center center;
background-image: url("/images/gradient-bg.png");
color: #222222;
}
header {
background: rgba(26, 26, 26, 0.9);
backdrop-filter: blur(2px);
box-sizing: border-box;
}
.home-btn {
height: 46px;
width: 46px;
line-height: 46px;
padding-left: 3px;
background-size: cover;
background-position: center center;
background-image: url("/images/gradient-bg.png");
color: #222222;
}
.home-btn:hover {
color: white;
}
.home-btn:hover {
color: white;
}
.home-btn:active {
color: #222222;
border: 2px solid #222222
}
.home-btn:active {
color: #222222;
border: 2px solid #222222;
}
p {
font-size: 10px;
}
ul button {
pointer-events: none;
}
p {
font-size: 10px;
}
ul button {
pointer-events: none;
}
ul button.active {
color: white;
pointer-events: all;
opacity: 1;
}
ul button.active {
color: white;
pointer-events: all;
opacity: 1;
}
</style>

View file

@ -1,37 +1,40 @@
<script lang="ts">
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 = () => {
authStore.updateSession({ hide_welcome: true });
}
};
</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()}>
<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>
<img class="object-contain" src="/images/welcome-bg.png" alt="welcome"/>
<img class="object-contain" src="/images/welcome-bg.png" alt="welcome" />
</figure>
<div class="flex-grow mt-20 px-12 relative">
<h1 class="text-primary text-4xl mb-4">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>
<div class="relative mt-20 flex-grow px-12">
<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>
<Button type="plain" color="secondary" class="w-7/12"
onClick={() => close()}
>
<Button type="plain" color="secondary" class="w-7/12" onClick={() => close()}>
START EXPLORING
</Button>
</div>
</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={() => {
close()
close();
}}
></button>
/>
</aside>
</section>
@ -39,13 +42,13 @@
section {
width: 100%;
height: 100vh;
background: rgba(0,0,0, 0.5);
background: rgba(0, 0, 0, 0.5);
}
article {
height: 472px;
width: 725px;
background: rgba(0,0,0, 0.8);
background: rgba(0, 0, 0, 0.8);
}
figure {
@ -54,4 +57,4 @@
img {
height: 100%;
}
</style>
</style>

View file

@ -3,47 +3,47 @@ import * as pub from "$env/static/public";
import { getSession } from "@native";
type DefaultMixpanelProps = {
device_id: string;
uid: string;
distinct_id: string;
locale: string;
gui_version: string;
device_id: string;
uid: string;
distinct_id: string;
locale: string;
gui_version: string;
};
let mixpanelDefaultData: DefaultMixpanelProps;
const getLocalSession = async (): Promise<DefaultMixpanelProps> => {
if (mixpanelDefaultData) return mixpanelDefaultData;
const session = await getSession();
mixpanelDefaultData = {
device_id: session?.device_id || "unregistered",
uid: session?.user?.developer_id || "unauthenticated",
distinct_id: session?.device_id || "unregistered",
locale: session?.locale || "unknown",
gui_version: pub.PUBLIC_VERSION
};
return mixpanelDefaultData;
if (mixpanelDefaultData) return mixpanelDefaultData;
const session = await getSession();
mixpanelDefaultData = {
device_id: session?.device_id || "unregistered",
uid: session?.user?.developer_id || "unauthenticated",
distinct_id: session?.device_id || "unregistered",
locale: session?.locale || "unknown",
gui_version: pub.PUBLIC_VERSION
};
return mixpanelDefaultData;
};
mixpanel.init(pub.PUBLIC_MIXPANEL_TOKEN, { debug: true });
enum AnalyticsAction {
install = "INSTALL_ACTION",
install_failed = "INSTALL_ACTION_FAILED",
search = "SEARCH_ACTION",
search_failed = "SEARCH_ACTION_FAILED"
install = "INSTALL_ACTION",
install_failed = "INSTALL_ACTION_FAILED",
search = "SEARCH_ACTION",
search_failed = "SEARCH_ACTION_FAILED"
}
const trackAction = (action: AnalyticsAction, data?: { [key: string]: any }) => {
getLocalSession()
.then((props) => {
mixpanel.track(action, {
...(data || {}),
...props
});
})
.catch((error) => {
// TODO: log remote error stream
console.error(error);
});
getLocalSession()
.then((props) => {
mixpanel.track(action, {
...(data || {}),
...props
});
})
.catch((error) => {
// TODO: log remote error stream
console.error(error);
});
};
/**
@ -52,7 +52,7 @@ const trackAction = (action: AnalyticsAction, data?: { [key: string]: any }) =>
* @returns void
*/
export const trackInstall = (packageFullname: string) =>
trackAction(AnalyticsAction.install, { pkg: packageFullname });
trackAction(AnalyticsAction.install, { pkg: packageFullname });
/**
* save failed installation event to mixpanel
@ -60,21 +60,21 @@ export const trackInstall = (packageFullname: string) =>
* @param error error message
*/
export const trackInstallFailed = (packageFullname: string, error: string) => {
trackAction(AnalyticsAction.install_failed, {
pkg: packageFullname,
error
});
trackAction(AnalyticsAction.install_failed, {
pkg: packageFullname,
error
});
};
export const trackSearch = (search_term: string, result_count: number) => {
if (result_count > 0) {
trackAction(AnalyticsAction.search, {
search_term,
result_count
});
} else {
trackAction(AnalyticsAction.search_failed, {
search_term
});
}
if (result_count > 0) {
trackAction(AnalyticsAction.search, {
search_term,
result_count
});
} else {
trackAction(AnalyticsAction.search_failed, {
search_term
});
}
};

View file

@ -2,57 +2,57 @@ import axios from "axios";
import type { Contributor, Package } from "@tea/ui/types";
import yaml from "js-yaml";
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(
owner: string,
repo: string
owner: string,
repo: string
): Promise<{ data: string; type: "md" | "rst" }> {
let type: "md" | "rst" = "md";
let data = "";
const req = await axios.get(`https://api.github.com/repos/${owner}/${repo}/readme`);
let type: "md" | "rst" = "md";
let data = "";
const req = await axios.get(`https://api.github.com/repos/${owner}/${repo}/readme`);
if (req.data?.download_url) {
type = req.data.name.endsWith(".rst") ? "rst" : "md";
const reqDl = await axios.get(req.data.download_url);
data = reqDl.data;
}
return { data, type };
if (req.data?.download_url) {
type = req.data.name.endsWith(".rst") ? "rst" : "md";
const reqDl = await axios.get(req.data.download_url);
data = reqDl.data;
}
return { data, type };
}
export async function getContributors(owner: string, repo: string): Promise<Contributor[]> {
// maintainer/repo
let contributors: Contributor[] = [];
const req = await axios.get(`https://api.github.com/repos/${owner}/${repo}/contributors`);
if (req.data) {
contributors = req.data.map((c: Contributor & { id: number }) => ({
login: c.login,
avatar_url: c.avatar_url,
name: c.name || "",
github_id: c.id,
contributions: c.contributions
}));
}
return contributors;
// maintainer/repo
let contributors: Contributor[] = [];
const req = await axios.get(`https://api.github.com/repos/${owner}/${repo}/contributors`);
if (req.data) {
contributors = req.data.map((c: Contributor & { id: number }) => ({
login: c.login,
avatar_url: c.avatar_url,
name: c.name || "",
github_id: c.id,
contributions: c.contributions
}));
}
return contributors;
}
export async function getRepoAsPackage(owner: string, repo: string): Promise<Partial<Package>> {
const req = await axios.get(`https://api.github.com/repos/${owner}/${repo}`);
const pkg: Partial<Package> = {};
if (req.data) {
pkg.license = req.data?.license?.name || "";
}
return pkg;
const req = await axios.get(`https://api.github.com/repos/${owner}/${repo}`);
const pkg: Partial<Package> = {};
if (req.data) {
pkg.license = req.data?.license?.name || "";
}
return pkg;
}
export const trimGithubSlug = (slug: string): string => {
const [owner, repo] = slug.split("/");
return [owner, repo].join("/");
const [owner, repo] = slug.split("/");
return [owner, repo].join("/");
};

View file

@ -4,7 +4,7 @@ import { captureException } from "./sentry";
// TODO: figure out how to detect if pkaged
const oldError = log.error;
log.error = (...params: any[]) => {
oldError(params);
captureException(params[0].message);
oldError(params);
captureException(params[0].message);
};
export default log;

View file

@ -13,12 +13,12 @@
import type { Package, Review, AirtablePost, Bottle } from "@tea/ui/types";
import {
type GUIPackage,
type DeviceAuth,
type Session,
AuthStatus,
type Packages,
type AutoUpdateStatus
type GUIPackage,
type DeviceAuth,
type Session,
AuthStatus,
type Packages,
type AutoUpdateStatus
} from "./types";
import * as mock from "./native-mock";
@ -31,270 +31,270 @@ import log from "./logger";
const { ipcRenderer, shell } = window.require("electron");
export async function getDistPackages(): Promise<Package[]> {
try {
return withRetry(async () => {
const req = await axios.get<Package[]>(
"https://s3.amazonaws.com/preview.gui.tea.xyz/packages.json"
);
log.info("packages received:", req.data.length);
return req.data;
});
} catch (error) {
log.error("getDistPackagesList:", error);
return [];
}
try {
return withRetry(async () => {
const req = await axios.get<Package[]>(
"https://s3.amazonaws.com/preview.gui.tea.xyz/packages.json"
);
log.info("packages received:", req.data.length);
return req.data;
});
} catch (error) {
log.error("getDistPackagesList:", error);
return [];
}
}
export async function getInstalledPackages(): Promise<InstalledPackage[]> {
let pkgs: InstalledPackage[] = [];
try {
log.info("getting installed packages");
pkgs = (await ipcRenderer.invoke("get-installed-packages")) as InstalledPackage[];
log.info("got installed packages:", pkgs.length);
} catch (error) {
log.error(error);
}
return pkgs;
let pkgs: InstalledPackage[] = [];
try {
log.info("getting installed packages");
pkgs = (await ipcRenderer.invoke("get-installed-packages")) as InstalledPackage[];
log.info("got installed packages:", pkgs.length);
} catch (error) {
log.error(error);
}
return pkgs;
}
export async function getPackages(): Promise<GUIPackage[]> {
const [packages, installedPackages] = await Promise.all([
getDistPackages(),
ipcRenderer.invoke("get-installed-packages") as InstalledPackage[]
]);
const [packages, installedPackages] = await Promise.all([
getDistPackages(),
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
// --- it has noticeable slowness
log.info(`native: installed ${installedPackages.length} out of ${(packages || []).length}`);
return (packages || []).map((pkg) => {
const installedPkg = installedPackages.find((p) => p.full_name === pkg.full_name);
return {
...pkg,
state: installedPkg ? PackageStates.INSTALLED : PackageStates.AVAILABLE,
installed_versions: installedPkg?.installed_versions || []
};
});
// 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
log.info(`native: installed ${installedPackages.length} out of ${(packages || []).length}`);
return (packages || []).map((pkg) => {
const installedPkg = installedPackages.find((p) => p.full_name === pkg.full_name);
return {
...pkg,
state: installedPkg ? PackageStates.INSTALLED : PackageStates.AVAILABLE,
installed_versions: installedPkg?.installed_versions || []
};
});
}
export async function getFeaturedPackages(): Promise<Package[]> {
const packages = await mock.getFeaturedPackages();
return packages;
const packages = await mock.getFeaturedPackages();
return packages;
}
export async function getPackageReviews(full_name: string): Promise<Review[]> {
console.log(`getting reviews for ${full_name}`);
const reviews: Review[] =
(await apiGet<Review[]>(`packages/${full_name.replaceAll("/", ":")}/reviews`)) ?? [];
console.log(`getting reviews for ${full_name}`);
const reviews: Review[] =
(await apiGet<Review[]>(`packages/${full_name.replaceAll("/", ":")}/reviews`)) ?? [];
return reviews;
return reviews;
}
export async function installPackage(pkg: GUIPackage, version?: string) {
const latestVersion = pkg.version;
const specificVersion = version || latestVersion;
const latestVersion = pkg.version;
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", {
full_name: pkg.full_name,
version: specificVersion
});
const res = await ipcRenderer.invoke("install-package", {
full_name: pkg.full_name,
version: specificVersion
});
if (res instanceof Error) {
throw res;
}
if (res instanceof Error) {
throw res;
}
}
export async function syncPantry() {
const res = await ipcRenderer.invoke("sync-pantry");
if (res instanceof Error) {
throw res;
}
const res = await ipcRenderer.invoke("sync-pantry");
if (res instanceof Error) {
throw res;
}
}
export async function getTopPackages(): Promise<GUIPackage[]> {
const packages = await mock.getTopPackages();
return packages;
const packages = await mock.getTopPackages();
return packages;
}
export async function getAllPosts(tag?: string): Promise<AirtablePost[]> {
// add filter here someday: tag = news | course
try {
const queryParams = {
...(tag ? { tag } : {}),
nocache: "true"
};
const posts = await apiGet<AirtablePost[]>("posts", queryParams);
return posts || [];
} catch (error) {
log.error(error);
return [];
}
// add filter here someday: tag = news | course
try {
const queryParams = {
...(tag ? { tag } : {}),
nocache: "true"
};
const posts = await apiGet<AirtablePost[]>("posts", queryParams);
return posts || [];
} catch (error) {
log.error(error);
return [];
}
}
export async function getDeviceAuth(deviceId: string): Promise<DeviceAuth> {
let auth: DeviceAuth = {
status: AuthStatus.UNKNOWN,
key: ""
};
try {
const data = await apiGet<DeviceAuth>(`/auth/device/${deviceId}`);
if (data) auth = data;
} catch (error) {
log.error(error);
auth = await getDeviceAuth(deviceId);
}
return auth;
let auth: DeviceAuth = {
status: AuthStatus.UNKNOWN,
key: ""
};
try {
const data = await apiGet<DeviceAuth>(`/auth/device/${deviceId}`);
if (data) auth = data;
} catch (error) {
log.error(error);
auth = await getDeviceAuth(deviceId);
}
return auth;
}
export async function getPackageBottles(packageName: string): Promise<Bottle[]> {
try {
return withRetry(async () => {
const pkg = await apiGet<Package>(`packages/${packageName.replaceAll("/", ":")}`);
log.info(`got ${pkg?.bottles?.length || 0} bottles for ${packageName}`);
return (pkg && pkg.bottles) || [];
});
} catch (error) {
log.error("getPackageBottles:", error);
return [];
}
try {
return withRetry(async () => {
const pkg = await apiGet<Package>(`packages/${packageName.replaceAll("/", ":")}`);
log.info(`got ${pkg?.bottles?.length || 0} bottles for ${packageName}`);
return (pkg && pkg.bottles) || [];
});
} catch (error) {
log.error("getPackageBottles:", error);
return [];
}
}
export async function getPackage(packageName: string): Promise<Partial<Package>> {
try {
return await withRetry(async () => {
const data = await apiGet<Partial<Package>>(`packages/${packageName.replaceAll("/", ":")}`);
if (data) {
return data;
} else {
throw new Error(`package:${packageName} not found`);
}
});
} catch (error) {
log.error("getPackage:", error);
return {};
}
try {
return await withRetry(async () => {
const data = await apiGet<Partial<Package>>(`packages/${packageName.replaceAll("/", ":")}`);
if (data) {
return data;
} else {
throw new Error(`package:${packageName} not found`);
}
});
} catch (error) {
log.error("getPackage:", error);
return {};
}
}
export const getSession = async (): Promise<Session | null> => {
try {
const session = await ipcRenderer.invoke("get-session");
if (!session) throw new Error("no session found");
return session;
} catch (error) {
log.error(error);
return null;
}
try {
const session = await ipcRenderer.invoke("get-session");
if (!session) throw new Error("no session found");
return session;
} catch (error) {
log.error(error);
return null;
}
};
export const updateSession = async (session: Partial<Session>) => {
try {
await ipcRenderer.invoke("update-session", session);
} catch (error) {
log.error(error);
}
try {
await ipcRenderer.invoke("update-session", session);
} catch (error) {
log.error(error);
}
};
export const openPackageEntrypointInTerminal = (pkg: string) => {
try {
ipcRenderer.invoke("open-terminal", { pkg });
} catch (error) {
log.error(error);
}
try {
ipcRenderer.invoke("open-terminal", { pkg });
} catch (error) {
log.error(error);
}
};
export const shellOpenExternal = (link?: string) => {
if (!link) {
return;
}
shell.openExternal(link);
if (!link) {
return;
}
shell.openExternal(link);
};
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 getProtocolPath = async (): Promise<string> => {
const path = await ipcRenderer.invoke("get-protocol-path");
return path;
const path = await ipcRenderer.invoke("get-protocol-path");
return path;
};
export const submitLogs = async (): Promise<string> => {
const response = await ipcRenderer.invoke("submit-logs");
return response;
const response = await ipcRenderer.invoke("submit-logs");
return response;
};
export const isPackageInstalled = async (fullName: string, version?: string): Promise<boolean> => {
let isInstalled = false;
const pkgs = await getInstalledPackages();
const pkg = pkgs.find((p) => p.full_name === fullName);
let isInstalled = false;
const pkgs = await getInstalledPackages();
const pkg = pkgs.find((p) => p.full_name === fullName);
if (pkg) {
isInstalled = true;
if (version) {
isInstalled = pkg.installed_versions.includes(version);
}
}
if (pkg) {
isInstalled = true;
if (version) {
isInstalled = pkg.installed_versions.includes(version);
}
}
return isInstalled;
return isInstalled;
};
export const setBadgeCount = async (count: number) => {
try {
await ipcRenderer.invoke("set-badge-count", { count });
} catch (error) {
log.error(error);
}
try {
await ipcRenderer.invoke("set-badge-count", { count });
} catch (error) {
log.error(error);
}
};
export const deletePackage = async (args: { fullName: string; version: string }) => {
const result = await ipcRenderer.invoke("delete-package", args);
if (result instanceof Error) {
throw result;
}
const result = await ipcRenderer.invoke("delete-package", args);
if (result instanceof Error) {
throw result;
}
};
export const loadPackageCache = async () => {
try {
return await ipcRenderer.invoke("load-package-cache");
} catch (error) {
log.error(error);
}
try {
return await ipcRenderer.invoke("load-package-cache");
} catch (error) {
log.error(error);
}
};
export const writePackageCache = async (pkgs: Packages) => {
try {
await ipcRenderer.invoke("write-package-cache", pkgs);
} catch (error) {
log.error(error);
}
try {
await ipcRenderer.invoke("write-package-cache", pkgs);
} catch (error) {
log.error(error);
}
};
export const topbarDoubleClick = async () => {
try {
ipcRenderer.invoke("topbar-double-click");
} catch (error) {
log.error(error);
}
try {
ipcRenderer.invoke("topbar-double-click");
} catch (error) {
log.error(error);
}
};
export const cacheImageURL = async (url: string): Promise<string | undefined> => {
if (!url) return "";
try {
const cachedSrc = await ipcRenderer.invoke("cache-image", url);
return cachedSrc;
} catch (error) {
log.error("Failed to cache image:", error);
}
if (!url) return "";
try {
const cachedSrc = await ipcRenderer.invoke("cache-image", url);
return cachedSrc;
} catch (error) {
log.error("Failed to cache image:", error);
}
};
export const getAutoUpdateStatus = async (): Promise<AutoUpdateStatus> => {
try {
return await ipcRenderer.invoke("get-auto-update-status");
} catch (error) {
log.error(error);
return { status: "up-to-date" };
}
try {
return await ipcRenderer.invoke("get-auto-update-status");
} catch (error) {
log.error(error);
return { status: "up-to-date" };
}
};

View file

@ -15,399 +15,399 @@ import _ from "lodash";
import * as v1Client from "$libs/v1-client";
const packages: Package[] = [
{
slug: "mesonbuild_com",
homepage: "https://mesonbuild.com",
name: "mesonbuild.com",
version: "0.63.3",
last_modified: "2022-10-06T15:45:08.000Z",
full_name: "mesonbuild.com",
dl_count: 270745,
thumb_image_name: "mesonbuild_com_option 1.jpg ",
maintainer: "",
desc: "Fast and user friendly build system",
thumb_image_url: "https://tea.xyz/Images/packages/mesonbuild_com.jpg",
installs: 0,
categories: ["foundation_essentials"],
created: "2022-10-06T15:45:08.000Z",
manual_sorting: 0,
card_layout: "bottom"
},
{
slug: "pixman_org",
homepage: "http://www.pixman.org/",
maintainer: "freedesktop",
name: "pixman.org",
version: "0.40.0",
last_modified: "2022-09-26T19:37:47.000Z",
full_name: "pixman.org",
dl_count: 0,
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.",
thumb_image_url: "https://tea.xyz/Images/packages/pixman_org.jpg",
installs: 0,
categories: ["foundation_essentials"],
created: "2022-09-26T19:37:47.000Z",
manual_sorting: 1,
card_layout: "bottom"
},
{
slug: "freedesktop_org_pkg_config",
homepage: "https://freedesktop.org",
maintainer: "freedesktop.org",
name: "pkg-config",
version: "0.29.2",
last_modified: "2022-10-20T01:32:15.000Z",
full_name: "freedesktop.org/pkg-config",
dl_count: 2661501,
thumb_image_name: "freedecktop_org_pkg_config option 1.jpg ",
desc: "Manage compile and link flags for libraries",
thumb_image_url: "https://tea.xyz/Images/packages/freedesktop_org_pkg_config.jpg",
installs: 0,
categories: ["foundation_essentials"],
created: "2022-10-20T01:32:15.000Z",
manual_sorting: 2,
card_layout: "bottom"
},
{
slug: "gnu_org_gettext",
homepage: "https://gnu.org",
maintainer: "gnu.org",
name: "gettext",
version: "0.21.1",
last_modified: "2022-10-20T01:23:46.000Z",
full_name: "gnu.org/gettext",
dl_count: 3715970,
thumb_image_name: "gnu_org_gettext_option 1.jpg ",
desc: "GNU internationalization (i18n) and localization (l10n) library",
thumb_image_url: "https://tea.xyz/Images/packages/gnu_org_gettext.jpg",
installs: 0,
categories: ["foundation_essentials"],
created: "2022-10-20T01:23:46.000Z",
manual_sorting: 3,
card_layout: "bottom"
},
{
slug: "ipfs_tech",
homepage: "https://ipfs.tech",
name: "ipfs.tech",
version: "0.16.0",
last_modified: "2022-10-19T21:36:52.000Z",
full_name: "ipfs.tech",
dl_count: 14457,
thumb_image_name: "ipfs_tech_option 2.jpg ",
maintainer: "",
desc: "Peer-to-peer hypermedia protocol",
thumb_image_url: "https://tea.xyz/Images/packages/ipfs_tech.jpg",
installs: 0,
categories: ["foundation_essentials"],
created: "2022-10-19T21:36:52.000Z",
manual_sorting: 4,
card_layout: "bottom"
},
{
slug: "nixos_org_patchelf",
homepage: "https://nixos.org",
maintainer: "nixos.org",
name: "patchelf",
version: "0.15.0",
last_modified: "2022-09-27T04:50:44.000Z",
full_name: "nixos.org/patchelf",
dl_count: 0,
thumb_image_name: "nixos_org_patchelf_option 1.jpg ",
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",
installs: 0,
categories: ["top_packages", "foundation_essentials"],
created: "2022-09-27T04:50:44.000Z",
manual_sorting: 5,
card_layout: "bottom"
},
{
slug: "tea_xyz",
homepage: "https://tea.xyz",
maintainer: "tea.xyz",
name: "tea.xyz",
version: "0.8.6",
last_modified: "2022-10-19T19:13:51.000Z",
full_name: "tea.xyz",
dl_count: 0,
thumb_image_name: "tea_xyz_option 2.jpg ",
desc: "Website of tea.xyz",
thumb_image_url: "https://tea.xyz/Images/packages/tea_xyz.jpg",
installs: 0,
categories: ["top_packages", "foundation_essentials"],
created: "2022-10-19T19:13:51.000Z",
manual_sorting: 6,
card_layout: "bottom"
},
{
slug: "charm_sh_gum",
homepage: "https://charm.sh",
maintainer: "charm.sh",
name: "gum",
version: "0.8.0",
last_modified: "2022-10-21T02:15:16.000Z",
full_name: "charm.sh/gum",
dl_count: 0,
thumb_image_name: "charm_sh_gum.jpg ",
desc: "",
thumb_image_url: "https://tea.xyz/Images/packages/charm_sh_gum.jpg",
installs: 0,
categories: ["top_packages", "foundation_essentials"],
created: "2022-10-21T02:15:16.000Z",
manual_sorting: 7,
card_layout: "bottom"
},
{
slug: "pyyaml_org",
homepage: "https://pyyaml.org",
name: "pyyaml.org",
version: "0.2.5",
last_modified: "2022-10-03T15:35:14.000Z",
full_name: "pyyaml.org",
dl_count: 107505,
thumb_image_name: "pyyaml_org_option 1.jpg ",
maintainer: "",
desc: "YAML framework for Python",
thumb_image_url: "https://tea.xyz/Images/packages/pyyaml_org.jpg",
installs: 0,
categories: ["top_packages", "foundation_essentials"],
created: "2022-10-03T15:35:14.000Z",
manual_sorting: 8,
card_layout: "bottom"
},
{
slug: "tea_xyz_gx_cc",
homepage: "https://tea.xyz",
maintainer: "tea.xyz",
name: "cc",
version: "0.1.0",
last_modified: "2022-10-19T16:47:44.000Z",
full_name: "tea.xyz/gx/cc",
dl_count: 0,
thumb_image_name: "tea_xyz_gx.jpg ",
desc: "",
thumb_image_url: "https://tea.xyz/Images/packages/tea_xyz_gx_cc.jpg",
installs: 0,
categories: ["top_packages", "foundation_essentials"],
created: "2022-10-19T16:47:44.000Z",
manual_sorting: 9,
card_layout: "bottom"
}
{
slug: "mesonbuild_com",
homepage: "https://mesonbuild.com",
name: "mesonbuild.com",
version: "0.63.3",
last_modified: "2022-10-06T15:45:08.000Z",
full_name: "mesonbuild.com",
dl_count: 270745,
thumb_image_name: "mesonbuild_com_option 1.jpg ",
maintainer: "",
desc: "Fast and user friendly build system",
thumb_image_url: "https://tea.xyz/Images/packages/mesonbuild_com.jpg",
installs: 0,
categories: ["foundation_essentials"],
created: "2022-10-06T15:45:08.000Z",
manual_sorting: 0,
card_layout: "bottom"
},
{
slug: "pixman_org",
homepage: "http://www.pixman.org/",
maintainer: "freedesktop",
name: "pixman.org",
version: "0.40.0",
last_modified: "2022-09-26T19:37:47.000Z",
full_name: "pixman.org",
dl_count: 0,
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.",
thumb_image_url: "https://tea.xyz/Images/packages/pixman_org.jpg",
installs: 0,
categories: ["foundation_essentials"],
created: "2022-09-26T19:37:47.000Z",
manual_sorting: 1,
card_layout: "bottom"
},
{
slug: "freedesktop_org_pkg_config",
homepage: "https://freedesktop.org",
maintainer: "freedesktop.org",
name: "pkg-config",
version: "0.29.2",
last_modified: "2022-10-20T01:32:15.000Z",
full_name: "freedesktop.org/pkg-config",
dl_count: 2661501,
thumb_image_name: "freedecktop_org_pkg_config option 1.jpg ",
desc: "Manage compile and link flags for libraries",
thumb_image_url: "https://tea.xyz/Images/packages/freedesktop_org_pkg_config.jpg",
installs: 0,
categories: ["foundation_essentials"],
created: "2022-10-20T01:32:15.000Z",
manual_sorting: 2,
card_layout: "bottom"
},
{
slug: "gnu_org_gettext",
homepage: "https://gnu.org",
maintainer: "gnu.org",
name: "gettext",
version: "0.21.1",
last_modified: "2022-10-20T01:23:46.000Z",
full_name: "gnu.org/gettext",
dl_count: 3715970,
thumb_image_name: "gnu_org_gettext_option 1.jpg ",
desc: "GNU internationalization (i18n) and localization (l10n) library",
thumb_image_url: "https://tea.xyz/Images/packages/gnu_org_gettext.jpg",
installs: 0,
categories: ["foundation_essentials"],
created: "2022-10-20T01:23:46.000Z",
manual_sorting: 3,
card_layout: "bottom"
},
{
slug: "ipfs_tech",
homepage: "https://ipfs.tech",
name: "ipfs.tech",
version: "0.16.0",
last_modified: "2022-10-19T21:36:52.000Z",
full_name: "ipfs.tech",
dl_count: 14457,
thumb_image_name: "ipfs_tech_option 2.jpg ",
maintainer: "",
desc: "Peer-to-peer hypermedia protocol",
thumb_image_url: "https://tea.xyz/Images/packages/ipfs_tech.jpg",
installs: 0,
categories: ["foundation_essentials"],
created: "2022-10-19T21:36:52.000Z",
manual_sorting: 4,
card_layout: "bottom"
},
{
slug: "nixos_org_patchelf",
homepage: "https://nixos.org",
maintainer: "nixos.org",
name: "patchelf",
version: "0.15.0",
last_modified: "2022-09-27T04:50:44.000Z",
full_name: "nixos.org/patchelf",
dl_count: 0,
thumb_image_name: "nixos_org_patchelf_option 1.jpg ",
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",
installs: 0,
categories: ["top_packages", "foundation_essentials"],
created: "2022-09-27T04:50:44.000Z",
manual_sorting: 5,
card_layout: "bottom"
},
{
slug: "tea_xyz",
homepage: "https://tea.xyz",
maintainer: "tea.xyz",
name: "tea.xyz",
version: "0.8.6",
last_modified: "2022-10-19T19:13:51.000Z",
full_name: "tea.xyz",
dl_count: 0,
thumb_image_name: "tea_xyz_option 2.jpg ",
desc: "Website of tea.xyz",
thumb_image_url: "https://tea.xyz/Images/packages/tea_xyz.jpg",
installs: 0,
categories: ["top_packages", "foundation_essentials"],
created: "2022-10-19T19:13:51.000Z",
manual_sorting: 6,
card_layout: "bottom"
},
{
slug: "charm_sh_gum",
homepage: "https://charm.sh",
maintainer: "charm.sh",
name: "gum",
version: "0.8.0",
last_modified: "2022-10-21T02:15:16.000Z",
full_name: "charm.sh/gum",
dl_count: 0,
thumb_image_name: "charm_sh_gum.jpg ",
desc: "",
thumb_image_url: "https://tea.xyz/Images/packages/charm_sh_gum.jpg",
installs: 0,
categories: ["top_packages", "foundation_essentials"],
created: "2022-10-21T02:15:16.000Z",
manual_sorting: 7,
card_layout: "bottom"
},
{
slug: "pyyaml_org",
homepage: "https://pyyaml.org",
name: "pyyaml.org",
version: "0.2.5",
last_modified: "2022-10-03T15:35:14.000Z",
full_name: "pyyaml.org",
dl_count: 107505,
thumb_image_name: "pyyaml_org_option 1.jpg ",
maintainer: "",
desc: "YAML framework for Python",
thumb_image_url: "https://tea.xyz/Images/packages/pyyaml_org.jpg",
installs: 0,
categories: ["top_packages", "foundation_essentials"],
created: "2022-10-03T15:35:14.000Z",
manual_sorting: 8,
card_layout: "bottom"
},
{
slug: "tea_xyz_gx_cc",
homepage: "https://tea.xyz",
maintainer: "tea.xyz",
name: "cc",
version: "0.1.0",
last_modified: "2022-10-19T16:47:44.000Z",
full_name: "tea.xyz/gx/cc",
dl_count: 0,
thumb_image_name: "tea_xyz_gx.jpg ",
desc: "",
thumb_image_url: "https://tea.xyz/Images/packages/tea_xyz_gx_cc.jpg",
installs: 0,
categories: ["top_packages", "foundation_essentials"],
created: "2022-10-19T16:47:44.000Z",
manual_sorting: 9,
card_layout: "bottom"
}
];
export const getInstalledPackages = () => [];
export async function getDistPackages(): Promise<Package[]> {
return packages;
return packages;
}
export async function getPackages(): Promise<GUIPackage[]> {
return packages.map((pkg) => {
return {
...pkg,
state: PackageStates.AVAILABLE
};
});
return packages.map((pkg) => {
return {
...pkg,
state: PackageStates.AVAILABLE
};
});
}
export async function getFeaturedPackages(): Promise<Package[]> {
await delay(2000);
return packages.slice(0, 4);
await delay(2000);
return packages.slice(0, 4);
}
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 reviews: Review[] = [];
const reviewCount = _.random(9, 21);
const reviews: Review[] = [];
for (let i = 0; i < reviewCount; i++) {
const title = loremIpsum({
count: _.random(2, 5),
format: "plain",
paragraphLowerBound: 3,
paragraphUpperBound: 7,
random: Math.random,
sentenceLowerBound: 5,
sentenceUpperBound: 15,
units: "words"
});
const comment = loremIpsum({
count: 2,
format: "plain",
paragraphLowerBound: 3,
paragraphUpperBound: 7,
random: Math.random,
sentenceLowerBound: 5,
sentenceUpperBound: 15,
units: "sentences"
});
const rating = _.random(0, 5);
reviews.push({
title,
comment,
rating
});
}
for (let i = 0; i < reviewCount; i++) {
const title = loremIpsum({
count: _.random(2, 5),
format: "plain",
paragraphLowerBound: 3,
paragraphUpperBound: 7,
random: Math.random,
sentenceLowerBound: 5,
sentenceUpperBound: 15,
units: "words"
});
const comment = loremIpsum({
count: 2,
format: "plain",
paragraphLowerBound: 3,
paragraphUpperBound: 7,
random: Math.random,
sentenceLowerBound: 5,
sentenceUpperBound: 15,
units: "sentences"
});
const rating = _.random(0, 5);
reviews.push({
title,
comment,
rating
});
}
await delay(2000);
return reviews;
await delay(2000);
return reviews;
}
export async function installPackage(pkg: GUIPackage, version?: string) {
console.log("installing: ", pkg.full_name, version);
await delay(10000);
console.log("installing: ", pkg.full_name, version);
await delay(10000);
}
export async function syncPantry() {
console.log("syncing pantry");
await delay(1000);
console.log("syncing pantry");
await delay(1000);
}
function delay(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
return new Promise((resolve) => setTimeout(resolve, ms));
}
export async function getTopPackages(): Promise<GUIPackage[]> {
await delay(500);
return packages.slice(0, 9).map((pkg) => {
return {
...pkg,
state: PackageStates.AVAILABLE
};
});
await delay(500);
return packages.slice(0, 9).map((pkg) => {
return {
...pkg,
state: PackageStates.AVAILABLE
};
});
}
export async function getAllPosts(type: string): Promise<AirtablePost[]> {
console.log("filter by type:", type);
const posts: AirtablePost[] = [
{
airtable_record_id: "a",
link: "https://google.com",
title: "Tea Inc releases game changing api!",
sub_title: "lorem ipsum dolor sit amet",
short_description: "lorem ipsum dolor sit amet",
thumb_image_url: "/images/bored-ape.png",
thumb_image_name: "borred-api.png",
created_at: new Date(),
updated_at: new Date(),
published_at: new Date(),
tags: ["news"]
},
{
airtable_record_id: "b",
link: "https://google.com",
title: "Bored Ape not bored anymore",
sub_title: "lorem ipsum dolor sit amet",
short_description: "lorem ipsum dolor sit amet",
thumb_image_url: "/images/bored-ape.png",
thumb_image_name: "borred-api.png",
created_at: new Date(),
updated_at: new Date(),
published_at: new Date(),
tags: ["news"]
},
{
airtable_record_id: "c",
link: "https://google.com",
title: "Markdown can be executed! hoohah!",
sub_title: "lorem ipsum dolor sit amet",
short_description: "lorem ipsum dolor sit amet",
thumb_image_url: "/images/bored-ape.png",
thumb_image_name: "borred-api.png",
created_at: new Date(),
updated_at: new Date(),
published_at: new Date(),
tags: ["news"]
}
];
console.log("filter by type:", type);
const posts: AirtablePost[] = [
{
airtable_record_id: "a",
link: "https://google.com",
title: "Tea Inc releases game changing api!",
sub_title: "lorem ipsum dolor sit amet",
short_description: "lorem ipsum dolor sit amet",
thumb_image_url: "/images/bored-ape.png",
thumb_image_name: "borred-api.png",
created_at: new Date(),
updated_at: new Date(),
published_at: new Date(),
tags: ["news"]
},
{
airtable_record_id: "b",
link: "https://google.com",
title: "Bored Ape not bored anymore",
sub_title: "lorem ipsum dolor sit amet",
short_description: "lorem ipsum dolor sit amet",
thumb_image_url: "/images/bored-ape.png",
thumb_image_name: "borred-api.png",
created_at: new Date(),
updated_at: new Date(),
published_at: new Date(),
tags: ["news"]
},
{
airtable_record_id: "c",
link: "https://google.com",
title: "Markdown can be executed! hoohah!",
sub_title: "lorem ipsum dolor sit amet",
short_description: "lorem ipsum dolor sit amet",
thumb_image_url: "/images/bored-ape.png",
thumb_image_name: "borred-api.png",
created_at: new Date(),
updated_at: new Date(),
published_at: new Date(),
tags: ["news"]
}
];
return posts;
return posts;
}
export async function getDeviceAuth(deviceId: string): Promise<any> {
const data = await v1Client.get<any>(`/auth/device/${deviceId}`);
return data;
const data = await v1Client.get<any>(`/auth/device/${deviceId}`);
return data;
}
export async function getPackageBottles(name: string): Promise<Bottle[]> {
return [
{ 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: "x86-64", version: "3.39.4", 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.40.0", 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 }
];
return [
{ 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: "x86-64", version: "3.39.4", 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.40.0", 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 }
];
}
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> => {
return null;
return null;
};
export const updateSession = async (session: Partial<Session>) => {
console.log(session);
console.log(session);
};
export const openTerminal = (cmd: string) => console.log(cmd);
export const shellOpenExternal = (link?: string) => {
window.open(link, "_blank");
window.open(link, "_blank");
};
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 = () => {
console.log("relaunch");
console.log("relaunch");
};
export const getProtocolPath = async (): Promise<string> => "";
export const submitLogs = async (): Promise<string> => {
return "deviceId---logid";
return "deviceId---logid";
};
export const isPackageInstalled = async (_v?: string): Promise<boolean> => {
return true;
return true;
};
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 }) => {
console.log("delete package", args);
console.log("delete package", args);
};
export const loadPackageCache = async () => {
return { version: "1", packages: {} };
return { version: "1", packages: {} };
};
export const writePackageCache = async (pkgs: Packages) => {
console.log("write package cache", pkgs);
console.log("write package cache", pkgs);
};
export const topbarDoubleClick = async () => {
console.log("topbar double click");
console.log("topbar double click");
};
export const cacheImageURL = async (_url: string): Promise<string | undefined> => {
return undefined;
return undefined;
};
export const getAutoUpdateStatus = async (): Promise<AutoUpdateStatus> => {
return { status: "up-to-date" };
return { status: "up-to-date" };
};
export async function openPackageEntrypointInTerminal(pkg: string) {
//noop
//noop
}

View file

@ -1,49 +1,49 @@
import { getPkgBottles } from "../tea-dir";
describe("tea-dir module", () => {
it("should getPkgBottles from nested Dir object/s", () => {
const results = getPkgBottles({
name: "kkos",
path: "/Users/x/.tea/github.com/kkos",
children: [
{ name: ".DS_Store", path: "/Users/x/.tea/github.com/kkos/.DS_Store" },
{
name: "oniguruma",
path: "/Users/x/.tea/github.com/kkos/oniguruma",
children: [
{ name: ".DS_Store", path: "/Users/x/.tea/github.com/kkos/oniguruma/.DS_Store" },
{
path: "/Users/x/.tea/github.com/kkos/oniguruma/v6",
name: "v6",
children: [
{ name: ".DS_Store", path: "/Users/x/.tea/github.com/kkos/oniguruma/v6/.DS_Store" }
]
},
{
name: "v*",
path: "/Users/x/.tea/github.com/kkos/oniguruma/v*",
children: []
},
{
name: "v6.9.8",
path: "/Users/x/.tea/github.com/kkos/oniguruma/v6.9.8",
children: []
},
{
name: "v6.9",
path: "/Users/x/.tea/github.com/kkos/oniguruma/v6.9",
children: []
}
]
}
]
});
it("should getPkgBottles from nested Dir object/s", () => {
const results = getPkgBottles({
name: "kkos",
path: "/Users/x/.tea/github.com/kkos",
children: [
{ name: ".DS_Store", path: "/Users/x/.tea/github.com/kkos/.DS_Store" },
{
name: "oniguruma",
path: "/Users/x/.tea/github.com/kkos/oniguruma",
children: [
{ name: ".DS_Store", path: "/Users/x/.tea/github.com/kkos/oniguruma/.DS_Store" },
{
path: "/Users/x/.tea/github.com/kkos/oniguruma/v6",
name: "v6",
children: [
{ name: ".DS_Store", path: "/Users/x/.tea/github.com/kkos/oniguruma/v6/.DS_Store" }
]
},
{
name: "v*",
path: "/Users/x/.tea/github.com/kkos/oniguruma/v*",
children: []
},
{
name: "v6.9.8",
path: "/Users/x/.tea/github.com/kkos/oniguruma/v6.9.8",
children: []
},
{
name: "v6.9",
path: "/Users/x/.tea/github.com/kkos/oniguruma/v6.9",
children: []
}
]
}
]
});
expect(results).toEqual([
"github.com/kkos/oniguruma/v*",
"github.com/kkos/oniguruma/v6",
"github.com/kkos/oniguruma/v6.9",
"github.com/kkos/oniguruma/v6.9.8"
]);
});
expect(results).toEqual([
"github.com/kkos/oniguruma/v*",
"github.com/kkos/oniguruma/v6",
"github.com/kkos/oniguruma/v6.9",
"github.com/kkos/oniguruma/v6.9.8"
]);
});
});

View file

@ -3,30 +3,30 @@
// import { join } from 'upath';
type Dir = {
name: string;
path: string;
children?: Dir[];
name: string;
path: string;
children?: Dir[];
};
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[] => {
const bottles: string[] = [];
const bottles: string[] = [];
const pkg = packageDir.path.split(".tea/")[1];
const version = pkg.split("/v")[1];
const pkg = packageDir.path.split(".tea/")[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) {
bottles.push(pkg);
} else if (packageDir?.children?.length) {
const childBottles = packageDir.children
.map(getPkgBottles)
.reduce((arr, bottles) => [...arr, ...bottles], []);
bottles.push(...childBottles);
}
if (version && isVersion) {
bottles.push(pkg);
} else if (packageDir?.children?.length) {
const childBottles = packageDir.children
.map(getPkgBottles)
.reduce((arr, bottles) => [...arr, ...bottles], []);
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
export const findAvailableVersions = (pkg: GUIPackage) => {
// default to just showing the latest if bottles haven't loaded yet
if (!pkg.bottles) {
return [pkg.version];
}
// default to just showing the latest if bottles haven't loaded yet
if (!pkg.bottles) {
return [pkg.version];
}
const versionSet = new Set<string>();
for (const b of pkg.bottles) {
versionSet.add(b.version);
}
const versionSet = new Set<string>();
for (const b of pkg.bottles) {
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";
// Add a new version to the list of installed versions while maintaining the sort order
export const addInstalledVersion = (
installedVersions: string[] | undefined,
newVersion: string
installedVersions: string[] | undefined,
newVersion: string
) => {
if (!installedVersions) {
return [newVersion];
}
if (!installedVersions) {
return [newVersion];
}
return [...installedVersions, newVersion].sort((a, b) =>
semverCompare(cleanVersion(b), cleanVersion(a))
);
return [...installedVersions, newVersion].sort((a, b) =>
semverCompare(cleanVersion(b), cleanVersion(a))
);
};
export const findRecentInstalledVersion = (pkg: GUIPackage) => {
// this assumes that the versions are already sorted
return pkg.installed_versions?.[0];
// this assumes that the versions are already sorted
return pkg.installed_versions?.[0];
};
export const isInstalling = (pkg: GUIPackage) => {
return (
pkg.install_progress_percentage &&
pkg.install_progress_percentage > 0 &&
pkg.install_progress_percentage < 100
);
return (
pkg.install_progress_percentage &&
pkg.install_progress_percentage > 0 &&
pkg.install_progress_percentage < 100
);
};

View file

@ -2,20 +2,20 @@ import * as Sentry from "@sentry/browser";
import type { Session } from "./types";
export function initSentry(session?: Session) {
Sentry.init({
dsn: "https://5ff29bb5b3b64cd4bd4f4960ef1db2e3@o4504750197899264.ingest.sentry.io/4504750206746624"
});
if (session) {
console.log("sentry init", session);
Sentry.configureScope(async (scope) => {
scope.setUser({
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
});
});
}
Sentry.init({
dsn: "https://5ff29bb5b3b64cd4bd4f4960ef1db2e3@o4504750197899264.ingest.sentry.io/4504750206746624"
});
if (session) {
console.log("sentry init", session);
Sentry.configureScope(async (scope) => {
scope.setUser({
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
});
});
}
}
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 initializeFeaturedPackages = async () => {
console.log("intialize featured packages");
const packages = await getFeaturedPackages();
featuredPackages.set(packages);
console.log("intialize featured packages");
const packages = await getFeaturedPackages();
featuredPackages.set(packages);
};
interface PackagesReview {
[full_name: string]: Review[];
[full_name: string]: Review[];
}
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) => {
if (full_name && !packagesReviews[full_name]) {
packagesReviews[full_name] = [];
const reviews = await getPackageReviews(full_name);
update((v) => {
return {
...v,
[full_name]: reviews
};
});
}
};
const getSetPackageReviews = async (full_name: string) => {
if (full_name && !packagesReviews[full_name]) {
packagesReviews[full_name] = [];
const reviews = await getPackageReviews(full_name);
update((v) => {
return {
...v,
[full_name]: reviews
};
});
}
};
return {
subscribe: (full_name: string, reset: (reviews: Review[]) => void) => {
getSetPackageReviews(full_name);
return subscribe((value) => {
if (value[full_name]) {
reset(value[full_name]);
}
});
}
};
return {
subscribe: (full_name: string, reset: (reviews: Review[]) => void) => {
getSetPackageReviews(full_name);
return subscribe((value) => {
if (value[full_name]) {
reset(value[full_name]);
}
});
}
};
}
export const packagesReviewStore = initPackagesReviewStore();
function initPosts() {
let initialized = false;
const { subscribe } = writable<AirtablePost[]>([]);
const posts: AirtablePost[] = [];
let postsIndex: Fuse<AirtablePost>;
let initialized = false;
const { subscribe } = writable<AirtablePost[]>([]);
const posts: AirtablePost[] = [];
let postsIndex: Fuse<AirtablePost>;
if (!initialized) {
initialized = true;
// getAllPosts().then(set);
}
if (!initialized) {
initialized = true;
// getAllPosts().then(set);
}
subscribe((v) => {
posts.push(...v);
postsIndex = new Fuse(posts, {
keys: ["title", "sub_title", "short_description", "tags"]
});
});
subscribe((v) => {
posts.push(...v);
postsIndex = new Fuse(posts, {
keys: ["title", "sub_title", "short_description", "tags"]
});
});
return {
subscribe,
search: async (term: string, limit = 10) => {
const res = postsIndex.search(term, { limit });
const matchingPosts: AirtablePost[] = res.map((v) => v.item);
return matchingPosts;
},
subscribeByTag: (tag: string, cb: (posts: AirtablePost[]) => void) => {
subscribe((newPosts: AirtablePost[]) => {
const filteredPosts = newPosts.filter((post) => post.tags.includes(tag));
cb(filteredPosts);
});
}
};
return {
subscribe,
search: async (term: string, limit = 10) => {
const res = postsIndex.search(term, { limit });
const matchingPosts: AirtablePost[] = res.map((v) => v.item);
return matchingPosts;
},
subscribeByTag: (tag: string, cb: (posts: AirtablePost[]) => void) => {
subscribe((newPosts: AirtablePost[]) => {
const filteredPosts = newPosts.filter((post) => post.tags.includes(tag));
cb(filteredPosts);
});
}
};
}
export const postsStore = initPosts();
function initSearchStore() {
const searching = writable<boolean>(false);
const packagesSearch = writable<GUIPackage[]>([]);
const postsSearch = writable<AirtablePost[]>([]);
const searching = writable<boolean>(false);
const packagesSearch = writable<GUIPackage[]>([]);
const postsSearch = writable<AirtablePost[]>([]);
// TODO:
// should use algolia if user is somehow online
// TODO:
// should use algolia if user is somehow online
return {
searching,
packagesSearch,
postsSearch,
search: async (term: string) => {
try {
if (term) {
const [
resultPkgs
// resultPosts
] = await Promise.all([
packagesStore.search(term, 5)
// postsStore.search(term, 10)
]);
trackSearch(term, resultPkgs.length);
packagesSearch.set(resultPkgs);
// postsSearch.set(resultPosts);
} else {
packagesSearch.set([]);
// postsSearch.set([]);
}
} catch (error) {
console.error(error);
}
}
};
return {
searching,
packagesSearch,
postsSearch,
search: async (term: string) => {
try {
if (term) {
const [
resultPkgs
// resultPosts
] = await Promise.all([
packagesStore.search(term, 5)
// postsStore.search(term, 10)
]);
trackSearch(term, resultPkgs.length);
packagesSearch.set(resultPkgs);
// postsSearch.set(resultPosts);
} else {
packagesSearch.set([]);
// postsSearch.set([]);
}
} catch (error) {
console.error(error);
}
}
};
}
export const searchStore = initSearchStore();

View file

@ -8,81 +8,81 @@ import { initSentry } from "../sentry";
export let session: Session | null = null;
export const getSession = async (): Promise<Session | null> => {
session = await electronGetSession();
return session;
session = await electronGetSession();
return session;
};
export default function initAuthStore() {
const user = writable<Developer | undefined>();
const sessionStore = writable<Session>({});
let pollLoop = 0;
const user = writable<Developer | undefined>();
const sessionStore = writable<Session>({});
let pollLoop = 0;
const deviceIdStore = writable<string>("");
let deviceId = "";
const deviceIdStore = writable<string>("");
let deviceId = "";
getSession().then((sess) => {
if (sess) {
session = sess;
initSentry(sess);
sessionStore.set(sess);
deviceIdStore.set(sess.device_id!);
deviceId = sess.device_id!;
if (sess.user) user.set(sess.user);
}
});
getSession().then((sess) => {
if (sess) {
session = sess;
initSentry(sess);
sessionStore.set(sess);
deviceIdStore.set(sess.device_id!);
deviceId = sess.device_id!;
if (sess.user) user.set(sess.user);
}
});
let timer: NodeJS.Timer | null;
let timer: NodeJS.Timer | null;
async function updateSession(data: Session) {
sessionStore.update((val) => ({
...val,
...data
}));
async function updateSession(data: Session) {
sessionStore.update((val) => ({
...val,
...data
}));
initSentry(data);
await electronUpdateSession(data);
}
initSentry(data);
await electronUpdateSession(data);
}
async function pollSession() {
if (!timer) {
timer = setInterval(async () => {
pollLoop++;
try {
const data = await getDeviceAuth(deviceId);
if (data.status === "SUCCESS") {
updateSession({
key: data.key,
user: data.user
});
user.set(data.user!);
timer && clearInterval(timer);
timer = null;
}
} catch (error) {
console.error(error);
}
async function pollSession() {
if (!timer) {
timer = setInterval(async () => {
pollLoop++;
try {
const data = await getDeviceAuth(deviceId);
if (data.status === "SUCCESS") {
updateSession({
key: data.key,
user: data.user
});
user.set(data.user!);
timer && clearInterval(timer);
timer = null;
}
} catch (error) {
console.error(error);
}
if (pollLoop > 20 && timer) {
clearInterval(timer);
pollLoop = 0;
timer = null;
}
}, 2000);
}
}
if (pollLoop > 20 && timer) {
clearInterval(timer);
pollLoop = 0;
timer = null;
}
}, 2000);
}
}
function clearSession() {
updateSession({ key: undefined, user: undefined });
user.set(undefined);
}
function clearSession() {
updateSession({ key: undefined, user: undefined });
user.set(undefined);
}
return {
user,
session: sessionStore,
deviceId,
deviceIdStore,
pollSession,
clearSession,
updateSession
};
return {
user,
session: sessionStore,
deviceId,
deviceIdStore,
pollSession,
clearSession,
updateSession
};
}

View file

@ -2,55 +2,55 @@ import { writable } from "svelte/store";
import { goto } from "$app/navigation";
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 nextPathStore = writable<string>("");
const prevPathStore = 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 isMovingBack = false;
let isMovingNext = false;
let isMovingBack = false;
return {
historyStore,
prevPath: prevPathStore,
nextPath: nextPathStore,
next: () => {
if (currentIndex < history.length - 1) {
isMovingNext = true;
goto(history[currentIndex + 1]);
prevPathStore.set(history[currentIndex]);
currentIndex++;
if (currentIndex >= history.length - 1) nextPathStore.set("");
}
},
back: () => {
if (currentIndex > 0) {
isMovingBack = true;
goto(history[currentIndex - 1]);
nextPathStore.set(history[currentIndex]);
currentIndex--;
if (currentIndex === 0) prevPathStore.set("");
}
},
setNewPath: (newNextPath: string, _newPrevPath: string) => {
const oldCurrentPath = history[currentIndex];
const isNavArrows = isMovingBack || isMovingNext;
if (!isNavArrows && newNextPath !== oldCurrentPath) {
historyStore.update((history) => {
const cleanHistory = history.filter((_v, i) => i <= currentIndex);
currentIndex = cleanHistory.length;
prevPathStore.set(cleanHistory[currentIndex - 1]);
return [...cleanHistory, newNextPath];
});
}
isMovingNext = false;
isMovingBack = false;
}
};
return {
historyStore,
prevPath: prevPathStore,
nextPath: nextPathStore,
next: () => {
if (currentIndex < history.length - 1) {
isMovingNext = true;
goto(history[currentIndex + 1]);
prevPathStore.set(history[currentIndex]);
currentIndex++;
if (currentIndex >= history.length - 1) nextPathStore.set("");
}
},
back: () => {
if (currentIndex > 0) {
isMovingBack = true;
goto(history[currentIndex - 1]);
nextPathStore.set(history[currentIndex]);
currentIndex--;
if (currentIndex === 0) prevPathStore.set("");
}
},
setNewPath: (newNextPath: string, _newPrevPath: string) => {
const oldCurrentPath = history[currentIndex];
const isNavArrows = isMovingBack || isMovingNext;
if (!isNavArrows && newNextPath !== oldCurrentPath) {
historyStore.update((history) => {
const cleanHistory = history.filter((_v, i) => i <= currentIndex);
currentIndex = cleanHistory.length;
prevPathStore.set(cleanHistory[currentIndex - 1]);
return [...cleanHistory, newNextPath];
});
}
isMovingNext = false;
isMovingBack = false;
}
};
}

View file

@ -7,51 +7,51 @@ import type { Notification } from "@tea/ui/types";
import { listenToChannel, relaunch } from "@native";
export default function initNotificationStore() {
const notifications: Notification[] = [];
const { update, subscribe } = writable<Notification[]>([]);
const notifications: Notification[] = [];
const { update, subscribe } = writable<Notification[]>([]);
const remove = (id: string) => {
update((notifications) => notifications.filter((n) => n.id != id));
};
const remove = (id: string) => {
update((notifications) => notifications.filter((n) => n.id != id));
};
listenToChannel("message", (data: any) => {
const { message, params }: { message: string; params: { [key: string]: string } } = data;
listenToChannel("message", (data: any) => {
const { message, params }: { message: string; params: { [key: string]: string } } = data;
update((value) => {
const newNotification: Notification = {
id: nanoid(4),
message,
i18n_key: params["i18n_key"] || "",
type: NotificationType.ACTION_BANNER,
params
};
if (params.action) {
newNotification.callback_label = params.action.toUpperCase();
newNotification.callback = () => {
relaunch();
remove(newNotification.id); // not sure yet
};
}
return [...value, newNotification];
});
});
update((value) => {
const newNotification: Notification = {
id: nanoid(4),
message,
i18n_key: params["i18n_key"] || "",
type: NotificationType.ACTION_BANNER,
params
};
if (params.action) {
newNotification.callback_label = params.action.toUpperCase();
newNotification.callback = () => {
relaunch();
remove(newNotification.id); // not sure yet
};
}
return [...value, newNotification];
});
});
return {
notifications,
subscribe,
remove,
add: (partialNotification: Partial<Notification>) => {
if (!partialNotification.message) throw new Error("message is required");
return {
notifications,
subscribe,
remove,
add: (partialNotification: Partial<Notification>) => {
if (!partialNotification.message) throw new Error("message is required");
const notification: Notification = {
id: nanoid(4),
i18n_key: partialNotification.i18n_key || "",
type: NotificationType.MESSAGE,
message: partialNotification.message || "",
...partialNotification
};
const notification: Notification = {
id: nanoid(4),
i18n_key: partialNotification.i18n_key || "",
type: NotificationType.MESSAGE,
message: partialNotification.message || "",
...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 Fuse from "fuse.js";
import {
getPackage,
getDistPackages,
getInstalledPackages,
installPackage,
deletePackage,
getPackageBottles,
setBadgeCount,
loadPackageCache,
writePackageCache,
syncPantry,
cacheImageURL,
listenToChannel
getPackage,
getDistPackages,
getInstalledPackages,
installPackage,
deletePackage,
getPackageBottles,
setBadgeCount,
loadPackageCache,
writePackageCache,
syncPantry,
cacheImageURL,
listenToChannel
} from "@native";
import { getReadme, getContributors, getRepoAsPackage } from "$libs/github";
@ -31,337 +31,337 @@ import log from "$libs/logger";
const packageRefreshInterval = 1000 * 60 * 60; // 1 hour
export default function initPackagesStore() {
let initialized = false;
let isDestroyed = false;
let refreshTimeoutId: ReturnType<typeof setTimeout> | null = null;
let initialized = false;
let isDestroyed = false;
let refreshTimeoutId: ReturnType<typeof setTimeout> | null = null;
const packageMap = writable<Packages>({ version: "0", packages: {} });
const packageList = derived(packageMap, ($packages) => Object.values($packages.packages));
const packageMap = writable<Packages>({ version: "0", packages: {} });
const packageList = derived(packageMap, ($packages) => Object.values($packages.packages));
let packagesIndex: Fuse<GUIPackage>;
let packagesIndex: Fuse<GUIPackage>;
const updateAllPackages = (guiPkgs: GUIPackage[]) => {
packageMap.update((pkgs) => {
guiPkgs.forEach((pkg) => {
const oldPkg = pkgs.packages[pkg.full_name];
pkgs.packages[pkg.full_name] = { ...oldPkg, ...pkg };
});
setBadgeCountFromPkgs(pkgs);
return pkgs;
});
};
const updateAllPackages = (guiPkgs: GUIPackage[]) => {
packageMap.update((pkgs) => {
guiPkgs.forEach((pkg) => {
const oldPkg = pkgs.packages[pkg.full_name];
pkgs.packages[pkg.full_name] = { ...oldPkg, ...pkg };
});
setBadgeCountFromPkgs(pkgs);
return pkgs;
});
};
const updatePackage = (full_name: string, props: Partial<GUIPackage>, newVersion?: string) => {
packageMap.update((pkgs) => {
const pkg = pkgs.packages[full_name];
if (pkg) {
const updatedPkg = { ...pkg, ...props };
const updatePackage = (full_name: string, props: Partial<GUIPackage>, newVersion?: string) => {
packageMap.update((pkgs) => {
const pkg = pkgs.packages[full_name];
if (pkg) {
const updatedPkg = { ...pkg, ...props };
if (newVersion) {
updatedPkg.installed_versions = addInstalledVersion(
updatedPkg.installed_versions,
newVersion
);
}
if (newVersion) {
updatedPkg.installed_versions = addInstalledVersion(
updatedPkg.installed_versions,
newVersion
);
}
updatedPkg.state = getPackageState(updatedPkg);
pkgs.packages[full_name] = updatedPkg;
updatedPkg.state = getPackageState(updatedPkg);
pkgs.packages[full_name] = updatedPkg;
setBadgeCountFromPkgs(pkgs);
}
return pkgs;
});
};
setBadgeCountFromPkgs(pkgs);
}
return pkgs;
});
};
// getPackage state centralizes the logic for determining the state of the package based on the other properties
const getPackageState = (pkg: GUIPackage): PackageStates => {
if (pkg.isUninstalling) {
//TODO: maybe there should be an uninstalling state too? Although that needs UI/UX changes
return PackageStates.AVAILABLE;
}
// getPackage state centralizes the logic for determining the state of the package based on the other properties
const getPackageState = (pkg: GUIPackage): PackageStates => {
if (pkg.isUninstalling) {
//TODO: maybe there should be an uninstalling state too? Although that needs UI/UX changes
return PackageStates.AVAILABLE;
}
const isUpToDate = pkg.version === pkg.installed_versions?.[0];
const isUpToDate = pkg.version === pkg.installed_versions?.[0];
if (isInstalling(pkg)) {
const hasNoVersions = !pkg.installed_versions?.length;
if (hasNoVersions || isUpToDate) {
return PackageStates.INSTALLING;
}
return PackageStates.UPDATING;
}
if (isInstalling(pkg)) {
const hasNoVersions = !pkg.installed_versions?.length;
if (hasNoVersions || isUpToDate) {
return PackageStates.INSTALLING;
}
return PackageStates.UPDATING;
}
if (!pkg.installed_versions?.length) {
return PackageStates.AVAILABLE;
}
if (!pkg.installed_versions?.length) {
return PackageStates.AVAILABLE;
}
return isUpToDate ? PackageStates.INSTALLED : PackageStates.NEEDS_UPDATE;
};
return isUpToDate ? PackageStates.INSTALLED : PackageStates.NEEDS_UPDATE;
};
const syncPackageData = async (guiPkg: Partial<GUIPackage> | undefined) => {
if (!guiPkg) return;
const syncPackageData = async (guiPkg: Partial<GUIPackage> | undefined) => {
if (!guiPkg) return;
const pkg = await getPackage(guiPkg.full_name!); // ATM: pkg only bottles and github:string
const readmeMd = `# ${guiPkg.full_name} #
const pkg = await getPackage(guiPkg.full_name!); // ATM: pkg only bottles and github:string
const readmeMd = `# ${guiPkg.full_name} #
To read more about this package go to [${guiPkg.homepage}](${guiPkg.homepage}).
`;
const updatedPackage: Partial<GUIPackage> = {
...pkg,
readme: {
data: readmeMd,
type: "md"
},
synced: true,
github: pkg.github
? trimGithubSlug(pkg.github)
: pkg.full_name?.includes("github.com")
? trimGithubSlug(pkg.full_name.split("github.com/")[1])
: ""
};
if (updatedPackage.github) {
const [owner, repo] = updatedPackage.github.split("/");
const [readme, contributors, repoData] = await Promise.all([
getReadme(owner, repo),
getContributors(owner, repo),
getRepoAsPackage(owner, repo)
]);
if (readme) {
updatedPackage.readme = readme;
}
updatedPackage.contributors = contributors;
updatedPackage.license = repoData.license;
}
const updatedPackage: Partial<GUIPackage> = {
...pkg,
readme: {
data: readmeMd,
type: "md"
},
synced: true,
github: pkg.github
? trimGithubSlug(pkg.github)
: pkg.full_name?.includes("github.com")
? trimGithubSlug(pkg.full_name.split("github.com/")[1])
: ""
};
if (updatedPackage.github) {
const [owner, repo] = updatedPackage.github.split("/");
const [readme, contributors, repoData] = await Promise.all([
getReadme(owner, repo),
getContributors(owner, repo),
getRepoAsPackage(owner, repo)
]);
if (readme) {
updatedPackage.readme = readme;
}
updatedPackage.contributors = contributors;
updatedPackage.license = repoData.license;
}
updatePackage(guiPkg.full_name!, updatedPackage);
};
updatePackage(guiPkg.full_name!, updatedPackage);
};
const init = async function () {
log.info("packages store: try initialize");
const init = async function () {
log.info("packages store: try initialize");
if (!initialized) {
const cachedPkgs: Packages = await loadPackageCache();
log.info(`Loaded ${Object.keys(cachedPkgs.packages).length} packages from cache`);
packageMap.set(cachedPkgs);
if (!initialized) {
const cachedPkgs: Packages = await loadPackageCache();
log.info(`Loaded ${Object.keys(cachedPkgs.packages).length} packages from cache`);
packageMap.set(cachedPkgs);
await refreshPackages();
initialized = true;
}
log.info("packages store: initialized!");
};
await refreshPackages();
initialized = true;
}
log.info("packages store: initialized!");
};
const refreshPackages = async () => {
if (isDestroyed) return;
const refreshPackages = async () => {
if (isDestroyed) return;
log.info("packages store: refreshing...");
log.info("packages store: refreshing...");
const pkgs = await getDistPackages();
const guiPkgs: GUIPackage[] = pkgs.map((p) => ({
...p,
state: PackageStates.AVAILABLE
}));
const pkgs = await getDistPackages();
const guiPkgs: GUIPackage[] = pkgs.map((p) => ({
...p,
state: PackageStates.AVAILABLE
}));
if (!initialized) {
// set packages data so that i can render something in the UI already
updateAllPackages(guiPkgs);
log.info("initialized packages store with ", guiPkgs.length);
}
if (!initialized) {
// set packages data so that i can render something in the UI already
updateAllPackages(guiPkgs);
log.info("initialized packages store with ", guiPkgs.length);
}
packagesIndex = new Fuse(guiPkgs, {
keys: ["name", "full_name", "desc", "categories"],
minMatchCharLength: 3,
threshold: 0.3
});
log.info("refreshed packages fuse index");
packagesIndex = new Fuse(guiPkgs, {
keys: ["name", "full_name", "desc", "categories"],
minMatchCharLength: 3,
threshold: 0.3
});
log.info("refreshed packages fuse index");
try {
const installedPkgs: InstalledPackage[] = await getInstalledPackages();
try {
const installedPkgs: InstalledPackage[] = await getInstalledPackages();
log.info("updating state of packages");
for (const pkg of guiPkgs) {
const iPkg = installedPkgs.find((p) => p.full_name === pkg.full_name);
if (iPkg) {
pkg.installed_versions = iPkg.installed_versions;
updatePackage(pkg.full_name, {
installed_versions: iPkg.installed_versions
});
}
}
} catch (error) {
log.error(error);
}
log.info("updating state of packages");
for (const pkg of guiPkgs) {
const iPkg = installedPkgs.find((p) => p.full_name === pkg.full_name);
if (iPkg) {
pkg.installed_versions = iPkg.installed_versions;
updatePackage(pkg.full_name, {
installed_versions: iPkg.installed_versions
});
}
}
} catch (error) {
log.error(error);
}
try {
await withRetry(syncPantry);
} catch (err) {
log.error(err);
}
try {
await withRetry(syncPantry);
} catch (err) {
log.error(err);
}
refreshTimeoutId = setTimeout(() => refreshPackages(), packageRefreshInterval); // refresh every hour
};
refreshTimeoutId = setTimeout(() => refreshPackages(), packageRefreshInterval); // refresh every hour
};
// Destructor for the package store
const destroy = () => {
isDestroyed = true;
if (refreshTimeoutId) {
clearTimeout(refreshTimeoutId);
}
log.info("packages store: destroyed");
};
// Destructor for the package store
const destroy = () => {
isDestroyed = true;
if (refreshTimeoutId) {
clearTimeout(refreshTimeoutId);
}
log.info("packages store: destroyed");
};
const installPkg = async (pkg: GUIPackage, version?: string) => {
const versionToInstall = version || pkg.version;
const installPkg = async (pkg: GUIPackage, version?: string) => {
const versionToInstall = version || pkg.version;
try {
updatePackage(pkg.full_name, { install_progress_percentage: 0.01 });
await installPackage(pkg, versionToInstall);
trackInstall(pkg.full_name);
notificationStore.add({
message: `Package ${pkg.full_name} v${versionToInstall} has been installed.`
});
} catch (error) {
log.error(error);
let message = "Unknown Error";
if (error instanceof Error) message = error.message;
trackInstallFailed(pkg.full_name, message || "unknown");
try {
updatePackage(pkg.full_name, { install_progress_percentage: 0.01 });
await installPackage(pkg, versionToInstall);
trackInstall(pkg.full_name);
notificationStore.add({
message: `Package ${pkg.full_name} v${versionToInstall} has been installed.`
});
} catch (error) {
log.error(error);
let message = "Unknown Error";
if (error instanceof Error) message = error.message;
trackInstallFailed(pkg.full_name, message || "unknown");
notificationStore.add({
message: `Package ${pkg.full_name} v${versionToInstall} failed to install.`,
type: NotificationType.ERROR
});
} finally {
updatePackage(pkg.full_name, { install_progress_percentage: 100 });
}
};
notificationStore.add({
message: `Package ${pkg.full_name} v${versionToInstall} failed to install.`,
type: NotificationType.ERROR
});
} finally {
updatePackage(pkg.full_name, { install_progress_percentage: 100 });
}
};
const uninstallPkg = async (pkg: GUIPackage) => {
let fakeTimer: NodeJS.Timer | null = null;
try {
fakeTimer = withFakeLoader(pkg, (progress) => {
updatePackage(pkg.full_name, {
install_progress_percentage: progress,
isUninstalling: true
});
});
const uninstallPkg = async (pkg: GUIPackage) => {
let fakeTimer: NodeJS.Timer | null = null;
try {
fakeTimer = withFakeLoader(pkg, (progress) => {
updatePackage(pkg.full_name, {
install_progress_percentage: progress,
isUninstalling: true
});
});
for (const v of pkg.installed_versions || []) {
await deletePkg(pkg, v);
}
for (const v of pkg.installed_versions || []) {
await deletePkg(pkg, v);
}
setTimeout(() => {
updatePackage(pkg.full_name, {
installed_versions: []
});
}, 3000);
} catch (error) {
log.error(error);
notificationStore.add({
message: `Package ${pkg.full_name} failed to uninstall.`,
type: NotificationType.ERROR
});
} finally {
fakeTimer && clearTimeout(fakeTimer);
updatePackage(pkg.full_name, { install_progress_percentage: 0, isUninstalling: false });
}
};
setTimeout(() => {
updatePackage(pkg.full_name, {
installed_versions: []
});
}, 3000);
} catch (error) {
log.error(error);
notificationStore.add({
message: `Package ${pkg.full_name} failed to uninstall.`,
type: NotificationType.ERROR
});
} finally {
fakeTimer && clearTimeout(fakeTimer);
updatePackage(pkg.full_name, { install_progress_percentage: 0, isUninstalling: false });
}
};
const fetchPackageBottles = async (pkgName: string) => {
// TODO: this api should take an architecture argument or else an architecture filter should be applied downstreawm
const bottles = await getPackageBottles(pkgName);
if (bottles?.length) {
updatePackage(pkgName, { bottles });
}
};
const fetchPackageBottles = async (pkgName: string) => {
// TODO: this api should take an architecture argument or else an architecture filter should be applied downstreawm
const bottles = await getPackageBottles(pkgName);
if (bottles?.length) {
updatePackage(pkgName, { bottles });
}
};
const deletePkg = async (pkg: GUIPackage, version: string) => {
log.info("deleting package: ", pkg.full_name, " version: ", version);
await deletePackage({ fullName: pkg.full_name, version });
updatePackage(pkg.full_name, {
installed_versions: pkg.installed_versions?.filter((v) => v !== version)
});
};
const deletePkg = async (pkg: GUIPackage, version: string) => {
log.info("deleting package: ", pkg.full_name, " version: ", version);
await deletePackage({ fullName: pkg.full_name, version });
updatePackage(pkg.full_name, {
installed_versions: pkg.installed_versions?.filter((v) => v !== version)
});
};
const writePackageCacheWithDebounce = withDebounce(writePackageCache);
packageMap.subscribe(async (pkgs) => {
writePackageCacheWithDebounce(pkgs);
});
const writePackageCacheWithDebounce = withDebounce(writePackageCache);
packageMap.subscribe(async (pkgs) => {
writePackageCacheWithDebounce(pkgs);
});
const cachePkgImage = async (pkg: GUIPackage): Promise<string> => {
let cacheFileURL = "";
updatePackage(pkg.full_name, { cached_image_url: "" });
if (pkg.thumb_image_url && !pkg.thumb_image_url.includes("package-thumb-nolabel4.jpg")) {
const result = await cacheImageURL(pkg.thumb_image_url);
if (result) {
cacheFileURL = result;
updatePackage(pkg.full_name, { cached_image_url: cacheFileURL });
}
}
return cacheFileURL;
};
const cachePkgImage = async (pkg: GUIPackage): Promise<string> => {
let cacheFileURL = "";
updatePackage(pkg.full_name, { cached_image_url: "" });
if (pkg.thumb_image_url && !pkg.thumb_image_url.includes("package-thumb-nolabel4.jpg")) {
const result = await cacheImageURL(pkg.thumb_image_url);
if (result) {
cacheFileURL = result;
updatePackage(pkg.full_name, { cached_image_url: cacheFileURL });
}
}
return cacheFileURL;
};
listenToChannel("install-progress", ({ full_name, progress }: any) => {
if (!full_name) {
return;
}
updatePackage(full_name, { install_progress_percentage: progress });
});
listenToChannel("install-progress", ({ full_name, progress }: any) => {
if (!full_name) {
return;
}
updatePackage(full_name, { install_progress_percentage: progress });
});
listenToChannel("pkg-installed", ({ full_name, version }: any) => {
if (!full_name) {
return;
}
updatePackage(full_name, {}, version);
});
listenToChannel("pkg-installed", ({ full_name, version }: any) => {
if (!full_name) {
return;
}
updatePackage(full_name, {}, version);
});
return {
packageList,
search: async (term: string, limit = 5): Promise<GUIPackage[]> => {
if (!term || !packagesIndex) return [];
// TODO: if online, use algolia else use Fuse
const res = packagesIndex.search(term, { limit });
const matchingPackages: GUIPackage[] = res.map((v) => v.item);
return matchingPackages;
},
fetchPackageBottles,
init,
installPkg,
uninstallPkg,
syncPackageData,
deletePkg,
destroy,
cachePkgImage
};
return {
packageList,
search: async (term: string, limit = 5): Promise<GUIPackage[]> => {
if (!term || !packagesIndex) return [];
// TODO: if online, use algolia else use Fuse
const res = packagesIndex.search(term, { limit });
const matchingPackages: GUIPackage[] = res.map((v) => v.item);
return matchingPackages;
},
fetchPackageBottles,
init,
installPkg,
uninstallPkg,
syncPackageData,
deletePkg,
destroy,
cachePkgImage
};
}
// This is only used for uninstall now
export const withFakeLoader = (
pkg: GUIPackage,
callback: (progress: number) => void
pkg: GUIPackage,
callback: (progress: number) => void
): NodeJS.Timer => {
let fakeLoadingProgress = 1;
const ms = 100;
const assumedDlSpeedMb = 1024 * 1024 * 3; // 3mbps
const size = pkg?.bottles?.length ? pkg.bottles[0].bytes : assumedDlSpeedMb * 10;
const eta = size / assumedDlSpeedMb;
let fakeLoadingProgress = 1;
const ms = 100;
const assumedDlSpeedMb = 1024 * 1024 * 3; // 3mbps
const size = pkg?.bottles?.length ? pkg.bottles[0].bytes : assumedDlSpeedMb * 10;
const eta = size / assumedDlSpeedMb;
const increment = 1 / eta / 10;
const increment = 1 / eta / 10;
const fakeTimer = setInterval(() => {
const progressLeft = 100 - fakeLoadingProgress;
const addProgress = progressLeft * increment;
fakeLoadingProgress = fakeLoadingProgress + addProgress;
callback(fakeLoadingProgress);
}, ms);
const fakeTimer = setInterval(() => {
const progressLeft = 100 - fakeLoadingProgress;
const addProgress = progressLeft * increment;
fakeLoadingProgress = fakeLoadingProgress + addProgress;
callback(fakeLoadingProgress);
}, ms);
return fakeTimer;
return fakeTimer;
};
const setBadgeCountFromPkgs = (pkgs: Packages) => {
try {
const needsUpdateCount = Object.values(pkgs.packages).filter(
(p) => p.state === PackageStates.NEEDS_UPDATE
).length;
setBadgeCount(needsUpdateCount);
} catch (error) {
log.error(error);
}
try {
const needsUpdateCount = Object.values(pkgs.packages).filter(
(p) => p.state === PackageStates.NEEDS_UPDATE
).length;
setBadgeCount(needsUpdateCount);
} catch (error) {
log.error(error);
}
};

View file

@ -3,19 +3,19 @@ import { getAutoUpdateStatus, listenToChannel } from "@native";
import { writable } from "svelte/store";
export default function initAppUpdateStore() {
const updateStatus = writable<AutoUpdateStatus>({ status: "up-to-date" });
const updateStatus = writable<AutoUpdateStatus>({ status: "up-to-date" });
getAutoUpdateStatus().then((status: AutoUpdateStatus) => {
updateStatus.update(() => status);
});
getAutoUpdateStatus().then((status: AutoUpdateStatus) => {
updateStatus.update(() => status);
});
listenToChannel("app-update-status", (status: AutoUpdateStatus) => {
if (status.status) {
updateStatus.update(() => status);
}
});
listenToChannel("app-update-status", (status: AutoUpdateStatus) => {
if (status.status) {
updateStatus.update(() => status);
}
});
return {
updateStatus
};
return {
updateStatus
};
}

View file

@ -3,9 +3,9 @@ import translations from "./translations.json";
/** @type {import('sveltekit-i18n').Config} */
const config = {
initLocale: "en",
fallbackLocale: "en",
translations
initLocale: "en",
fallbackLocale: "en",
translations
};
export const { t, l, locales, locale } = new i18n(config);

View file

@ -1,83 +1,83 @@
{
"en": {
"lang": {
"en": "English"
},
"store-search-placeholder": "search packages",
"package": {
"update-all": "UPDATE ALL",
"cta-AVAILABLE": "INSTALL",
"cta-INSTALLED": "INSTALLED",
"cta-INSTALLING": "INSTALLING",
"cta-UNINSTALLED": "RE-INSTALL",
"cta-UNINSTALL": "UNINSTALL",
"cta-NEEDS_UPDATE": "UPDATE",
"cta-UPDATING": "UPDATING",
"cta-PRUNE": "PRUNE",
"cta-PRUNING": "PRUNING"
},
"footer": {
"quick-links-title": "quick links",
"about-tea-store": "about the tea store",
"report-a-problem": "report a problem",
"visit-website": "visit tea.xyz",
"terms-services": "terms & services",
"privacy-policy": "privacy-policy"
},
"documentation": {
"title": "documentation",
"featured-courses-title": "featured courses",
"workshops": "workshops"
},
"view-all": "view all",
"sorting": {
"label": "Sort by",
"popularity": "Most popular",
"most-recent": "Most recent"
},
"common": {
"home": "home",
"all": "All",
"articles": "Articles",
"workshops": "Workshops",
"details": "details",
"versions": "versions",
"metadata": "Metadata",
"homepage": "Homepage",
"documentation": "Documentation",
"github-repository": "Github Repository",
"contributors": "Contributors",
"view-on-github": "VIEW ON GITHUB"
},
"notification": {
"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."
},
"side-menu-title": {
"discover": "discover",
"all": "All Packages",
"installed": "Installed Packages",
"installed_updates_available": "Available Updates",
"recently_updated": "Recently Updated",
"new_packages": "New Packages",
"popular": "Popular",
"featured": "Featured",
"essentials": "Essentials",
"starstruck": "Starstruck Heavyweights",
"made_by_tea": "made by tea"
},
"tags": {
"discover": "discover",
"all": "all",
"installed": "installed",
"installed_updates_available": "Updates available",
"recently_updated": "Recently updated",
"new_packages": "New packages",
"popular": "Popular",
"featured": "Featured",
"essentials": "Essentials",
"starstruck": "Starstruck",
"made_by_tea": "Made by tea"
}
}
"en": {
"lang": {
"en": "English"
},
"store-search-placeholder": "search packages",
"package": {
"update-all": "UPDATE ALL",
"cta-AVAILABLE": "INSTALL",
"cta-INSTALLED": "INSTALLED",
"cta-INSTALLING": "INSTALLING",
"cta-UNINSTALLED": "RE-INSTALL",
"cta-UNINSTALL": "UNINSTALL",
"cta-NEEDS_UPDATE": "UPDATE",
"cta-UPDATING": "UPDATING",
"cta-PRUNE": "PRUNE",
"cta-PRUNING": "PRUNING"
},
"footer": {
"quick-links-title": "quick links",
"about-tea-store": "about the tea store",
"report-a-problem": "report a problem",
"visit-website": "visit tea.xyz",
"terms-services": "terms & services",
"privacy-policy": "privacy-policy"
},
"documentation": {
"title": "documentation",
"featured-courses-title": "featured courses",
"workshops": "workshops"
},
"view-all": "view all",
"sorting": {
"label": "Sort by",
"popularity": "Most popular",
"most-recent": "Most recent"
},
"common": {
"home": "home",
"all": "All",
"articles": "Articles",
"workshops": "Workshops",
"details": "details",
"versions": "versions",
"metadata": "Metadata",
"homepage": "Homepage",
"documentation": "Documentation",
"github-repository": "Github Repository",
"contributors": "Contributors",
"view-on-github": "VIEW ON GITHUB"
},
"notification": {
"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."
},
"side-menu-title": {
"discover": "discover",
"all": "All Packages",
"installed": "Installed Packages",
"installed_updates_available": "Available Updates",
"recently_updated": "Recently Updated",
"new_packages": "New Packages",
"popular": "Popular",
"featured": "Featured",
"essentials": "Essentials",
"starstruck": "Starstruck Heavyweights",
"made_by_tea": "made by tea"
},
"tags": {
"discover": "discover",
"all": "all",
"installed": "installed",
"installed_updates_available": "Updates available",
"recently_updated": "Recently updated",
"new_packages": "New packages",
"popular": "Popular",
"featured": "Featured",
"essentials": "Essentials",
"starstruck": "Starstruck",
"made_by_tea": "Made by tea"
}
}
}

View file

@ -6,77 +6,77 @@
import type { Package, Developer } from "@tea/ui/types";
export enum PackageStates {
AVAILABLE = "AVAILABLE",
INSTALLED = "INSTALLED",
INSTALLING = "INSTALLING",
NEEDS_UPDATE = "NEEDS_UPDATE",
UPDATING = "UPDATING"
AVAILABLE = "AVAILABLE",
INSTALLED = "INSTALLED",
INSTALLING = "INSTALLING",
NEEDS_UPDATE = "NEEDS_UPDATE",
UPDATING = "UPDATING"
}
export type Packages = {
version: string;
packages: { [full_name: string]: GUIPackage };
version: string;
packages: { [full_name: string]: GUIPackage };
};
export type GUIPackage = Package & {
state: PackageStates;
installed_versions?: string[];
synced?: boolean;
install_progress_percentage?: number;
isUninstalling?: boolean;
cached_image_url?: string;
state: PackageStates;
installed_versions?: string[];
synced?: boolean;
install_progress_percentage?: number;
isUninstalling?: boolean;
cached_image_url?: string;
};
export type Course = {
title: string;
sub_title: string;
banner_image_url: string;
link: string;
title: string;
sub_title: string;
banner_image_url: string;
link: string;
};
export type Category = {
label: string;
cta_label: string;
packages: GUIPackage[];
label: string;
cta_label: string;
packages: GUIPackage[];
};
export enum AuthStatus {
UNKNOWN = "UNKNOWN",
PENDING = "PENDING",
SUCCESS = "SUCCESS",
FAILED = "FAILED"
UNKNOWN = "UNKNOWN",
PENDING = "PENDING",
SUCCESS = "SUCCESS",
FAILED = "FAILED"
}
export type DeviceAuth = {
status: AuthStatus;
user?: Developer;
key: string;
status: AuthStatus;
user?: Developer;
key: string;
};
export interface Session {
device_id?: string;
key?: string;
user?: Developer;
locale?: string;
hide_welcome?: boolean;
device_id?: string;
key?: string;
user?: Developer;
locale?: string;
hide_welcome?: boolean;
}
export enum SideMenuOptions {
discover = "discover",
all = "all",
installed = "installed",
installed_updates_available = "installed_updates_available",
recently_updated = "recently_updated",
new_packages = "new_packages",
popular = "popular",
featured = "featured",
essentials = "essentials",
starstruck = "starstruck",
made_by_tea = "made_by_tea"
discover = "discover",
all = "all",
installed = "installed",
installed_updates_available = "installed_updates_available",
recently_updated = "recently_updated",
new_packages = "new_packages",
popular = "popular",
featured = "featured",
essentials = "essentials",
starstruck = "starstruck",
made_by_tea = "made_by_tea"
}
export type InstalledPackage = Required<Pick<GUIPackage, "full_name" | "installed_versions">>;
export type AutoUpdateStatus = {
status: "up-to-date" | "available" | "ready";
version?: string;
status: "up-to-date" | "available" | "ready";
version?: string;
};

View file

@ -3,27 +3,27 @@ import log from "$libs/logger";
type DebounceableFunc = (...args: any[]) => void;
export type DebounceOptions = {
lingerMs?: number;
lingerMs?: number;
};
export default function withDebounce(
f: DebounceableFunc,
{ lingerMs = 1000 }: DebounceOptions = {}
f: DebounceableFunc,
{ lingerMs = 1000 }: DebounceOptions = {}
) {
let timeoutId: ReturnType<typeof setTimeout> | undefined = undefined;
let timeoutId: ReturnType<typeof setTimeout> | undefined = undefined;
return (...args: any[]) => {
if (timeoutId) {
clearTimeout(timeoutId);
}
return (...args: any[]) => {
if (timeoutId) {
clearTimeout(timeoutId);
}
timeoutId = setTimeout(() => {
try {
f(...args);
} catch (err) {
//swallow the error, there is no good way to signal failure to the caller
log.error(err);
}
}, lingerMs);
};
timeoutId = setTimeout(() => {
try {
f(...args);
} catch (err) {
//swallow the error, there is no good way to signal failure to the caller
log.error(err);
}
}, lingerMs);
};
}

View file

@ -1,34 +1,34 @@
import log from "$libs/logger";
export type RetryOptions = {
// Number of times to retry. default 10
maxRetries?: number;
// Initial delay in ms. default 100
initialDelayMs?: number;
// Maximum delay in ms. default 5000
maxDelayMs?: number;
// Number of times to retry. default 10
maxRetries?: number;
// Initial delay in ms. default 100
initialDelayMs?: number;
// Maximum delay in ms. default 5000
maxDelayMs?: number;
};
// Retry a function up to maxRetries times, with exponential backoff
// With defaults retry cadence will look like this:
// 100ms, 200ms, 400ms, 800ms, 1600ms, 3200ms, 5000ms, 5000ms, 5000ms, 5000ms
export default async function withRetry<T>(
fn: () => Promise<T>,
{ maxRetries = 10, initialDelayMs = 100, maxDelayMs = 5000 }: RetryOptions = {}
fn: () => Promise<T>,
{ maxRetries = 10, initialDelayMs = 100, maxDelayMs = 5000 }: RetryOptions = {}
) {
let retries = 0;
let currentDelay = initialDelayMs;
while (retries <= maxRetries) {
try {
return await fn();
} catch (err) {
log.error(err);
retries++;
await wait(currentDelay);
currentDelay = Math.min(currentDelay * 2, maxDelayMs);
}
}
throw new Error(`Failed after ${maxRetries} retries`);
let retries = 0;
let currentDelay = initialDelayMs;
while (retries <= maxRetries) {
try {
return await fn();
} catch (err) {
log.error(err);
retries++;
await wait(currentDelay);
currentDelay = Math.min(currentDelay * 2, maxDelayMs);
}
}
throw new Error(`Failed after ${maxRetries} retries`);
}
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 async function get<T>(
urlPath: string,
params?: { [key: string]: string }
urlPath: string,
params?: { [key: string]: string }
): 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 =
session?.device_id && session?.user
? await getHeaders(`GET/${urlPath}`, session)
: { Authorization: "public " };
const headers =
session?.device_id && session?.user
? await getHeaders(`GET/${urlPath}`, session)
: { Authorization: "public " };
const req = await axios.request({
method: "GET",
baseURL: "https://api.tea.xyz",
url: ["v1", ...urlPath.split("/")].filter((p) => p).join("/"),
headers,
params,
validateStatus: (status) => status >= 200 && status < 300
});
const req = await axios.request({
method: "GET",
baseURL: "https://api.tea.xyz",
url: ["v1", ...urlPath.split("/")].filter((p) => p).join("/"),
headers,
params,
validateStatus: (status) => status >= 200 && status < 300
});
return req.data as T;
return req.data as T;
}
async function getHeaders(path: string, session: Session) {
const unixMs = new Date().getTime();
const unixHexSecs = Math.round(unixMs / 1000).toString(16); // hex
const deviceId = session.device_id?.split("-")[0];
const preHash = [unixHexSecs, session.key, deviceId, path].join("");
const unixMs = new Date().getTime();
const unixHexSecs = Math.round(unixMs / 1000).toString(16); // hex
const deviceId = session.device_id?.split("-")[0];
const preHash = [unixHexSecs, session.key, deviceId, path].join("");
const Authorization = bcrypt.hashSync(preHash, 10);
const Authorization = bcrypt.hashSync(preHash, 10);
return {
Authorization,
["tea-ts"]: unixMs.toString(),
["tea-uid"]: session.user?.developer_id,
["tea-gui_id"]: session.device_id
};
return {
Authorization,
["tea-ts"]: unixMs.toString(),
["tea-uid"]: session.user?.developer_id,
["tea-gui_id"]: session.device_id
};
}

View file

@ -1,135 +1,135 @@
<script lang="ts">
import "$appcss";
import { goto } from "$app/navigation";
import { initSentry } from "$libs/sentry";
import { navigating } from "$app/stores";
import { afterNavigate } from "$app/navigation";
import TopBar from "$components/top-bar/top-bar.svelte";
import { navStore, packagesStore, searchStore } from "$libs/stores";
import { listenToChannel } from "@native";
import Mousetrap from "mousetrap";
import "$appcss";
import { goto } from "$app/navigation";
import { initSentry } from "$libs/sentry";
import { navigating } from "$app/stores";
import { afterNavigate } from "$app/navigation";
import TopBar from "$components/top-bar/top-bar.svelte";
import { navStore, packagesStore, searchStore } from "$libs/stores";
import { listenToChannel } from "@native";
import Mousetrap from "mousetrap";
import SearchPopupResults from "$components/search-popup-results/search-popup-results.svelte";
import { getProtocolPath } from "@native";
import SearchPopupResults from "$components/search-popup-results/search-popup-results.svelte";
import { getProtocolPath } from "@native";
import { onDestroy, onMount } from "svelte";
import { onDestroy, onMount } from "svelte";
let view: HTMLElement;
let view: HTMLElement;
const { setNewPath } = navStore;
const { searching } = searchStore;
const { setNewPath } = navStore;
const { searching } = searchStore;
$: if ($navigating) view.scrollTop = 0;
$: if ($navigating) view.scrollTop = 0;
afterNavigate(({ from, to }) => {
if (to && to?.route.id && from && from?.url) {
const nextPath = to.url.href.replace(to.url.origin, "");
const fromPath = from?.url.href.replace(from.url.origin, "");
setNewPath(nextPath, fromPath || "/");
}
});
afterNavigate(({ from, to }) => {
if (to && to?.route.id && from && from?.url) {
const nextPath = to.url.href.replace(to.url.origin, "");
const fromPath = from?.url.href.replace(from.url.origin, "");
setNewPath(nextPath, fromPath || "/");
}
});
const syncPath = async () => {
// used by the tea:// protocol to suggest a path to open
const path = await getProtocolPath();
if (path) goto(path);
};
const syncPath = async () => {
// used by the tea:// protocol to suggest a path to open
const path = await getProtocolPath();
if (path) goto(path);
};
onMount(async () => {
// used by the tea:// protocol to suggest a path to open
syncPath();
listenToChannel("sync-path", syncPath);
Mousetrap.bind(["command+k", "ctrl+k"], function () {
searchStore.searching.set(!$searching);
// return false to prevent default browser behavior
// and stop event from bubbling
return false;
});
Mousetrap.bind(["esc"], function () {
searchStore.searching.set(false);
return false;
});
packagesStore.init();
initSentry();
});
onMount(async () => {
// used by the tea:// protocol to suggest a path to open
syncPath();
listenToChannel("sync-path", syncPath);
Mousetrap.bind(["command+k", "ctrl+k"], function () {
searchStore.searching.set(!$searching);
// return false to prevent default browser behavior
// and stop event from bubbling
return false;
});
Mousetrap.bind(["esc"], function () {
searchStore.searching.set(false);
return false;
});
packagesStore.init();
initSentry();
});
onDestroy(() => {
packagesStore.destroy();
});
onDestroy(() => {
packagesStore.destroy();
});
</script>
<div id="main-layout" class="font-inter border-gray rounded-xl border transition-all">
<TopBar />
<div class="scroll-manager relative z-10">
<section class="relative" bind:this={view}>
<div class="content">
<slot />
</div>
<SearchPopupResults />
</section>
</div>
<TopBar />
<div class="scroll-manager relative z-10">
<section class="relative" bind:this={view}>
<div class="content">
<slot />
</div>
<SearchPopupResults />
</section>
</div>
</div>
<style>
#main-layout {
height: 100vh;
overflow: hidden;
}
#main-layout {
height: 100vh;
overflow: hidden;
}
.scroll-manager {
height: 100%;
overflow: hidden;
padding-right: 8px;
}
section {
height: calc(100vh - 50px); /* win.height - header*/
overflow-y: auto;
box-sizing: border-box;
}
.scroll-manager {
height: 100%;
overflow: hidden;
padding-right: 8px;
}
section {
height: calc(100vh - 50px); /* win.height - header*/
overflow-y: auto;
box-sizing: border-box;
}
slot {
z-index: 1;
}
slot {
z-index: 1;
}
div {
position: relative;
}
div {
position: relative;
}
aside {
top: 52px;
right: 5px;
width: 210px;
overflow: clip;
height: auto;
opacity: 1;
}
aside {
top: 52px;
right: 5px;
width: 210px;
overflow: clip;
height: auto;
opacity: 1;
}
.content {
height: auto;
overflow-y: hidden;
padding-left: 4px;
padding-right: 4px;
overflow-x: hidden;
}
.content {
height: auto;
overflow-y: hidden;
padding-left: 4px;
padding-right: 4px;
overflow-x: hidden;
}
/* width */
::-webkit-scrollbar {
width: 6px;
}
/* width */
::-webkit-scrollbar {
width: 6px;
}
/* Track */
::-webkit-scrollbar-track {
background: #272626;
}
/* Track */
::-webkit-scrollbar-track {
background: #272626;
}
/* Handle */
::-webkit-scrollbar-thumb {
background: #949494;
border-radius: 4px;
}
/* Handle */
::-webkit-scrollbar-thumb {
background: #949494;
border-radius: 4px;
}
/* Handle on hover */
::-webkit-scrollbar-thumb:hover {
background: white;
}
/* Handle on hover */
::-webkit-scrollbar-thumb:hover {
background: white;
}
</style>

View file

@ -1,83 +1,81 @@
<script lang="ts">
import "$appcss";
import "$appcss";
import { page } from "$app/stores";
import { t } from "$libs/translations";
import { afterNavigate } from "$app/navigation";
import { packagesStore, authStore } from "$libs/stores";
import Packages from "$components/packages/packages.svelte";
import DiscoverPackages from "$components/discover-packages/discover-packages.svelte";
import { PackageStates, SideMenuOptions, type GUIPackage } from "$libs/types";
// import SortingButtons from "$components/search-packages/sorting-buttons.svelte";
import SideMenu from "$components/side-menu/side-menu.svelte";
import NotificationBar from "$components/notification-bar/notification-bar.svelte";
import WelcomeModal from "$components/welcome-modal/welcome-modal.svelte";
import Button from "@tea/ui/button/button.svelte";
import log from "$libs/logger";
import { page } from "$app/stores";
import { t } from "$libs/translations";
import { afterNavigate } from "$app/navigation";
import { packagesStore, authStore } from "$libs/stores";
import Packages from "$components/packages/packages.svelte";
import DiscoverPackages from "$components/discover-packages/discover-packages.svelte";
import { PackageStates, SideMenuOptions, type GUIPackage } from "$libs/types";
// import SortingButtons from "$components/search-packages/sorting-buttons.svelte";
import SideMenu from "$components/side-menu/side-menu.svelte";
import NotificationBar from "$components/notification-bar/notification-bar.svelte";
import WelcomeModal from "$components/welcome-modal/welcome-modal.svelte";
import Button from "@tea/ui/button/button.svelte";
import log from "$libs/logger";
const { packageList } = packagesStore;
const { session } = authStore;
const { packageList } = packagesStore;
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 sortDirection: "asc" | "desc" = "desc";
let sortBy: "popularity" | "most recent" = "most recent";
let sortDirection: "asc" | "desc" = "desc";
let updating = false;
let updating = false;
let packagesScrollY = 0;
$: currentUpdatingPkg = $packageList.find((p) => p.state === PackageStates.UPDATING);
$: updatingMessage = `updating ${currentUpdatingPkg?.full_name} (${currentUpdatingPkg?.install_progress_percentage}%)`;
let packagesScrollY = 0;
$: currentUpdatingPkg = $packageList.find((p) => p.state === PackageStates.UPDATING);
$: updatingMessage = `updating ${currentUpdatingPkg?.full_name} (${currentUpdatingPkg?.install_progress_percentage}%)`;
$: pkgsToUpdate = $packageList.filter((p: GUIPackage) => p.state === PackageStates.NEEDS_UPDATE);
async function updateAll() {
updating = true;
log.info(`updating: ${pkgsToUpdate.length} packages`);
for (const pkg of pkgsToUpdate) {
try {
await packagesStore.installPkg(pkg);
} catch (error) {
log.error(error);
}
}
updating = false;
sideMenuOption = SideMenuOptions.all;
}
$: pkgsToUpdate = $packageList.filter((p: GUIPackage) => p.state === PackageStates.NEEDS_UPDATE);
async function updateAll() {
updating = true;
log.info(`updating: ${pkgsToUpdate.length} packages`);
for (const pkg of pkgsToUpdate) {
try {
await packagesStore.installPkg(pkg);
} catch (error) {
log.error(error);
}
}
updating = false;
sideMenuOption = SideMenuOptions.all;
}
$: needsUpdateCount = pkgsToUpdate.length;
$: needsUpdateCount = pkgsToUpdate.length;
afterNavigate(({ to }) => {
if (to?.url?.pathname === "/") {
const tab = to.url.searchParams.get("tab");
sideMenuOption = !tab ? SideMenuOptions.discover : (tab as SideMenuOptions);
}
});
afterNavigate(({ to }) => {
if (to?.url?.pathname === "/") {
const tab = to.url.searchParams.get("tab");
sideMenuOption = !tab ? SideMenuOptions.discover : (tab as SideMenuOptions);
}
});
</script>
<div id="content" class="flex flex-col">
<NotificationBar />
<article class="relative h-auto w-full flex-grow overflow-hidden">
<ul class="px-2">
{#if sideMenuOption == SideMenuOptions.discover}
<DiscoverPackages
bind:scrollY={packagesScrollY}
/>
{:else}
<Packages
packageFilter={sideMenuOption}
{sortBy}
{sortDirection}
bind:scrollY={packagesScrollY}
/>
{/if}
</ul>
<header class="z-30 flex items-center justify-between" class:scrolling={packagesScrollY > 150}>
<h1 class="text-primary font-mona pl-3 text-2xl font-bold">
{$t(`side-menu-title.${sideMenuOption}`).toLowerCase()}
</h1>
<!--
<NotificationBar />
<article class="relative h-auto w-full flex-grow overflow-hidden">
<ul class="px-2">
{#if sideMenuOption == SideMenuOptions.discover}
<DiscoverPackages bind:scrollY={packagesScrollY} />
{:else}
<Packages
packageFilter={sideMenuOption}
{sortBy}
{sortDirection}
bind:scrollY={packagesScrollY}
/>
{/if}
</ul>
<header class="z-30 flex items-center justify-between" class:scrolling={packagesScrollY > 150}>
<h1 class="text-primary font-mona pl-3 text-2xl font-bold">
{$t(`side-menu-title.${sideMenuOption}`).toLowerCase()}
</h1>
<!--
<section class="border-gray mt-4 mr-4 h-10 w-48 border rounded-sm">
we might bring it back?
@ -87,63 +85,63 @@
}} />
</section>
-->
{#if needsUpdateCount && sideMenuOption === SideMenuOptions.installed_updates_available}
<!-- 22px right margin to account for the scrollbar on the package cards -->
<div class="mr-[22px] flex items-center justify-end text-sm">
{#if currentUpdatingPkg}
<p class="text-gray px-2">{updatingMessage}</p>
{/if}
<div>
<Button
class="h-8 w-48 text-xs"
loading={updating}
type="plain"
color="secondary"
onClick={updateAll}
>
{$t(`package.update-all`)} [{needsUpdateCount}]
</Button>
</div>
</div>
{/if}
</header>
</article>
{#if needsUpdateCount && sideMenuOption === SideMenuOptions.installed_updates_available}
<!-- 22px right margin to account for the scrollbar on the package cards -->
<div class="mr-[22px] flex items-center justify-end text-sm">
{#if currentUpdatingPkg}
<p class="text-gray px-2">{updatingMessage}</p>
{/if}
<div>
<Button
class="h-8 w-48 text-xs"
loading={updating}
type="plain"
color="secondary"
onClick={updateAll}
>
{$t(`package.update-all`)} [{needsUpdateCount}]
</Button>
</div>
</div>
{/if}
</header>
</article>
</div>
<SideMenu bind:activeOption={sideMenuOption} />
{#if !$session.hide_welcome}
<WelcomeModal />
<WelcomeModal />
{/if}
<style>
#content {
width: calc(100vw - 211px);
margin-left: 205px;
height: calc(100vh - 50px);
overflow: hidden;
}
#content {
width: calc(100vw - 211px);
margin-left: 205px;
height: calc(100vh - 50px);
overflow: hidden;
}
header {
position: absolute;
top: 0px;
left: 1px;
height: 72px;
width: 100%;
background-image: linear-gradient(rgba(26, 26, 26, 1), rgba(26, 26, 26, 0));
padding-top: 15px;
}
header {
position: absolute;
top: 0px;
left: 1px;
height: 72px;
width: 100%;
background-image: linear-gradient(rgba(26, 26, 26, 1), rgba(26, 26, 26, 0));
padding-top: 15px;
}
header h1 {
padding-top: 8px;
}
header h1 {
padding-top: 8px;
}
header.scrolling {
height: 60px;
background-color: #222222;
padding-top: 5px;
}
header.scrolling {
height: 60px;
background-color: #222222;
padding-top: 5px;
}
header.scrolling h1 {
padding-top: 0px;
}
header.scrolling h1 {
padding-top: 0px;
}
</style>

View file

@ -1,22 +1,22 @@
<script>
import '$appcss';
import { t } from '$libs/translations';
import PageHeader from '$components/page-header/page-header.svelte';
import FeaturedCourses from '$components/featured-courses/featured-courses.svelte';
import EssentialWorkshops from '$components/essential-workshops/essential-workshops.svelte';
import "$appcss";
import { t } from "$libs/translations";
import PageHeader from "$components/page-header/page-header.svelte";
import FeaturedCourses from "$components/featured-courses/featured-courses.svelte";
import EssentialWorkshops from "$components/essential-workshops/essential-workshops.svelte";
</script>
<div>
<PageHeader>{$t('documentation.title').toUpperCase()}</PageHeader>
<PageHeader>{$t("documentation.title").toUpperCase()}</PageHeader>
<section>
<FeaturedCourses />
</section>
<section>
<FeaturedCourses />
</section>
<section class="mt-8">
<EssentialWorkshops
title={$t('documentation.workshops').toUpperCase()}
ctaLabel={$t("view-all")}
/>
</section>
<section class="mt-8">
<EssentialWorkshops
title={$t("documentation.workshops").toUpperCase()}
ctaLabel={$t("view-all")}
/>
</section>
</div>

View file

@ -1,8 +1,8 @@
<script>
import '$appcss';
import PageHeader from '$components/page-header/page-header.svelte';
import "$appcss";
import PageHeader from "$components/page-header/page-header.svelte";
</script>
<div>
<PageHeader>Packages</PageHeader>
<PageHeader>Packages</PageHeader>
</div>

View file

@ -1,95 +1,97 @@
<script lang="ts">
import "$appcss";
import { t } from "$libs/translations";
import "$appcss";
import { t } from "$libs/translations";
import { page } from "$app/stores";
// import PageHeader from '$components/page-header/page-header.svelte';
import PackageBanner from "$components/package-banner/package-banner.svelte";
// import SuggestedPackages from '$components/suggested-packages/suggested-packages.svelte';
import Tabs from "@tea/ui/tabs/tabs.svelte";
import type { Tab } from "@tea/ui/types";
import Bottles from "@tea/ui/bottles/bottles.svelte";
import PackageMetas from "@tea/ui/package-metas/package-metas.svelte";
import Markdown from "@tea/ui/markdown/markdown.svelte";
// import PackageSnippets from '@tea/ui/package-snippets/package-snippets.svelte';
import Preloader from "@tea/ui/Preloader/Preloader.svelte";
import { page } from "$app/stores";
// import PageHeader from '$components/page-header/page-header.svelte';
import PackageBanner from "$components/package-banner/package-banner.svelte";
// import SuggestedPackages from '$components/suggested-packages/suggested-packages.svelte';
import Tabs from "@tea/ui/tabs/tabs.svelte";
import type { Tab } from "@tea/ui/types";
import Bottles from "@tea/ui/bottles/bottles.svelte";
import PackageMetas from "@tea/ui/package-metas/package-metas.svelte";
import Markdown from "@tea/ui/markdown/markdown.svelte";
// import PackageSnippets from '@tea/ui/package-snippets/package-snippets.svelte';
import Preloader from "@tea/ui/Preloader/Preloader.svelte";
/** @type {import('./$types').PageData} */
export let data: { slug: string; content: string; title: string };
/** @type {import('./$types').PageData} */
export let data: { slug: string; content: string; title: string };
import { packagesStore } from "$libs/stores";
import { onMount } from "svelte";
import NotificationBar from "$components/notification-bar/notification-bar.svelte";
import { packagesStore } from "$libs/stores";
import { onMount } from "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[];
$: bottles = pkg?.bottles || [];
$: versions = [...new Set(bottles.map((b) => b.version))];
$: readme = pkg?.readme || { data: "", type: "md" };
// let reviews: Review[];
$: bottles = pkg?.bottles || [];
$: versions = [...new Set(bottles.map((b) => b.version))];
$: readme = pkg?.readme || { data: "", type: "md" };
$: tabs = [
readme?.data !== "" && {
label: $t("common.details"),
component: Markdown,
props: { pkg, source: readme }
},
bottles?.length && {
label: `${$t("common.versions")} (${versions.length || 0})`,
component: Bottles,
props: {
bottles
}
}
].filter((t) => t && t?.label) as unknown as Tab[];
$: tabs = [
readme?.data !== "" && {
label: $t("common.details"),
component: Markdown,
props: { pkg, source: readme }
},
bottles?.length && {
label: `${$t("common.versions")} (${versions.length || 0})`,
component: Bottles,
props: {
bottles
}
}
].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(() => {
packagesStore.syncPackageData(pkg);
});
onMount(() => {
packagesStore.syncPackageData(pkg);
});
</script>
<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>
{#if tab && tab !== "all"}
<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>
<a class="hover:text-white hover:opacity-80" href="/">{$t("common.home")}</a>
{#if tab && tab !== "all"}
<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>
</header>
<div class="mx-16 mb-4">
<NotificationBar />
<NotificationBar />
</div>
{#if pkg}
<div class="px-16">
<section>
<PackageBanner {pkg} />
</section>
<div class="px-16">
<section>
<PackageBanner {pkg} />
</section>
<section class="mt-8 flex gap-8">
<div class="w-2/3">
<Tabs {tabs} defaultTab={$t("common.details")} />
</div>
<div class="w-1/3">
{#if pkg}
<PackageMetas {pkg} />
{/if}
</div>
</section>
<!-- <PageHeader class="mt-8" coverUrl="/images/headers/header_bg_1.png">SNIPPETS</PageHeader> -->
<!-- <section class="mt-8">
<section class="mt-8 flex gap-8">
<div class="w-2/3">
<Tabs {tabs} defaultTab={$t("common.details")} />
</div>
<div class="w-1/3">
{#if pkg}
<PackageMetas {pkg} />
{/if}
</div>
</section>
<!-- <PageHeader class="mt-8" coverUrl="/images/headers/header_bg_1.png">SNIPPETS</PageHeader> -->
<!-- <section class="mt-8">
<PackageSnippets />
</section> -->
<!-- <section class="mt-8">
<!-- <section class="mt-8">
<PackageReviews reviews={reviews || []} />
</section> -->
<!-- {#if pkg}
<!-- {#if pkg}
<PageHeader class="mt-8" coverUrl="/images/headers/header_bg_1.png"
>YOU MAY ALSO LIKE...</PageHeader
>
@ -97,7 +99,7 @@
<SuggestedPackages {pkg} />
</section>
{/if} -->
</div>
</div>
{:else}
<Preloader />
<Preloader />
{/if}

View file

@ -2,10 +2,10 @@ import type { LoadEvent } from "@sveltejs/kit";
/** @type {import('./$types').PageLoad} */
export function load({ params }: LoadEvent) {
// TODO: search package details here
return {
title: `${params.slug}`,
content: "",
slug: params.slug
};
// TODO: search package details here
return {
title: `${params.slug}`,
content: "",
slug: params.slug
};
}

View file

@ -3,26 +3,26 @@ import preprocess from "svelte-preprocess";
/** @type {import('@sveltejs/kit').Config} */
const config = {
// Consult https://github.com/sveltejs/svelte-preprocess
// for more information about preprocessors
preprocess: [
preprocess({
postcss: true
})
],
kit: {
adapter: adapter({
pages: "build",
assets: "build",
fallback: "app.html"
}),
alias: {
"@tea/ui/*": "../ui/src/*"
}
// ssr: false,
// hydrate the <div id="svelte"> element in src/app.html
// target: '#svelte'
}
// Consult https://github.com/sveltejs/svelte-preprocess
// for more information about preprocessors
preprocess: [
preprocess({
postcss: true
})
],
kit: {
adapter: adapter({
pages: "build",
assets: "build",
fallback: "app.html"
}),
alias: {
"@tea/ui/*": "../ui/src/*"
}
// ssr: false,
// hydrate the <div id="svelte"> element in src/app.html
// target: '#svelte'
}
};
export default config;

View file

@ -1,6 +1,6 @@
import { theme, plugins } from "@tea/ui/tailwind.config.cjs";
module.exports = {
content: ["./src/**/*.{html,svelte,ts,js}", "../ui/src/**/*.{html,svelte,ts,js}"],
theme,
plugins: [...plugins]
content: ["./src/**/*.{html,svelte,ts,js}", "../ui/src/**/*.{html,svelte,ts,js}"],
theme,
plugins: [...plugins]
};

View file

@ -1,28 +1,28 @@
{
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"allowSyntheticDefaultImports": true,
"allowJs": true,
"checkJs": true,
"esModuleInterop": true,
"moduleResolution": "node",
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true,
"types": ["vitest/globals", "@testing-library/jest-dom"],
"paths": {
"$appcss": ["src/app.css"],
"$libs/*": ["src/libs/*"],
"@native": ["src/libs/native-electron.ts"],
"$components/*": ["src/components/*"],
"@tea/ui/*": ["../ui/src/*"]
}
},
"include": ["../ui/types/*.d.ts"]
// 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
// from the referenced tsconfig.json - TypeScript does not merge them in
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"allowSyntheticDefaultImports": true,
"allowJs": true,
"checkJs": true,
"esModuleInterop": true,
"moduleResolution": "node",
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true,
"types": ["vitest/globals", "@testing-library/jest-dom"],
"paths": {
"$appcss": ["src/app.css"],
"$libs/*": ["src/libs/*"],
"@native": ["src/libs/native-electron.ts"],
"$components/*": ["src/components/*"],
"@tea/ui/*": ["../ui/src/*"]
}
},
"include": ["../ui/types/*.d.ts"]
// 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
// 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 config: UserConfig = {
plugins: [sveltekit()],
resolve: {
alias: {
"@tea/ui/*": path.resolve("../ui/src/*"),
// this dynamic-ish static importing is followed by the svelte build
// but for vscode editing intellisense tsconfig.json is being used
"@native": isMock
? path.resolve("src/libs/native-mock.ts")
: path.resolve("src/libs/native-electron.ts"),
$components: path.resolve("./src/components"),
$libs: path.resolve("./src/libs"),
$appcss: path.resolve("./src/app.css")
}
},
server: {
port: 3000,
fs: {
allow: [".."]
}
},
test: {
// Jest like globals
globals: true,
environment: "jsdom",
include: ["src/**/*.{test,spec}.ts"],
// Extend jest-dom matchers
setupFiles: ["./setupTest.js"],
coverage: {
provider: "c8"
}
}
plugins: [sveltekit()],
resolve: {
alias: {
"@tea/ui/*": path.resolve("../ui/src/*"),
// this dynamic-ish static importing is followed by the svelte build
// but for vscode editing intellisense tsconfig.json is being used
"@native": isMock
? path.resolve("src/libs/native-mock.ts")
: path.resolve("src/libs/native-electron.ts"),
$components: path.resolve("./src/components"),
$libs: path.resolve("./src/libs"),
$appcss: path.resolve("./src/app.css")
}
},
server: {
port: 3000,
fs: {
allow: [".."]
}
},
test: {
// Jest like globals
globals: true,
environment: "jsdom",
include: ["src/**/*.{test,spec}.ts"],
// Extend jest-dom matchers
setupFiles: ["./setupTest.js"],
coverage: {
provider: "c8"
}
}
};
export default config;

View file

@ -1,41 +1,41 @@
module.exports = {
root: true,
globals: {
NodeJS: true
},
parser: "@typescript-eslint/parser",
extends: [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"prettier",
"plugin:storybook/recommended"
],
plugins: ["svelte3", "@typescript-eslint"],
ignorePatterns: ["*.cjs"],
overrides: [
{
files: ["*.svelte"],
processor: "svelte3/svelte3"
}
],
settings: {
"svelte3/typescript": () => require("typescript")
},
parserOptions: {
sourceType: "module",
ecmaVersion: 2020
},
env: {
browser: true,
es2017: true,
node: true
},
rules: {
"@typescript-eslint/no-unused-vars": [
"warn",
{
argsIgnorePattern: "^_"
}
]
}
root: true,
globals: {
NodeJS: true
},
parser: "@typescript-eslint/parser",
extends: [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"prettier",
"plugin:storybook/recommended"
],
plugins: ["svelte3", "@typescript-eslint"],
ignorePatterns: ["*.cjs"],
overrides: [
{
files: ["*.svelte"],
processor: "svelte3/svelte3"
}
],
settings: {
"svelte3/typescript": () => require("typescript")
},
parserOptions: {
sourceType: "module",
ecmaVersion: 2020
},
env: {
browser: true,
es2017: true,
node: true
},
rules: {
"@typescript-eslint/no-unused-vars": [
"warn",
{
argsIgnorePattern: "^_"
}
]
}
};

View file

@ -1,10 +1,10 @@
{
"tabWidth": 2,
"useTabs": true,
"singleQuote": false,
"trailingComma": "none",
"printWidth": 100,
"plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"],
"pluginSearchDirs": ["."],
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
"tabWidth": 2,
"useTabs": false,
"singleQuote": false,
"trailingComma": "none",
"printWidth": 100,
"plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"],
"pluginSearchDirs": ["../../node_modules"],
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
}

View file

@ -1,16 +1,16 @@
const path = require("path");
module.exports = {
stories: ["../src/**/*.stories.mdx", "../src/**/*.stories.@(js|jsx|ts|tsx|svelte)"],
addons: [
"@storybook/addon-links",
"@storybook/addon-essentials",
"@storybook/addon-interactions"
],
framework: {
name: "@storybook/svelte-vite",
options: {}
},
docs: {
docsPage: true
}
stories: ["../src/**/*.stories.mdx", "../src/**/*.stories.@(js|jsx|ts|tsx|svelte)"],
addons: [
"@storybook/addon-links",
"@storybook/addon-essentials",
"@storybook/addon-interactions"
],
framework: {
name: "@storybook/svelte-vite",
options: {}
},
docs: {
docsPage: true
}
};

Some files were not shown because too many files have changed in this diff Show more