Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add fuzzy find generic indicator #749

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified bun.lockb
Binary file not shown.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"floating-vue": "^5.0.3",
"flowbite": "^2.2.1",
"flowbite-vue": "^0.1.2",
"fuse.js": "^7.0.0",
"gamepad.js": "^1.0.4",
"gsap": "^3.11.3",
"haversine-distance": "^1.2.1",
Expand Down
1 change: 0 additions & 1 deletion src/components/Dialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,6 @@ watch(show, () => {
margin-right: -50%;
transform: translate(-50%, -50%);
height: fit-content;
width: 300px;
background-color: rgba(71, 85, 105, 0.3);
backdrop-filter: blur(1px);
box-shadow: 0 0 20px 5px rgba(0, 0, 0, 0.25);
Expand Down
215 changes: 161 additions & 54 deletions src/components/mini-widgets/VeryGenericIndicator.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@
<span class="w-full text-sm font-semibold leading-4 whitespace-nowrap">{{ miniWidget.options.displayName }}</span>
</div>
</div>
<Dialog v-model:show="miniWidget.managerVars.configMenuOpen" class="w-80">
<div class="w-full h-full">
<Dialog v-model:show="miniWidget.managerVars.configMenuOpen" class="w-[24rem]">
<div class="w-full h-full px-2 py-4">
<div class="flex items-center mb-3 justify-evenly">
<div
class="px-3 py-1 transition-all rounded-md cursor-pointer select-none text-slate-100 hover:bg-slate-400"
Expand All @@ -30,71 +30,108 @@
</div>
</div>
<div v-if="currentTab === 'custom'" class="flex flex-col items-center justify-around">
<div class="flex items-center justify-between w-full my-1">
<span class="mr-1 text-slate-100">Display name</span>
<input v-model="miniWidget.options.displayName" class="w-48 px-2 py-1 rounded-md bg-slate-200" />
<div class="flex flex-col items-center justify-between w-full mt-3">
<span class="w-full mb-1 text-sm text-slate-100/50">Display name</span>
<input v-model="miniWidget.options.displayName" class="w-full px-2 py-1 rounded-md bg-slate-200" />
</div>
<div class="flex items-center justify-between w-full my-1">
<span class="mr-1 text-slate-100">Variable</span>
<div class="w-48">
<Dropdown v-model="miniWidget.options.variableName" :options="Object.keys(store.genericVariables)" />
<div class="flex flex-col items-center justify-between w-full mt-3">
<span class="w-full mb-1 text-sm text-slate-100/50">Variable</span>
<div class="relative w-full">
<button
class="w-full py-1 pl-2 pr-8 text-left transition-all rounded-md bg-slate-200 hover:bg-slate-400"
@click="showVariableChooseModal = !showVariableChooseModal"
>
<p class="text-ellipsis overflow-x-clip">{{ miniWidget.options.variableName || 'Click to choose...' }}</p>
</button>
<span
class="absolute right-0.5 m-1 text-2xl -translate-y-1 cursor-pointer text-slate-500 mdi mdi-swap-horizontal-bold"
/>
</div>
</div>
<div class="flex items-center justify-between w-full my-1">
<span class="mr-1 text-slate-100">Unit</span>
<input v-model="miniWidget.options.variableUnit" class="w-48 px-2 py-1 rounded-md bg-slate-200" />
</div>
<div class="flex items-center justify-between w-full my-1">
<span class="mr-1 text-slate-100">Multiplier</span>
<input v-model="miniWidget.options.variableMultiplier" class="w-48 px-2 py-1 rounded-md bg-slate-200" />
<Transition>
<div v-if="showVariableChooseModal" class="flex flex-col justify-center w-full mx-1 my-3 align-center">
<input
v-model="variableNameSearchString"
placeholder="Search variable..."
class="w-full px-2 py-1 rounded-md bg-slate-200"
/>
<div class="grid w-full h-32 grid-cols-1 my-2 overflow-x-hidden overflow-y-scroll">
<span
v-for="variable in variableNamesToShow"
:key="variable"
class="h-8 p-1 m-1 overflow-x-hidden text-white transition-all rounded-md cursor-pointer select-none bg-slate-700 hover:bg-slate-400/20"
@click="chooseVariable(variable)"
>
{{ variable }}
</span>
</div>
</div>
</Transition>
<div class="flex items-center justify-between w-full mt-2">
<div class="flex flex-col items-center justify-between w-full mx-5">
<span class="w-full mb-1 text-sm text-slate-100/50">Unit</span>
<input v-model="miniWidget.options.variableUnit" class="w-full px-2 py-1 rounded-md bg-slate-200" />
</div>
<div class="flex flex-col items-center justify-between w-full mx-5">
<span class="w-full mb-1 text-sm text-slate-100/50">Multiplier</span>
<input v-model="miniWidget.options.variableMultiplier" class="w-full px-2 py-1 rounded-md bg-slate-200" />
</div>
</div>
<div class="flex items-center justify-between w-full my-1">
<span class="mr-1 text-slate-100">Icon</span>
<div class="relative w-48">
<input v-model="miniWidget.options.iconName" class="w-full py-1 pl-2 pr-8 rounded-md bg-slate-200" />
<div class="flex flex-col items-center justify-between w-full mt-3">
<span class="w-full mb-1 text-sm text-slate-100/50">Icon</span>
<div class="relative w-full">
<button
class="w-full py-1 pl-2 pr-8 text-left transition-all rounded-md bg-slate-200 hover:bg-slate-400"
@click="showIconChooseModal = !showIconChooseModal"
>
<p class="text-ellipsis overflow-x-clip">{{ miniWidget.options.iconName || 'Click to choose...' }}</p>
</button>
<span
class="absolute right-0.5 m-1 text-2xl -translate-y-1 cursor-pointer text-slate-500 mdi"
:class="[miniWidget.options.iconName]"
/>
</div>
</div>
<div class="flex items-center justify-center w-full mt-2">
<input
v-model="iconSearchString"
class="w-48 px-2 py-1 rounded-md bg-slate-200"
placeholder="Search icons..."
/>
</div>
<RecycleScroller
v-if="iconSearchString === ''"
v-slot="{ item }"
class="w-full h-40 mt-3"
:items="iconsNames.filter((name) => name.includes(iconSearchString))"
:item-size="46"
:grid-items="6"
>
<span
class="m-1 text-white cursor-pointer mdi icon-symbol"
:class="[item]"
@click="miniWidget.options.iconName = item"
>
</span>
</RecycleScroller>
<div v-else class="grid w-full h-40 grid-cols-6 mt-3 overflow-x-hidden overflow-y-scroll">
<span
v-for="icon in iconsNames.filter((name) => name.includes(iconSearchString))"
:key="icon"
class="m-1 text-white cursor-pointer mdi icon-symbol"
:class="[icon]"
@click="miniWidget.options.iconName = icon"
/>
</div>
<Transition>
<div v-if="showIconChooseModal" class="flex flex-col items-center justify-center w-full mt-2">
<div>
<input
v-model="iconSearchString"
class="w-full px-2 py-1 rounded-md bg-slate-200"
placeholder="Search icons..."
/>
</div>
<RecycleScroller
v-if="iconSearchString === '' && showIconChooseModal"
v-slot="{ item }"
class="w-full h-40 mt-3"
:items="iconsNames"
:item-size="46"
:grid-items="7"
>
<span class="m-1 text-white cursor-pointer mdi icon-symbol" :class="[item]" @click="chooseIcon(item)">
</span>
</RecycleScroller>
<div
v-else-if="showIconChooseModal"
class="grid w-full h-40 grid-cols-7 mt-3 overflow-x-hidden overflow-y-scroll"
>
<span
v-for="icon in iconsToShow"
:key="icon"
class="m-1 text-white cursor-pointer mdi icon-symbol"
:class="[icon]"
@click="chooseIcon(icon)"
/>
</div>
</div>
</Transition>
</div>
<div v-if="currentTab === 'presets'" class="flex flex-wrap items-center justify-around">
<div
v-for="(template, i) in veryGenericIndicatorPresets"
:key="i"
class="flex items-center w-[6.25rem] h-12 py-1 pl-6 pr-1 rounded-md text-white justify-center cursor-pointer hover:bg-slate-100/20 transition-all"
class="flex items-center w-[6.25rem] h-12 m-2 rounded-md text-white justify-center cursor-pointer hover:bg-slate-100/20 transition-all"
@click="setIndicatorFromTemplate(template)"
>
<span class="relative w-[2rem] mdi icon-symbol" :class="[template.iconName]"></span>
Expand All @@ -112,9 +149,11 @@

<script setup lang="ts">
import * as MdiExports from '@mdi/js/mdi'
import { watchThrottled } from '@vueuse/core'
import Fuse from 'fuse.js'
import Swal from 'sweetalert2'
import { computed, onBeforeMount, onMounted, ref, toRefs, watch } from 'vue'

import Dropdown from '@/components/Dropdown.vue'
import { round } from '@/libs/utils'
import { useMainVehicleStore } from '@/stores/mainVehicle'
import { type VeryGenericIndicatorPreset, veryGenericIndicatorPresets } from '@/types/genericIndicator'
Expand Down Expand Up @@ -169,23 +208,91 @@ const updateVariableState = (): void => {
const updateWidgetName = (): void => {
miniWidget.value.name = miniWidget.value.options.displayName || miniWidget.value.options.variableName
}
watch(store.genericVariables, updateVariableState)
const updateGenericVariablesNames = (): void => {
allVariablesNames.value = Object.keys(store.genericVariables)
}
watch(store.genericVariables, () => {
updateVariableState()
updateGenericVariablesNames()
})
watch(
miniWidget,
() => {
updateVariableState()
updateWidgetName()
updateGenericVariablesNames()
},
{ deep: true }
)
onMounted(() => {
updateVariableState()
updateWidgetName()
updateGenericVariablesNames()
})

const fuseOptions = { includeScore: true, ignoreLocation: true, threshold: 0.3 }

let iconsNames: string[] = []

// Search for icon using fuzzy-finder
const iconSearchString = ref('')
const iconsToShow = ref<string[]>([])
watchThrottled(
iconSearchString,
() => {
const iconFuse = new Fuse(iconsNames, fuseOptions)
const filteredIconsResult = iconFuse.search(iconSearchString.value)
iconsToShow.value = filteredIconsResult.map((r) => r.item)
},
{ throttle: 1000 }
)

// Search for variable using fuzzy-finder
const variableNameSearchString = ref('')
const variableNamesToShow = ref<string[]>([])
const allVariablesNames = ref<string[]>([])
const showVariableChooseModal = ref(false)
const showIconChooseModal = ref(false)

watchThrottled(
[variableNameSearchString, allVariablesNames],
() => {
if (variableNameSearchString.value === '') {
variableNamesToShow.value = allVariablesNames.value
return
}

const variableNameFuse = new Fuse(allVariablesNames.value, fuseOptions)
const filteredVariablesResult = variableNameFuse.search(variableNameSearchString.value)
variableNamesToShow.value = filteredVariablesResult.map((r) => r.item)
},
{ throttle: 300 }
)

const chooseVariable = (variable: string): void => {
miniWidget.value.options.variableName = variable
variableNameSearchString.value = ''
showVariableChooseModal.value = false
}

const chooseIcon = (iconName: string): void => {
miniWidget.value.options.iconName = iconName
iconSearchString.value = ''
showIconChooseModal.value = false
}

watch(showVariableChooseModal, async (newValue) => {
if (newValue === true && variableNamesToShow.value.isEmpty()) {
miniWidget.value.managerVars.configMenuOpen = false
showVariableChooseModal.value = false
await Swal.fire({
text: 'No variables found to choose from. Please make sure your vehicle is connected.',
icon: 'error',
})
miniWidget.value.managerVars.configMenuOpen = true
}
})

const currentTab = ref('presets')

const setIndicatorFromTemplate = (template: VeryGenericIndicatorPreset): void => {
Expand Down
Loading