mirror of
https://github.com/ivabus/gui
synced 2025-04-23 14:07:14 +03:00
init cmd+k open search (#399)
* #398 init cmd+k open search * #398 ctrl+shift+del clear search --------- Co-authored-by: neil molina <neil@neils-MacBook-Pro.local>
This commit is contained in:
parent
01f2118093
commit
d2cf2cb4a6
7 changed files with 202 additions and 119 deletions
|
@ -78,6 +78,7 @@
|
|||
"@sentry/electron": "^4.3.0",
|
||||
"@sentry/svelte": "^7.38.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",
|
||||
|
@ -97,6 +98,7 @@
|
|||
"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",
|
||||
|
|
|
@ -1,27 +1,28 @@
|
|||
<script lang="ts">
|
||||
import { 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';
|
||||
|
||||
const { searching, packagesSearch } = searchStore;
|
||||
// import type { AirtablePost } from '@tea/ui/types';
|
||||
let term: string;
|
||||
let packages: GUIPackage[] = [];
|
||||
// let articles: AirtablePost[] = []; // news, blogs, etc
|
||||
// let workshops: AirtablePost[] = []; // workshops, course
|
||||
let loading = true;
|
||||
|
||||
searchStore.subscribe((v) => {
|
||||
term = v;
|
||||
});
|
||||
searchStore.packagesSearch.subscribe((pkgs) => {
|
||||
packages = pkgs;
|
||||
});
|
||||
// searchStore.packagesSearch.subscribe((pkgs) => {
|
||||
// packages = pkgs;
|
||||
// });
|
||||
// searchStore.postsSearch.subscribe((posts) => {
|
||||
// let partialArticles: AirtablePost[] = [];
|
||||
// let partialWorkshops: AirtablePost[] = [];
|
||||
|
@ -42,110 +43,141 @@
|
|||
|
||||
const onClose = () => {
|
||||
term = '';
|
||||
searchStore.searching.set(false);
|
||||
};
|
||||
</script>
|
||||
|
||||
<section class={term ? 'show' : ''}>
|
||||
{#if $searching}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<figure on:click={onClose} />
|
||||
<div class="border-gray z-20 border bg-black">
|
||||
<header class="flex justify-between p-4">
|
||||
<div class="text-primary text-2xl">Showing search results for `{term}`</div>
|
||||
<button on:click={onClose}>✕</button>
|
||||
<div id="bg-close" class="z-40" on:click={onClose}></div>
|
||||
<section class="z-50">
|
||||
<header class="flex border border-gray border-t-0 border-x-0 bg-black">
|
||||
<div class="relative w-full">
|
||||
<SearchInput
|
||||
class="w-full rounded-sm h-9"
|
||||
size="small"
|
||||
placeholder={`${$t("store-search-placeholder")}`}
|
||||
onSearch={(search) => {
|
||||
term = search;
|
||||
searchStore.search(search);
|
||||
}}
|
||||
/>
|
||||
<div class="absolute top-2 right-3 opacity-50 flex items-center gap-1">
|
||||
<button class="text-xs mt-1">clear</button>
|
||||
<kbd class=" bg-gray text-white px-2 mt-1 rounded-sm flex items-center">
|
||||
<span class="text-xs">ctrl + shift + del</span>
|
||||
</kbd>
|
||||
</div>
|
||||
</div>
|
||||
<button class="mr-2" on:click={onClose}>✕</button>
|
||||
</header>
|
||||
<header class="text-gray p-4 text-lg">
|
||||
Packages ({packages.length})
|
||||
</header>
|
||||
<ul class="flex flex-col gap-2 p-2">
|
||||
{#if packages.length > 0}
|
||||
{#each packages as pkg}
|
||||
<div class={pkg.state === PackageStates.INSTALLING ? 'animate-pulse' : ''}>
|
||||
<PackageResult
|
||||
{pkg}
|
||||
{onClose}
|
||||
ctaLabel={$t(`package.cta-${pkg.state}`)}
|
||||
ctaColor={pkg.state === PackageStates.INSTALLED ? "green": "secondary"}
|
||||
onClick={async () => {
|
||||
try {
|
||||
pkg.state = PackageStates.INSTALLING;
|
||||
await installPackage(pkg, pkg.version);
|
||||
pkg.state = PackageStates.INSTALLED;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
{:else if loading}
|
||||
{#each Array(12) as _}
|
||||
<section class="h-50 border-gray border p-4">
|
||||
{#if term}
|
||||
<div class="z-20 bg-black">
|
||||
<header class="text-gray p-4 text-lg">
|
||||
Packages ({$packagesSearch.length})
|
||||
</header>
|
||||
<ul class="flex flex-col gap-2 p-2">
|
||||
{#if $packagesSearch.length > 0}
|
||||
{#each $packagesSearch as pkg}
|
||||
<div class={pkg.state === PackageStates.INSTALLING ? 'animate-pulse' : ''}>
|
||||
<PackageResult
|
||||
{pkg}
|
||||
{onClose}
|
||||
onClick={async () => {
|
||||
try {
|
||||
pkg.state = PackageStates.INSTALLING;
|
||||
await installPackage(pkg, pkg.version);
|
||||
pkg.state = PackageStates.INSTALLED;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
</ul>
|
||||
<!-- <header class="text-primary p-4 text-lg">
|
||||
Top Article Results ({articles.length})
|
||||
</header>
|
||||
{#if articles.length}
|
||||
<Posts posts={articles} linkTarget="_blank" />
|
||||
{:else if loading}
|
||||
<section class="border-gray h-64 border bg-black p-4">
|
||||
<Preloader />
|
||||
</section>
|
||||
{/each}
|
||||
{/if}
|
||||
</ul>
|
||||
<!-- <header class="text-primary p-4 text-lg">
|
||||
Top Article Results ({articles.length})
|
||||
</header>
|
||||
{#if articles.length}
|
||||
<Posts posts={articles} linkTarget="_blank" />
|
||||
{:else if loading}
|
||||
<section class="border-gray h-64 border bg-black p-4">
|
||||
<Preloader />
|
||||
</section>
|
||||
{/if}
|
||||
<header class="text-primary p-4 text-lg">
|
||||
Top Workshop Results ({workshops.length})
|
||||
</header>
|
||||
{#if workshops.length}
|
||||
<Posts posts={workshops} linkTarget="_blank" />
|
||||
{:else if loading}
|
||||
<section class="border-gray h-64 border bg-black p-4">
|
||||
<Preloader />
|
||||
</section>
|
||||
{/if} -->
|
||||
</div>
|
||||
{:else}
|
||||
<div class="w-full h-full flex flex-col justify-center bg-black">
|
||||
<p class="text-gray text-center">start typing to search</p>
|
||||
</div>
|
||||
{/if}
|
||||
<header class="text-primary p-4 text-lg">
|
||||
Top Workshop Results ({workshops.length})
|
||||
</header>
|
||||
{#if workshops.length}
|
||||
<Posts posts={workshops} linkTarget="_blank" />
|
||||
{:else if loading}
|
||||
<section class="border-gray h-64 border bg-black p-4">
|
||||
<Preloader />
|
||||
</section>
|
||||
{/if} -->
|
||||
</div>
|
||||
</section>
|
||||
</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: 48px;
|
||||
left: 0px;
|
||||
right: 0;
|
||||
top: 50px;
|
||||
left: 50px;
|
||||
right: 50px;
|
||||
bottom: 50px;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
transition: opacity 0.3s ease-in-out;
|
||||
opacity: 0%;
|
||||
opacity: 1;
|
||||
overflow: hidden;
|
||||
height: 0px;
|
||||
}
|
||||
|
||||
section.show {
|
||||
padding: 36px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
section > figure {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
section.show {
|
||||
opacity: 100%;
|
||||
height: 100%;
|
||||
height: auto;
|
||||
border: gray 1px solid;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
section > div {
|
||||
position: relative;
|
||||
height: 0%;
|
||||
margin-top: 2px;
|
||||
height: calc(100% - 40px);
|
||||
width: 100%;
|
||||
transition: height 0.6s ease-in-out;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
section.show > div {
|
||||
height: 90%;
|
||||
|
||||
/* width */
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
/* Track */
|
||||
::-webkit-scrollbar-track {
|
||||
background: #272626;
|
||||
}
|
||||
|
||||
/* Handle */
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #949494;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* Handle on hover */
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: white;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -10,10 +10,6 @@
|
|||
|
||||
let { nextPath, prevPath } = navStore;
|
||||
|
||||
const onSearch = (term: string) => {
|
||||
searchStore.search(term);
|
||||
};
|
||||
|
||||
let currentPath: string;
|
||||
beforeUpdate(async () => {
|
||||
currentPath = $page.url.pathname;
|
||||
|
@ -28,13 +24,19 @@
|
|||
<button on:click={navStore.back} class:active={$prevPath} class="opacity-50 pt-1 text-xs"><i class="icon-arrow-left"/></button>
|
||||
<button on:click={navStore.next} class:active={$nextPath} class="opacity-50 pt-1 text-xs"><i class="icon-arrow-right"/></button>
|
||||
</ul>
|
||||
<div class="px-2 w-1/3">
|
||||
<div class="px-2 w-1/3 relative">
|
||||
<SearchInput
|
||||
class="w-full border border-gray rounded-sm h-9"
|
||||
size="small"
|
||||
placeholder={`${$t("store-search-placeholder")}`}
|
||||
{onSearch}
|
||||
onFocus={() => {
|
||||
searchStore.searching.set(true);
|
||||
}}
|
||||
/>
|
||||
<kbd class="absolute top-0 right-3 opacity-50 bg-gray text-white px-2 mt-1 rounded-sm flex items-center">
|
||||
<i class="text-lg">⌘</i>
|
||||
<span class="text-xs">K</span>
|
||||
</kbd>
|
||||
</div>
|
||||
<ProfileNavButton />
|
||||
</header>
|
||||
|
|
|
@ -95,44 +95,35 @@ export const postsStore = initPosts();
|
|||
|
||||
function initSearchStore() {
|
||||
const searching = writable<boolean>(false);
|
||||
const { subscribe, set } = writable<string>("");
|
||||
const packagesSearch = writable<GUIPackage[]>([]);
|
||||
const postsSearch = writable<AirtablePost[]>([]);
|
||||
|
||||
// TODO:
|
||||
// should use algolia if user is somehow online
|
||||
|
||||
const packagesFound: GUIPackage[] = [];
|
||||
|
||||
let term = "";
|
||||
|
||||
subscribe((v) => (term = v));
|
||||
packagesSearch.subscribe((v) => packagesFound.push(...v));
|
||||
|
||||
return {
|
||||
term,
|
||||
searching,
|
||||
packagesSearch,
|
||||
postsSearch,
|
||||
packagesFound,
|
||||
subscribe,
|
||||
search: async (term: string) => {
|
||||
searching.set(true);
|
||||
try {
|
||||
if (term) {
|
||||
const [resultPkgs, resultPosts] = await Promise.all([
|
||||
packagesStore.search(term, 5),
|
||||
postsStore.search(term, 10)
|
||||
const [
|
||||
resultPkgs
|
||||
// resultPosts
|
||||
] = await Promise.all([
|
||||
packagesStore.search(term, 5)
|
||||
// postsStore.search(term, 10)
|
||||
]);
|
||||
packagesSearch.set(resultPkgs);
|
||||
postsSearch.set(resultPosts);
|
||||
// postsSearch.set(resultPosts);
|
||||
} else {
|
||||
packagesSearch.set([]);
|
||||
postsSearch.set([]);
|
||||
// postsSearch.set([]);
|
||||
}
|
||||
set(term);
|
||||
} finally {
|
||||
searching.set(false);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -6,8 +6,9 @@
|
|||
import TopBar from '$components/top-bar/top-bar.svelte';
|
||||
import PopoutMenu from '$components/popout-menu/popout-menu.svelte';
|
||||
import Footer from '$components/footer/footer.svelte';
|
||||
import { navStore, packagesStore } from '$libs/stores';
|
||||
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';
|
||||
|
@ -17,8 +18,10 @@
|
|||
let view: HTMLElement;
|
||||
|
||||
const { sideNavOpen, setNewPath } = navStore;
|
||||
const { searching } = searchStore;
|
||||
|
||||
$: if ($navigating) view.scrollTop = 0;
|
||||
|
||||
|
||||
afterNavigate(({ from, to }) => {
|
||||
if (to && to?.route.id && from && from?.url) {
|
||||
|
@ -38,6 +41,12 @@
|
|||
// 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;
|
||||
});
|
||||
packagesStore.init();
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -1,27 +1,55 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import "../app.css";
|
||||
import Mousetrap from "mousetrap";
|
||||
|
||||
let clazz = "";
|
||||
export { clazz as class };
|
||||
export let size: "small" | "medium" | "large" = "small";
|
||||
export let onSearch: (text: string) => void;
|
||||
export let onSearch = (text: string) => {
|
||||
console.log(text);
|
||||
};
|
||||
export let onFocus = () => {
|
||||
console.log("focus");
|
||||
};
|
||||
export let placeholder = "search_";
|
||||
|
||||
let searchInput: HTMLInputElement;
|
||||
|
||||
let timer: NodeJS.Timeout;
|
||||
const onChange = (e: KeyboardEvent) => {
|
||||
const t = e.target as HTMLInputElement;
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(() => {
|
||||
onSearch && onSearch(t.value);
|
||||
onSearch(t.value);
|
||||
}, 300);
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
searchInput.focus();
|
||||
Mousetrap.bind(["ctrl+shift+del"], function () {
|
||||
searchInput.value = "";
|
||||
return false;
|
||||
});
|
||||
|
||||
Mousetrap.prototype.stopCallback = () => {
|
||||
return false;
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<section class={`flex items-center pb-1 ${size} ${clazz}`}>
|
||||
<div class="icon pl-4">
|
||||
<i class="icon-search-icon" />
|
||||
</div>
|
||||
<input type="search" class="flex-grow pb-2 text-sm" {placeholder} on:keyup={onChange} />
|
||||
<input
|
||||
bind:this={searchInput}
|
||||
type="search"
|
||||
class="flex-grow pb-2 text-sm"
|
||||
{placeholder}
|
||||
on:keyup={onChange}
|
||||
on:focus={onFocus}
|
||||
/>
|
||||
</section>
|
||||
|
||||
<!-- <input type="search" class="w-full bg-black h-12 p-4 border border-x-0 border-gray"/> -->
|
||||
|
@ -72,4 +100,11 @@
|
|||
color: #949494;
|
||||
opacity: 1; /* Firefox */
|
||||
}
|
||||
|
||||
input[type="search"]::-webkit-search-decoration,
|
||||
input[type="search"]::-webkit-search-cancel-button,
|
||||
input[type="search"]::-webkit-search-results-button,
|
||||
input[type="search"]::-webkit-search-results-decoration {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -32,6 +32,7 @@ importers:
|
|||
'@types/electron': ^1.6.10
|
||||
'@types/js-yaml': ^4.0.5
|
||||
'@types/mixpanel-browser': ^2.38.1
|
||||
'@types/mousetrap': ^1.6.11
|
||||
'@types/testing-library__jest-dom': ^5.14.5
|
||||
'@typescript-eslint/eslint-plugin': ^5.27.0
|
||||
'@typescript-eslint/parser': ^5.27.0
|
||||
|
@ -64,6 +65,7 @@ importers:
|
|||
mixpanel-browser: ^2.45.0
|
||||
mkdirp: ^2.1.3
|
||||
moment: ^2.29.4
|
||||
mousetrap: ^1.6.5
|
||||
postcss: ^8.4.19
|
||||
prettier: ^2.7.1
|
||||
prettier-plugin-svelte: ^2.7.0
|
||||
|
@ -93,6 +95,7 @@ importers:
|
|||
'@sentry/electron': 4.3.0
|
||||
'@sentry/svelte': 7.38.0_svelte@3.55.1
|
||||
'@types/electron': 1.6.10
|
||||
'@types/mousetrap': 1.6.11
|
||||
'@vitest/coverage-c8': 0.27.1_jsdom@21.0.0
|
||||
axios: 1.3.2
|
||||
bcryptjs: 2.4.3
|
||||
|
@ -112,6 +115,7 @@ importers:
|
|||
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
|
||||
|
@ -4424,6 +4428,10 @@ packages:
|
|||
resolution: {integrity: sha512-XzQbwgiOPsFXUQnjz3vSwcwrvJDbQ35bCiwa/1VXGrHvU1ti9+eqO1GY91DShzkEzKkkEEkxfNshS5dbBZqd7w==}
|
||||
dev: true
|
||||
|
||||
/@types/mousetrap/1.6.11:
|
||||
resolution: {integrity: sha512-F0oAily9Q9QQpv9JKxKn0zMKfOo36KHCW7myYsmUyf2t0g+sBTbG3UleTPoguHdE1z3GLFr3p7/wiOio52QFjQ==}
|
||||
dev: false
|
||||
|
||||
/@types/ms/0.7.31:
|
||||
resolution: {integrity: sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==}
|
||||
dev: true
|
||||
|
@ -10039,6 +10047,10 @@ packages:
|
|||
resolution: {integrity: sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==}
|
||||
dev: false
|
||||
|
||||
/mousetrap/1.6.5:
|
||||
resolution: {integrity: sha512-QNo4kEepaIBwiT8CDhP98umTetp+JNfQYBWvC1pc6/OAibuXtRcxZ58Qz8skvEHYvURne/7R8T5VoOI7rDsEUA==}
|
||||
dev: false
|
||||
|
||||
/mqtt-packet/6.10.0:
|
||||
resolution: {integrity: sha512-ja8+mFKIHdB1Tpl6vac+sktqy3gA8t9Mduom1BA75cI+R9AHnZOiaBQwpGiWnaVJLDGRdNhQmFaAqd7tkKSMGA==}
|
||||
dependencies:
|
||||
|
|
Loading…
Reference in a new issue