mirror of
https://github.com/ivabus/gui
synced 2025-04-23 14:07:14 +03:00
add install button to search results and package page (#372)
* add install button to search results and packae page
This commit is contained in:
parent
5a81d87839
commit
c8cf0e8c1f
8 changed files with 147 additions and 138 deletions
|
@ -1,37 +1,44 @@
|
|||
<script lang="ts">
|
||||
import type { GUIPackage } from "$libs/types";
|
||||
import Package from "$components/packages/package.svelte";
|
||||
import { PackageStates, type GUIPackage } from "$libs/types";
|
||||
import clickOutside from "@tea/ui/lib/clickOutside";
|
||||
import PackageStateButton from "./package-state-button.svelte";
|
||||
|
||||
export let buttonSize: "small" | "large" = "small";
|
||||
export let pkg: GUIPackage;
|
||||
export let availableVersions: string[] = [];
|
||||
|
||||
export let onClickCTA = async (_version: string) => {
|
||||
export let onClick = async (_version: string) => {
|
||||
console.log("do nothing");
|
||||
};
|
||||
|
||||
$: isOpened = false;
|
||||
|
||||
const toggleOpen = () => {
|
||||
if ([PackageStates.INSTALLING, PackageStates.UPDATING].includes(pkg.state)) {
|
||||
return;
|
||||
}
|
||||
isOpened = !isOpened;
|
||||
};
|
||||
|
||||
const handleClick = (version: string) => {
|
||||
isOpened = false;
|
||||
onClickCTA(version);
|
||||
onClick(version);
|
||||
};
|
||||
|
||||
const handleClickOutside = () => (isOpened = false);
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col justify-end">
|
||||
<div class="dropdown" use:clickOutside on:click_outside={handleClickOutside}>
|
||||
<PackageStateButton {pkg} onClick={() => (isOpened = !isOpened)} />
|
||||
<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 text-xs" on:click={() => handleClick(version)}>
|
||||
v{version + (idx === 0 ? " (latest)" : "")}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="dropdown" use:clickOutside on:click_outside={handleClickOutside}>
|
||||
<PackageStateButton {buttonSize} {pkg} onClick={toggleOpen} />
|
||||
<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 text-xs" on:click={() => handleClick(version)}>
|
||||
v{version + (idx === 0 ? " (latest)" : "")}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -67,7 +74,7 @@
|
|||
.dropdown {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 130px;
|
||||
min-width: 130px;
|
||||
}
|
||||
|
||||
.divider {
|
|
@ -3,6 +3,8 @@
|
|||
import Button from "@tea/ui/button/button.svelte";
|
||||
import { t } from "$libs/translations";
|
||||
|
||||
export let buttonSize: "small" | "large" = "small";
|
||||
|
||||
export let pkg: GUIPackage;
|
||||
export let onClick = () => {
|
||||
console.log("do nothing");
|
||||
|
@ -22,7 +24,7 @@
|
|||
</script>
|
||||
|
||||
<Button
|
||||
class="h-8 w-full border text-xs text-white "
|
||||
class={`w-full border text-xs text-white ${buttonSize === "small" ? "h-8" : "h-10"}`}
|
||||
type="plain"
|
||||
color={getColor(pkg.state)}
|
||||
{onClick}
|
|
@ -1,12 +1,14 @@
|
|||
<script lang="ts">
|
||||
import '$appcss';
|
||||
import '@tea/ui/icons/icons.css';
|
||||
import Button from '@tea/ui/button/button.svelte';
|
||||
import "$appcss";
|
||||
import "@tea/ui/icons/icons.css";
|
||||
import Button from "@tea/ui/button/button.svelte";
|
||||
|
||||
import { installPackage } from '@native';
|
||||
import { PackageStates, type GUIPackage } from '$libs/types';
|
||||
import { packagesStore } from '$libs/stores';
|
||||
import { shellOpenExternal } from '@native';
|
||||
import { installPackage } from "@native";
|
||||
import { PackageStates, type GUIPackage } from "$libs/types";
|
||||
import { packagesStore } from "$libs/stores";
|
||||
import { shellOpenExternal } from "@native";
|
||||
import InstallButton from "$components/install-button/install-button.svelte";
|
||||
import { findAvailableVersions } from "$libs/packages/pkg-utils";
|
||||
|
||||
export let pkg: GUIPackage;
|
||||
let installing = false;
|
||||
|
@ -16,10 +18,9 @@
|
|||
await installPackage(pkg);
|
||||
installing = false;
|
||||
packagesStore.updatePackage(pkg.full_name, {
|
||||
state: PackageStates.INSTALLED,
|
||||
state: PackageStates.INSTALLED
|
||||
});
|
||||
}
|
||||
|
||||
};
|
||||
</script>
|
||||
|
||||
<section class="mt-4 bg-black">
|
||||
|
@ -35,24 +36,22 @@
|
|||
</a>
|
||||
{/if}
|
||||
<p class="mt-4 text-sm">{pkg.desc}</p>
|
||||
<menu class="h-10 grid grid-cols-2 gap-4 mt-4 text-xs">
|
||||
{#if pkg.state === PackageStates.INSTALLED}
|
||||
<Button type="plain" color="primary">
|
||||
Latest version installed v{pkg.version}
|
||||
</Button>
|
||||
{:else if pkg.state === PackageStates.AVAILABLE}
|
||||
<Button type="plain" color="secondary" onClick={install} loading={installing}>
|
||||
{installing ? "Installing" : "Install"} v{pkg.version}
|
||||
</Button>
|
||||
{:else if pkg.state === PackageStates.NEEDS_UPDATE}
|
||||
<Button type="plain" color="secondary" onClick={install} loading={installing}>
|
||||
{installing ? "Updating" : "Update"} to v{pkg.version}
|
||||
</Button>
|
||||
{/if}
|
||||
<menu class="mt-4 grid h-10 grid-cols-2 gap-4 text-xs">
|
||||
<InstallButton
|
||||
buttonSize="large"
|
||||
{pkg}
|
||||
availableVersions={findAvailableVersions(pkg)}
|
||||
onClick={install}
|
||||
/>
|
||||
{#if pkg.github}
|
||||
<Button class="h-10" type="plain" color="black" onClick={() => {
|
||||
shellOpenExternal(`https://github.com/${pkg.github}`)
|
||||
}}>View on github</Button>
|
||||
<Button
|
||||
class="h-10"
|
||||
type="plain"
|
||||
color="black"
|
||||
onClick={() => {
|
||||
shellOpenExternal(`https://github.com/${pkg.github}`);
|
||||
}}>View on github</Button
|
||||
>
|
||||
{/if}
|
||||
</menu>
|
||||
</article>
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
import "../../app.css";
|
||||
import ImgLoader from "@tea/ui/img-loader/img-loader.svelte";
|
||||
import ProgressCircle from "@tea/ui/progress-circle/progress-circle.svelte";
|
||||
import InstallButton from "./install-button.svelte";
|
||||
import InstallButton from "../install-button/install-button.svelte";
|
||||
import type { GUIPackage } from "$libs/types";
|
||||
import VersionLabel from "./version-label.svelte";
|
||||
|
||||
|
@ -39,7 +39,9 @@
|
|||
</a>
|
||||
<footer class="absolute bottom-0 left-0 flex w-full items-stretch justify-between gap-2 p-2">
|
||||
<VersionLabel {pkg} {availableVersions} />
|
||||
<InstallButton {pkg} {availableVersions} {onClickCTA} />
|
||||
<div class="flex flex-col justify-end">
|
||||
<InstallButton {pkg} {availableVersions} onClick={onClickCTA} />
|
||||
</div>
|
||||
</footer>
|
||||
{#if progessLoading > 0 && progessLoading < 100}
|
||||
<div class="absolute top-0 left-0 h-full w-full bg-black bg-opacity-50">
|
||||
|
|
|
@ -4,16 +4,13 @@
|
|||
import type { GUIPackage } from "$libs/types";
|
||||
import { packagesStore, notificationStore } from "$libs/stores";
|
||||
import { onMount } from "svelte";
|
||||
import semverCompare from "semver/functions/compare";
|
||||
import { clean } from "semver";
|
||||
import PackageCard from "$components/package-card/package-card.svelte";
|
||||
import { findAvailableVersions } from "$libs/packages/pkg-utils";
|
||||
|
||||
export let tab = "all";
|
||||
export let pkg: GUIPackage;
|
||||
export let onClick: (version: string) => void;
|
||||
|
||||
let fakeLoadingProgress = 0;
|
||||
|
||||
const onClickCTA = async (version: string) => {
|
||||
await onClick(version);
|
||||
notificationStore.add({
|
||||
|
@ -21,24 +18,6 @@
|
|||
});
|
||||
};
|
||||
|
||||
const findAvailableVersions = (pkg: GUIPackage) => {
|
||||
// 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);
|
||||
}
|
||||
|
||||
return Array.from(versionSet).sort((a, b) => semverCompare(cleanVersion(b), cleanVersion(a)));
|
||||
};
|
||||
|
||||
const cleanVersion = (version: string) => {
|
||||
return clean(version) || "0.0.0";
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
packagesStore.fetchPackageBottles(pkg.full_name);
|
||||
});
|
||||
|
|
|
@ -1,20 +1,20 @@
|
|||
<script lang="ts">
|
||||
import '$appcss';
|
||||
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 type { GUIPackage } from "$libs/types";
|
||||
import moment from "moment";
|
||||
import { PackageStates, SideMenuOptions } from '$libs/types';
|
||||
import Preloader from '@tea/ui/Preloader/Preloader.svelte';
|
||||
import { PackageStates, SideMenuOptions } from "$libs/types";
|
||||
import Preloader from "@tea/ui/Preloader/Preloader.svelte";
|
||||
import Package from "./package.svelte";
|
||||
import { packagesStore } from '$libs/stores';
|
||||
import { packagesStore } from "$libs/stores";
|
||||
|
||||
const { packages: allPackages } = packagesStore;
|
||||
export let packageFilter: SideMenuOptions = SideMenuOptions.all;
|
||||
|
||||
export let sortBy: "popularity" | "most recent" = 'popularity';
|
||||
export let sortDirection: 'asc' | 'desc' = 'desc';
|
||||
export let sortBy: "popularity" | "most recent" = "popularity";
|
||||
export let sortDirection: "asc" | "desc" = "desc";
|
||||
|
||||
export let scrollY = 0;
|
||||
|
||||
|
@ -22,7 +22,7 @@
|
|||
let limit = loadMore;
|
||||
|
||||
// TODO: figure out a better type strategy here so that this breaks if SideMenuOptions is updated
|
||||
const pkgFilters: { [key:string]: (pkg: GUIPackage) => boolean } = {
|
||||
const pkgFilters: { [key: string]: (pkg: GUIPackage) => boolean } = {
|
||||
[SideMenuOptions.all]: (_pkg: GUIPackage) => true,
|
||||
[SideMenuOptions.installed]: (pkg: GUIPackage) => {
|
||||
return [
|
||||
|
@ -41,32 +41,34 @@
|
|||
[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.star_struct]: (pkg: GUIPackage) => pkg.categories.includes(SideMenuOptions.star_struct),
|
||||
[SideMenuOptions.made_by_tea]: (pkg: GUIPackage) => pkg.full_name.includes("tea.xyz"),
|
||||
}
|
||||
[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.star_struct]: (pkg: GUIPackage) =>
|
||||
pkg.categories.includes(SideMenuOptions.star_struct),
|
||||
[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;
|
||||
}
|
||||
};
|
||||
|
||||
$: 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;
|
||||
|
@ -76,18 +78,15 @@
|
|||
const addLimit = 3 * (minCardRows - cardRows);
|
||||
limit += addLimit;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<ul class="grid grid-cols-3 gap-2 bg-black"
|
||||
use:watchResize={onResize}
|
||||
on:scroll={onScroll}
|
||||
>
|
||||
<ul class="grid grid-cols-3 gap-2 bg-black" use:watchResize={onResize} on:scroll={onScroll}>
|
||||
{#if packages.length > 0}
|
||||
{#each packages as pkg, index}
|
||||
{#if index < limit}
|
||||
<div class={pkg.state === PackageStates.INSTALLING ? 'animate-pulse' : ''}>
|
||||
<div class={pkg.state === PackageStates.INSTALLING ? "animate-pulse" : ""}>
|
||||
<Package
|
||||
tab={packageFilter}
|
||||
{pkg}
|
||||
|
@ -103,7 +102,7 @@
|
|||
</section>
|
||||
{/each}
|
||||
{/if}
|
||||
<InfiniteScroll threshold={100} on:loadMore={() => limit += loadMore} />
|
||||
<InfiniteScroll threshold={100} on:loadMore={() => (limit += loadMore)} />
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
@ -124,9 +123,9 @@
|
|||
|
||||
/* Track */
|
||||
::-webkit-scrollbar-track {
|
||||
background: #272626;
|
||||
background: #272626;
|
||||
}
|
||||
|
||||
|
||||
/* Handle */
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #949494;
|
||||
|
@ -135,6 +134,6 @@
|
|||
|
||||
/* Handle on hover */
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: white;
|
||||
background: white;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,34 +1,35 @@
|
|||
<script lang="ts">
|
||||
import type { GUIPackage } from '$libs/types';
|
||||
import Button from "@tea/ui/button/button.svelte";
|
||||
import ImgLoader from "@tea/ui/img-loader/img-loader.svelte";
|
||||
export let pkg:GUIPackage;
|
||||
export let ctaLabel = "INSTALL";
|
||||
export let ctaType: "ghost" | "plain" = "plain";
|
||||
export let ctaColor: "green" | "secondary" = "secondary";
|
||||
export let onClick: () => void;
|
||||
import InstallButton from "$components/install-button/install-button.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 { findAvailableVersions } from "$libs/packages/pkg-utils";
|
||||
export let pkg: GUIPackage;
|
||||
export let onClick: (_version: string) => Promise<void>;
|
||||
|
||||
onMount(() => {
|
||||
packagesStore.fetchPackageBottles(pkg.full_name);
|
||||
});
|
||||
|
||||
const availableVersions = findAvailableVersions(pkg);
|
||||
</script>
|
||||
|
||||
<figure class="flex flex-row p-2 gap-2 border border-gray">
|
||||
<ImgLoader
|
||||
class="pkg-image object-cover w-16 h-16"
|
||||
src={!pkg.thumb_image_url.includes("https://tea.xyz")
|
||||
? "https://tea.xyz/Images/package-thumb-nolabel4.jpg"
|
||||
: pkg.thumb_image_url}
|
||||
alt={pkg.name}
|
||||
/>
|
||||
<header class="flex-grow">
|
||||
<h1>{pkg.full_name}</h1>
|
||||
<p class="text-xs line-clamp-2">{pkg.desc}</p>
|
||||
</header>
|
||||
<aside>
|
||||
<Button
|
||||
class="h-8 w-1/2 border border-gray p-2 text-xs"
|
||||
onClick={onClick}
|
||||
type={ctaType}
|
||||
color={ctaColor}
|
||||
>{ctaLabel}
|
||||
</Button>
|
||||
<footer class="text-xs text-center mt-2">v{pkg.version}</footer>
|
||||
</aside>
|
||||
</figure>
|
||||
<figure class="border-gray flex flex-row gap-2 border p-2">
|
||||
<ImgLoader
|
||||
class="pkg-image h-16 w-16 object-cover"
|
||||
src={!pkg.thumb_image_url.includes("https://tea.xyz")
|
||||
? "https://tea.xyz/Images/package-thumb-nolabel4.jpg"
|
||||
: pkg.thumb_image_url}
|
||||
alt={pkg.name}
|
||||
/>
|
||||
<header class="flex-grow">
|
||||
<h1>{pkg.full_name}</h1>
|
||||
<p class="line-clamp-2 text-xs">{pkg.desc}</p>
|
||||
</header>
|
||||
<aside>
|
||||
<InstallButton {pkg} {availableVersions} {onClick} />
|
||||
<footer class="mt-2 text-center text-xs">v{pkg.version}</footer>
|
||||
</aside>
|
||||
</figure>
|
||||
|
|
20
modules/desktop/src/libs/packages/pkg-utils.ts
Normal file
20
modules/desktop/src/libs/packages/pkg-utils.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
import type { GUIPackage } from "$libs/types";
|
||||
import { clean } from "semver";
|
||||
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];
|
||||
}
|
||||
|
||||
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)));
|
||||
};
|
||||
|
||||
export const cleanVersion = (version: string) => clean(version) || "0.0.0";
|
Loading…
Reference in a new issue