Merge pull request #59 from teaxyz/install-package-on-click

#55 install state and install on click
This commit is contained in:
Neil 2022-12-04 14:06:21 +08:00 committed by GitHub
commit a61778de55
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 241 additions and 90 deletions

View file

@ -1622,6 +1622,16 @@ dependencies = [
"vcpkg", "vcpkg",
] ]
[[package]]
name = "os_pipe"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6a252f1f8c11e84b3ab59d7a488e48e4478a93937e027076638c49536204639"
dependencies = [
"libc",
"windows-sys 0.42.0",
]
[[package]] [[package]]
name = "overload" name = "overload"
version = "0.1.1" version = "0.1.1"
@ -2339,6 +2349,16 @@ dependencies = [
"lazy_static", "lazy_static",
] ]
[[package]]
name = "shared_child"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0d94659ad3c2137fef23ae75b03d5241d633f8acded53d672decfa0e6e0caef"
dependencies = [
"libc",
"winapi",
]
[[package]] [[package]]
name = "siphasher" name = "siphasher"
version = "0.3.10" version = "0.3.10"
@ -2552,6 +2572,7 @@ dependencies = [
"objc", "objc",
"once_cell", "once_cell",
"open", "open",
"os_pipe",
"percent-encoding", "percent-encoding",
"rand 0.8.5", "rand 0.8.5",
"raw-window-handle", "raw-window-handle",
@ -2561,6 +2582,7 @@ dependencies = [
"serde_json", "serde_json",
"serde_repr", "serde_repr",
"serialize-to-javascript", "serialize-to-javascript",
"shared_child",
"state", "state",
"tar", "tar",
"tauri-macros", "tauri-macros",

View file

@ -17,7 +17,7 @@ tauri-build = { version = "1.2.0", features = [] }
[dependencies] [dependencies]
serde_json = "1.0" serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
tauri = { version = "1.2.0", features = ["http-all", "shell-open", "window-all"] } tauri = { version = "1.2.0", features = ["fs-read-dir", "http-all", "shell-all", "window-all"] }
uuid = "1.2.1" uuid = "1.2.1"
futures = "0.3" futures = "0.3"

View file

@ -1,4 +1,5 @@
#[tauri::command] #[tauri::command]
pub fn install_package(package: String) { pub fn install_package(package: String) {
println!("installing: {}", package); println!("installing: {}", package);
} }

View file

@ -18,10 +18,26 @@
"scope": ["https://api.tea.xyz/v1/*", "https://github.com/*"] "scope": ["https://api.tea.xyz/v1/*", "https://github.com/*"]
}, },
"shell": { "shell": {
"all": false, "all": true,
"execute": false, "execute": true,
"open": true, "open": true,
"scope": [], "scope": [
{
"name": "tea-install",
"cmd": "tea",
"args": [{ "validator": "\\S+" }, "true"]
},
{
"name": "node",
"cmd": "node",
"args": ["--version"]
},
{
"name": "list-packages",
"cmd": "ls",
"args": ["-R ~/.tea/tea.xyz/var/www | grep 'xz\\|gz'"]
}
],
"sidecar": false "sidecar": false
}, },
"window": { "window": {
@ -55,6 +71,13 @@
"startDragging": true, "startDragging": true,
"unmaximize": true, "unmaximize": true,
"unminimize": true "unminimize": true
},
"fs": {
"readDir": true,
"scope": [
"$HOME/.tea/*",
"$APPDATA/*"
]
} }
}, },
"bundle": { "bundle": {

View file

@ -3,42 +3,46 @@
import Fuse from 'fuse.js'; import Fuse from 'fuse.js';
import { packages as packagesStore, initializePackages } from '$libs/stores'; import { packages as packagesStore, initializePackages } from '$libs/stores';
import SortingButtons from './SortingButtons.svelte'; import SortingButtons from './SortingButtons.svelte';
import type { Package } from '@tea/ui/types'; import type { GUIPackage } from '$libs/types';
import { PackageStates } from '$libs/types';
import PackageCard from '@tea/ui/PackageCard/PackageCard.svelte'; import PackageCard from '@tea/ui/PackageCard/PackageCard.svelte';
import SearchInput from '@tea/ui/SearchInput/SearchInput.svelte'; import SearchInput from '@tea/ui/SearchInput/SearchInput.svelte';
import Preloader from '@tea/ui/Preloader/Preloader.svelte'; import Preloader from '@tea/ui/Preloader/Preloader.svelte';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
let allPackages: Package[] = []; import { installPackage } from '@api';
let packagesIndex: Fuse<Package>;
let packages: Package[] = []; let allPackages: GUIPackage[] = [];
let packagesIndex: Fuse<GUIPackage>;
let packages: GUIPackage[] = [];
let initialized = false; let initialized = false;
let sortBy = 'popularity'; let sortBy = 'popularity';
let sortDirection: 'asc' | 'desc' = 'desc'; let sortDirection: 'asc' | 'desc' = 'desc';
const searchLimit = 5; const searchLimit = 10;
const setPackages = (pkgs: Package[]) => { const setPackages = (pkgs: GUIPackage[], isSearch?: boolean) => {
console.log('pkgs sub', pkgs); packages = isSearch
packages = pkgs.sort((a, b) => { ? pkgs
if (sortBy === 'popularity') { : pkgs.sort((a, b) => {
const aPop = +a.dl_count + a.installs; if (sortBy === 'popularity') {
const bPop = +b.dl_count + b.installs; const aPop = +a.dl_count + a.installs;
return sortDirection === 'asc' ? aPop - bPop : bPop - aPop; const bPop = +b.dl_count + b.installs;
} else { return sortDirection === 'asc' ? aPop - bPop : bPop - aPop;
// most recent } else {
const aDate = new Date(a.last_modified); // most recent
const bDate = new Date(b.last_modified); const aDate = new Date(a.last_modified);
return sortDirection === 'asc' ? +aDate - +bDate : +bDate - +aDate; const bDate = new Date(b.last_modified);
} return sortDirection === 'asc' ? +aDate - +bDate : +bDate - +aDate;
}); }
});
}; };
packagesStore.subscribe((v) => { packagesStore.subscribe((v) => {
allPackages = v; allPackages = v;
setPackages(allPackages); setPackages(allPackages);
if (!packagesIndex) { if (!packagesIndex && allPackages.length) {
// dont remove or this can get crazy // dont remove or this can get crazy
packagesIndex = new Fuse(allPackages, { packagesIndex = new Fuse(allPackages, {
keys: ['name', 'full_name', 'desc'] keys: ['name', 'full_name', 'desc']
@ -54,15 +58,11 @@
}); });
const onSearch = (term: string) => { const onSearch = (term: string) => {
if (term !== '' && term.length > 3) { if (term !== '' && term.length > 1) {
const res = packagesIndex.search(term); const res = packagesIndex.search(term, { limit: searchLimit });
const matchingPackages = []; const matchingPackages: GUIPackage[] = res.map((v) => v.item);
for (let i = 0; i < searchLimit; i++) {
if (res[i]) { setPackages(matchingPackages, true);
matchingPackages.push(res[i].item);
}
}
setPackages(matchingPackages);
} else { } else {
setPackages(allPackages); setPackages(allPackages);
} }
@ -73,6 +73,15 @@
sortDirection = dir; sortDirection = dir;
setPackages(packages); setPackages(packages);
}; };
const getCTALabel = (state: PackageStates): string => {
return {
[PackageStates.AVAILABLE]: 'INSTALL',
[PackageStates.INSTALLED]: 'INSTALLED',
[PackageStates.INSTALLING]: 'INSTALLING',
[PackageStates.UNINSTALLED]: 'RE-INSTALL'
}[state];
};
</script> </script>
<div class="border border-gray bg-black"> <div class="border border-gray bg-black">
@ -87,9 +96,24 @@
</div> </div>
</section> </section>
<ul class="grid grid-cols-3"> <ul class="grid grid-cols-3">
{#if packages.length} {#if packages.length > 0}
{#each packages as pkg} {#each packages as pkg}
<PackageCard {pkg} link={`/packages/${pkg.slug}`} /> <div class={pkg.state === PackageStates.INSTALLING ? 'animate-pulse' : ''}>
<PackageCard
{pkg}
link={`/packages/${pkg.slug}`}
ctaLabel={getCTALabel(pkg.state)}
onClickCTA={async () => {
try {
pkg.state = PackageStates.INSTALLING;
await installPackage(pkg.full_name);
pkg.state = PackageStates.INSTALLED;
} catch (error) {
console.error(error);
}
}}
/>
</div>
{/each} {/each}
{:else} {:else}
{#each Array(12) as _} {#each Array(12) as _}

View file

@ -6,6 +6,8 @@
* * make cors work with api.tea.xyz/v1 * * make cors work with api.tea.xyz/v1
*/ */
import type { Package, Review } from '@tea/ui/types'; import type { Package, Review } from '@tea/ui/types';
import type { GUIPackage } from '../types';
import { PackageStates } from '../types';
import { loremIpsum } from 'lorem-ipsum'; import { loremIpsum } from 'lorem-ipsum';
import _ from 'lodash'; import _ from 'lodash';
@ -152,9 +154,14 @@ const packages: Package[] = [
} }
]; ];
export async function getPackages(): Promise<Package[]> { export async function getPackages(): Promise<GUIPackage[]> {
await delay(2000); await delay(2000);
return packages; return packages.map((pkg) => {
return {
...pkg,
state: PackageStates.AVAILABLE
};
});
} }
export async function getFeaturedPackages(): Promise<Package[]> { export async function getFeaturedPackages(): Promise<Package[]> {
@ -201,6 +208,11 @@ export async function getPackageReviews(full_name: string): Promise<Review[]> {
return reviews; return reviews;
} }
export async function installPackage(full_name: string) {
console.log('installing: ', full_name);
await delay(10000);
}
function delay(ms: number) { function delay(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms)); return new Promise((resolve) => setTimeout(resolve, ms));
} }

View file

@ -11,9 +11,14 @@
* - connect to a local platform api and returns a data * - connect to a local platform api and returns a data
*/ */
import { getClient } from '@tauri-apps/api/http'; import { getClient } from '@tauri-apps/api/http';
// import { invoke } from '@tauri-apps/api';
import { Command } from '@tauri-apps/api/shell';
import { readDir, BaseDirectory } from '@tauri-apps/api/fs';
import { Buffer } from 'buffer'; import { Buffer } from 'buffer';
import type { Package, Review } from '../types'; import type { Package, Review } from '@tea/ui/types';
import type { GUIPackage } from '../types';
import * as mock from './mock'; import * as mock from './mock';
import { PackageStates } from '../types';
const username = 'user'; const username = 'user';
const password = 'password'; const password = 'password';
@ -46,9 +51,20 @@ const join = function (...paths: string[]) {
.join('/'); .join('/');
}; };
export async function getPackages(): Promise<Package[]> { export async function getPackages(): Promise<GUIPackage[]> {
const packages = await get<Package[]>('packages'); const [packages, installedPackages] = await Promise.all([
return packages; get<Package[]>('packages'),
getInstalledPackages()
]);
return packages.map((pkg) => {
const found = installedPackages.find((p) => p.full_name === pkg.full_name);
return {
...pkg,
state: found ? PackageStates.INSTALLED : PackageStates.AVAILABLE,
installed_version: found ? found.version : ''
};
});
} }
export async function getFeaturedPackages(): Promise<Package[]> { export async function getFeaturedPackages(): Promise<Package[]> {
@ -62,3 +78,59 @@ export async function getPackageReviews(full_name: string): Promise<Review[]> {
return reviews; return reviews;
} }
export async function installPackage(full_name: string) {
try {
await installPackageCommand(full_name);
} catch (error) {
console.error(error);
}
}
async function installPackageCommand(full_name: string) {
return new Promise((resolve, reject) => {
const teaInstallCommand = new Command('tea-install', [`+${full_name}`, 'true']);
teaInstallCommand.on('error', reject);
const handleLineOutput = async (line: string | any) => {
const c = await child;
if (line?.code === 0 || line.includes('installed:')) {
c.kill();
resolve(c.pid);
} else if (line?.code === 1) {
reject();
}
};
teaInstallCommand.stdout.on('data', handleLineOutput);
teaInstallCommand.stderr.on('data', handleLineOutput);
teaInstallCommand.on('close', (line: string) => {
console.log('command closed!');
handleLineOutput(line || '');
});
teaInstallCommand.on('error', (line: string) => {
console.log('command error!', line);
handleLineOutput(line || '');
});
const child = teaInstallCommand.spawn();
});
}
async function getInstalledPackages() {
const entries = await readDir('.tea/tea.xyz/var/www', {
dir: BaseDirectory.Home,
recursive: false
});
const packages = entries
.filter((o) => o.path.match('^(.*).(g|x)z$'))
.map((o) => {
const [pkg_version] = (o?.name || '').split('+');
const version = pkg_version.split('-').pop();
const full_name = pkg_version.replace(`-${version}`, '');
return {
full_name,
version
};
});
return packages;
}

View file

@ -1,13 +1,13 @@
import { writable } from 'svelte/store'; import { writable } from 'svelte/store';
import type { Package, Review } from '@tea/ui/types'; import type { Package, Review } from '@tea/ui/types';
import type { GUIPackage } from '$libs/types';
// TODO: figure out a better structure for managing states maybe turn them into models? // TODO: figure out a better structure for managing states maybe turn them into models?
import { getPackages, getFeaturedPackages, getPackageReviews } from '@api'; import { getPackages, getFeaturedPackages, getPackageReviews } from '@api';
export const backLink = writable<string>('/'); export const backLink = writable<string>('/');
export const packages = writable<Package[]>([]); export const packages = writable<GUIPackage[]>([]);
export const featuredPackages = writable<Package[]>([]); export const featuredPackages = writable<Package[]>([]);

View file

@ -2,3 +2,17 @@
// else // else
// please use the package @tea/ui/src/types.ts // please use the package @tea/ui/src/types.ts
// things that go there are shared types/shapes like ie: Package // things that go there are shared types/shapes like ie: Package
import type { Package } from '@tea/ui/types';
export enum PackageStates {
AVAILABLE,
INSTALLED,
INSTALLING,
UNINSTALLED
}
export type GUIPackage = Package & {
state: PackageStates;
installed_version?: string;
};

View file

@ -2,29 +2,37 @@
import '../app.css'; import '../app.css';
import type { Package } from '../types'; import type { Package } from '../types';
import ImgLoader from '../ImgLoader/ImgLoader.svelte'; import ImgLoader from '../ImgLoader/ImgLoader.svelte';
export let pkg: Package; export let pkg: Package;
export let link: string; export let link: string;
export let ctaLabel: string;
export let onClickCTA = () => {
console.log('do nothing');
};
</script> </script>
<section class="package-card border border-gray p-4"> <section class="package-card border border-gray p-4">
<figure> <a href={link}>
<ImgLoader <figure>
class="pkg-image" <ImgLoader
src={!pkg.thumb_image_url.includes('https://tea.xyz') class="pkg-image"
? 'https://tea.xyz/Images/package-thumb-nolabel4.jpg' src={!pkg.thumb_image_url.includes('https://tea.xyz')
: pkg.thumb_image_url} ? 'https://tea.xyz/Images/package-thumb-nolabel4.jpg'
alt={pkg.name} : pkg.thumb_image_url}
/> alt={pkg.name}
<article class="card-thumb-label"> />
<i class="icon-tea-logo-iconasset-1"> <article class="card-thumb-label">
<!-- TODO: replace with icon.svg --> <i class="icon-tea-logo-iconasset-1">
</i> <!-- TODO: replace with icon.svg -->
<h3>{pkg.name}</h3> </i>
{#if pkg.maintainer} <h3>{pkg.name}</h3>
<h4>&#x2022;&nbsp;{pkg.maintainer}</h4> {#if pkg.maintainer}
{/if} <h4>&#x2022;&nbsp;{pkg.maintainer}</h4>
</article> {/if}
</figure> </article>
</figure>
</a>
<footer class="mt-4 flex items-center justify-between"> <footer class="mt-4 flex items-center justify-between">
<div> <div>
<p> <p>
@ -35,10 +43,7 @@
<span class="package-install-no">>{{- .installs -}}&nbsp;installs</span> --> <span class="package-install-no">>{{- .installs -}}&nbsp;installs</span> -->
</p> </p>
</div> </div>
<!-- TODO: move this button into its own reusable component --> <button class="p-2 font-machina" on:click={onClickCTA}>{ctaLabel}</button>
<a href={link}>
<button class="detail-btn"><i class="icon-enter-arrow" />details</button>
</a>
</footer> </footer>
</section> </section>
@ -91,40 +96,18 @@
padding: 0px; padding: 0px;
} }
.detail-btn { button {
position: relative;
float: right;
right: 0;
display: inline-block;
font-family: 'pp-neue-machina', sans-serif;
background-color: #1a1a1a; background-color: #1a1a1a;
border: 0.5px solid #ffffff; border: 0.5px solid #ffffff;
color: #fff; color: #fff;
padding-top: 0.279vw;
text-decoration: none; text-decoration: none;
text-transform: uppercase; text-transform: uppercase;
width: 120px; min-width: 120px;
height: 2.232vw;
min-height: 34px;
transition: 0.1s linear; transition: 0.1s linear;
} }
.detail-btn:hover { button:hover {
background-color: #8000ff; background-color: #8000ff;
box-shadow: inset 0vw 0vw 0vw 0.223vw #1a1a1a !important; box-shadow: inset 0vw 0vw 0vw 0.223vw #1a1a1a !important;
} }
/* Icon Styling */
.detail-btn .icon-enter-arrow {
display: inline-block;
position: relative;
margin-right: 0.558vw;
transition: 0.2s ease-in-out;
}
.detail-btn:hover .icon-enter-arrow {
display: inline-block;
transform: rotate(-45deg) !important;
}
</style> </style>