Skip to content
This repository has been archived by the owner on Jun 23, 2023. It is now read-only.

Commit

Permalink
Add search to the Umbrel App Store (#34)
Browse files Browse the repository at this point in the history
* Add search to the Umbrel App Store

* Simplify matching logic

Co-authored-by: Luke Childs <lukechilds123@gmail.com>
  • Loading branch information
mayankchhabra and lukechilds authored Sep 22, 2022
1 parent 5468961 commit f3d9ae7
Show file tree
Hide file tree
Showing 4 changed files with 172 additions and 8 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"bootstrap-vue": "^2.11.0",
"core-js": "^3.4.4",
"countup.js": "^2.0.4",
"lunr": "^2.3.9",
"moment": "^2.24.0",
"qrcode.vue": "^1.7.0",
"vue": "^2.6.10",
Expand Down
54 changes: 52 additions & 2 deletions src/store/modules/apps.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import lunr from "lunr";
import API from "@/helpers/api";
// import Vue from "vue"

// Initial state
const state = () => ({
Expand All @@ -8,7 +8,10 @@ const state = () => ({
installing: [],
uninstalling: [],
updating: [],
noAppsInstalled: false // we store this seperately instead of checking for empty installed array as that's the default state
noAppsInstalled: false, // we store this seperately instead of checking for empty installed array as that's the default state
searchIndex: null,
searchQuery: "",
searchResults: [],
});

// Functions to update the state directly
Expand All @@ -19,8 +22,35 @@ const mutations = {
state.noAppsInstalled = !apps.length;
},
setAppStore(state, appStore) {

// build a new search index if the app store has changed
// we store this in global state so it doesn't have to
// regenerate everytime the app store view is loaded and
// to persist the search query if the user changes views
if (state.store.length !== appStore.length) {
const searchIndex = lunr(function () {
this.ref('id');

// bump up the priority of name matching over tagline matching
// https://github.com/olivernn/lunr.js/issues/312#issuecomment-399657187
this.field('name', { boost: 10 });
this.field('tagline');

appStore.forEach((app) => {
this.add(app)
}, this);
});
state.searchIndex = searchIndex;
}

state.store = appStore;
},
setSearchQuery(state, searchQuery) {
state.searchQuery = searchQuery;
},
setSearchResults(state, searchResults) {
state.searchResults = searchResults;
},
addInstallingApp(state, appId) {
if (!state.installing.includes(appId)) {
state.installing.push(appId);
Expand Down Expand Up @@ -71,6 +101,26 @@ const actions = {
commit("setAppStore", appStore);
}
},
searchAppStore({ state, commit }, searchQuery) {
commit("setSearchQuery", searchQuery);

// don't search if the search index isn't built yet
if (!state.searchIndex) {
return commit("setSearchResults", []);
}

// get search results
// ~1 = allow fuzzy matching upto 1 character of mistake
// * = to allow for autocomplete search (eg. showing nextcloud when "nex" is typed)
// docs: https://lunrjs.com/guides/searching.html
const searchResults = state.searchIndex.search(`${searchQuery}~1 ${searchQuery}*`);

// create a new array of matched results
// in the same sorting order as lunr provides
const matchedApps = searchResults.map(result => state.store.find(app => app.id === result.ref));

commit("setSearchResults", matchedApps);
},
async update({ state, commit, dispatch }, appId) {
commit("addUpdatingApp", appId);
try {
Expand Down
120 changes: 114 additions & 6 deletions src/views/AppStore/AppStore.vue
Original file line number Diff line number Diff line change
@@ -1,13 +1,30 @@
<template>
<div class="pt-2">
<div class="my-3">
<div class="mt-3">
<div class="">
<div class="d-flex justify-content-between align-items-center">
<div>
<h1>app store</h1>
<p class="text-muted mb-0">
Add super powers to your Umbrel with amazing self-hosted applications
</p>
<p class="text-muted mb-0">
Add super powers to your Umbrel with amazing self-hosted applications
</p>
<div
class="search-input-container mt-3 mb-2 d-flex align-items-center"
:class="{'active': appStoreSearchQuery}"
>
<svg class="search-input-icon" width="18" height="21" viewBox="0 0 18 21" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.5146 15.9941C11.319 16.6359 9.95202 17 8.5 17C3.80558 17 0 13.1944 0 8.5C0 3.80558 3.80558 0 8.5 0C13.1944 0 17 3.80558 17 8.5C17 11.0223 15.9013 13.2881 14.1564 14.8448L17.7809 19.3753C18.1259 19.8066 18.056 20.4359 17.6247 20.7809C17.1934 21.1259 16.5641 21.056 16.2191 20.6247L12.5146 15.9941ZM15 8.5C15 12.0899 12.0899 15 8.5 15C4.91015 15 2 12.0899 2 8.5C2 4.91015 4.91015 2 8.5 2C12.0899 2 15 4.91015 15 8.5Z" fill="#80838D"/>
</svg>
<b-input
id="search-input"
ref="searchInput"
class="search-input"
type="text"
v-model="appStoreSearchQuery"
@input="search"
placeholder="Search apps"
></b-input>
</div>
</div>
<div class="position-relative" v-if="appsWithUpdate.length">
<b-button variant="primary" class="px-3" v-b-modal.app-updates-modal>
Expand All @@ -20,7 +37,52 @@
</div>
</div>
</div>
<div class="app-store-card-columns">

<div v-if="appStoreSearchQuery" class="app-store-card-columns">
<card-widget
v-for="app in appStoreSearchResults"
:key="app.id"
class="pt-4 pb-2 card-app-list"
>
<router-link
:to="{name: 'app-store-app', params: {id: app.id}}"
class="app-list-app d-flex justify-content-between align-items-center px-3 px-lg-4 py-3"
>
<div class="d-flex">
<div class="d-block">
<img
class="app-icon mr-2 mr-lg-3"
:src="`https://getumbrel.github.io/umbrel-apps-gallery/${app.id}/icon.svg`"
draggable="false"
/>
</div>
<div class="d-flex justify-content-center flex-column">
<h3 class="app-name text-title-color mb-1">
{{ app.name }}
</h3>
<p class="text-muted mb-0">
{{ app.tagline }}
</p>
</div>
</div>
<div class="ml-2 icon-arrow-container">
<svg
viewBox="0 0 14 25"
fill="none"
xmlns="http://www.w3.org/2000/svg"
class="icon-arrow"
>
<path
d="M0.512563 3.0484C-0.170855 2.35104 -0.170855 1.22039 0.512563 0.523023C1.19598 -0.174341 2.30402 -0.174341 2.98744 0.523023L13.4874 11.2373C14.1499 11.9133 14.1731 13.0019 13.54 13.7066L3.91502 24.4209C3.26193 25.1479 2.15494 25.197 1.44248 24.5306C0.730023 23.8642 0.681893 22.7346 1.33498 22.0076L9.82776 12.5537L0.512563 3.0484Z"
fill="#C3C6D1"
/>
</svg>
</div>
</router-link>
</card-widget>
</div>

<div v-show="!appStoreSearchQuery" class="app-store-card-columns">
<card-widget
v-for="categorizedApps in categorizedAppStore"
:key="categorizedApps[0].category"
Expand Down Expand Up @@ -141,7 +203,18 @@ export default {
...mapState({
appStore: (state) => state.apps.store,
updating: (state) => state.apps.updating,
appStoreSearchIndex: (state) => state.apps.searchIndex,
appStoreSearchResults: (state) => state.apps.searchResults,
}),
// for v-model to work with global state
appStoreSearchQuery: {
get () {
return this.$store.state.apps.searchQuery
},
set (value) {
this.$store.dispatch("apps/searchAppStore", value)
}
},
appsWithUpdate: function() {
return this.appStore.filter(app => app.updateAvailable)
},
Expand All @@ -167,10 +240,14 @@ export default {
// To avoid a 'double update'
this.$refs[app.id][0].updateApp();
});
}
},
},
created() {
this.$store.dispatch("apps/getAppStore");
// autofocus search input
// https://stackoverflow.com/a/63485725
this.$nextTick(() => this.$refs.searchInput.focus());
},
components: {
CardWidget,
Expand All @@ -180,6 +257,37 @@ export default {
</script>

<style lang="scss" scoped>
.search-input-container {
svg.search-input-icon {
transition: transform 0.3s ease;
path {
fill: var(--text-muted-color) !important;
transition: fill 0.3s ease;
}
}
&.active {
svg.search-input-icon {
transform: scale(1.1, 1.1) rotate(-5deg);
path {
fill: var(--text-color) !important;
}
}
}
.search-input {
width: 100%;
max-width: 160px;
background: transparent !important;
outline: none !important;
border: none !important;
box-shadow: none !important;
color: var(--text-color) !important;
&::placeholder, &::-webkit-input-placeholder, &::-moz-placeholder, &:-moz-placeholder, &:-ms-input-placeholder {
color: var(--text-muted-color) !important;
opacity: 1 !important;
}
}
}
.umbrel-dev-note {
position: relative;
overflow: visible;
Expand Down
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -5094,6 +5094,11 @@ lru-cache@^5.1.1:
dependencies:
yallist "^3.0.2"

lunr@^2.3.9:
version "2.3.9"
resolved "https://registry.yarnpkg.com/lunr/-/lunr-2.3.9.tgz#18b123142832337dd6e964df1a5a7707b25d35e1"
integrity sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==

make-dir@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5"
Expand Down

0 comments on commit f3d9ae7

Please sign in to comment.