mirror of
https://github.com/BetterSEQTA/BetterSEQTA-Plus.git
synced 2026-06-05 19:24:39 +00:00
feat: add calculator
This commit is contained in:
@@ -85,6 +85,7 @@
|
|||||||
"idb": "^8.0.2",
|
"idb": "^8.0.2",
|
||||||
"localforage": "^1.10.0",
|
"localforage": "^1.10.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
|
"mathjs": "^14.4.0",
|
||||||
"million": "^3.1.11",
|
"million": "^3.1.11",
|
||||||
"motion": "^12.4.12",
|
"motion": "^12.4.12",
|
||||||
"postcss": "^8.5.3",
|
"postcss": "^8.5.3",
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
|
aria-label="Color Picker Swatch"
|
||||||
onclick={onClick}
|
onclick={onClick}
|
||||||
style="background: {$settingsState.selectedColor}"
|
style="background: {$settingsState.selectedColor}"
|
||||||
class="w-16 h-8 rounded-md"
|
class="w-16 h-8 rounded-md"
|
||||||
|
|||||||
@@ -66,7 +66,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#snippet Shortcuts([index, Shortcut]: [string, { name: string, enabled: boolean }]) }
|
{#snippet Shortcuts([index, Shortcut]: [string, { name: string, enabled: boolean }]) }
|
||||||
<div class="flex items-center justify-between px-4 py-3">
|
<div class="flex justify-between items-center px-4 py-3">
|
||||||
<div class="pr-4">
|
<div class="pr-4">
|
||||||
<h2 class="text-sm">{Shortcut.name}</h2>
|
<h2 class="text-sm">{Shortcut.name}</h2>
|
||||||
</div>
|
</div>
|
||||||
@@ -95,7 +95,7 @@
|
|||||||
class="w-full"
|
class="w-full"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
class="w-full p-2 transition border-0 rounded-lg placeholder-zinc-300 bg-zinc-100 dark:bg-zinc-700 focus:bg-zinc-200/50 dark:focus:bg-zinc-600"
|
class="p-2 w-full rounded-lg border-0 transition placeholder-zinc-300 bg-zinc-100 dark:bg-zinc-700 focus:bg-zinc-200/50 dark:focus:bg-zinc-600"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Shortcut Name"
|
placeholder="Shortcut Name"
|
||||||
bind:value={newTitle}
|
bind:value={newTitle}
|
||||||
@@ -108,7 +108,7 @@
|
|||||||
class="w-full"
|
class="w-full"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
class="w-full p-2 my-2 transition border-0 rounded-lg placeholder-zinc-300 bg-zinc-100 dark:bg-zinc-700 focus:bg-zinc-200/50 dark:focus:bg-zinc-600"
|
class="p-2 my-2 w-full rounded-lg border-0 transition placeholder-zinc-300 bg-zinc-100 dark:bg-zinc-700 focus:bg-zinc-200/50 dark:focus:bg-zinc-600"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="URL eg. https://google.com"
|
placeholder="URL eg. https://google.com"
|
||||||
bind:value={newURL}
|
bind:value={newURL}
|
||||||
@@ -142,9 +142,9 @@
|
|||||||
|
|
||||||
<!-- Custom Shortcuts Section -->
|
<!-- Custom Shortcuts Section -->
|
||||||
{#each $settingsState.customshortcuts as shortcut, index}
|
{#each $settingsState.customshortcuts as shortcut, index}
|
||||||
<div class="flex items-center justify-between px-4 py-3">
|
<div class="flex justify-between items-center px-4 py-3">
|
||||||
{shortcut.name}
|
{shortcut.name}
|
||||||
<button onclick={() => deleteCustomShortcut(index)}>
|
<button aria-label="Delete Shortcut" onclick={() => deleteCustomShortcut(index)}>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width={1.5} stroke="currentColor" class="w-6 h-6">
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width={1.5} stroke="currentColor" class="w-6 h-6">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
|
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
@@ -0,0 +1,152 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { createEventDispatcher, onDestroy } from 'svelte';
|
||||||
|
import debounce from '@/seqta/utils/debounce';
|
||||||
|
import * as math from 'mathjs';
|
||||||
|
|
||||||
|
let { searchTerm = '', isSelected = false } = $props<{ searchTerm: string, isSelected: boolean }>();
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher<{
|
||||||
|
hasResult: string | null;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
let result = $state<string | null>(null);
|
||||||
|
let isCalculating = $state(false);
|
||||||
|
let inputUnit = $state<string>('');
|
||||||
|
let outputUnit = $state<string>('');
|
||||||
|
|
||||||
|
// Map of unit abbreviations to full names
|
||||||
|
const unitFullNames: Record<string, string> = {
|
||||||
|
km: 'Kilometers',
|
||||||
|
m: 'Meters',
|
||||||
|
cm: 'Centimeters',
|
||||||
|
mm: 'Millimeters',
|
||||||
|
mi: 'Miles',
|
||||||
|
ft: 'Feet',
|
||||||
|
in: 'Inches',
|
||||||
|
kg: 'Kilograms',
|
||||||
|
g: 'Grams',
|
||||||
|
lb: 'Pounds',
|
||||||
|
oz: 'Ounces',
|
||||||
|
l: 'Liters',
|
||||||
|
ml: 'Milliliters',
|
||||||
|
gal: 'Gallons',
|
||||||
|
h: 'Hours',
|
||||||
|
min: 'Minutes',
|
||||||
|
s: 'Seconds',
|
||||||
|
ms: 'Milliseconds'
|
||||||
|
};
|
||||||
|
|
||||||
|
function detectUnit(expression: string): string {
|
||||||
|
try {
|
||||||
|
const unit = math.unit(expression);
|
||||||
|
if (unit) {
|
||||||
|
// Get the base unit name
|
||||||
|
const unitStr = unit.formatUnits();
|
||||||
|
return unitFullNames[unitStr] || unitStr;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Not a unit or invalid expression
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process the input with debounce to avoid unnecessary calculations
|
||||||
|
const processInput = debounce((input: string) => {
|
||||||
|
try {
|
||||||
|
if (!input.trim()) {
|
||||||
|
result = null;
|
||||||
|
inputUnit = '';
|
||||||
|
outputUnit = '';
|
||||||
|
dispatch('hasResult', null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isCalculating = true;
|
||||||
|
|
||||||
|
// Let mathjs handle everything
|
||||||
|
const evaluated = math.evaluate(input);
|
||||||
|
|
||||||
|
// Format the result
|
||||||
|
if (evaluated !== undefined) {
|
||||||
|
if (math.typeOf(evaluated) === 'Unit') {
|
||||||
|
// Handle unit conversion results
|
||||||
|
result = math.format(evaluated, { precision: 14 });
|
||||||
|
inputUnit = detectUnit(input);
|
||||||
|
outputUnit = detectUnit(result);
|
||||||
|
} else if (typeof evaluated === 'number') {
|
||||||
|
// Handle regular numbers
|
||||||
|
if (math.round(evaluated) === evaluated) {
|
||||||
|
result = math.format(evaluated, { precision: 14 });
|
||||||
|
} else {
|
||||||
|
result = math.format(evaluated, { precision: 14 });
|
||||||
|
}
|
||||||
|
inputUnit = '';
|
||||||
|
outputUnit = '';
|
||||||
|
} else {
|
||||||
|
result = math.format(evaluated, { precision: 14 });
|
||||||
|
inputUnit = '';
|
||||||
|
outputUnit = '';
|
||||||
|
}
|
||||||
|
dispatch('hasResult', result);
|
||||||
|
} else {
|
||||||
|
result = null;
|
||||||
|
inputUnit = '';
|
||||||
|
outputUnit = '';
|
||||||
|
dispatch('hasResult', null);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// If mathjs throws an error, this isn't a valid expression
|
||||||
|
result = null;
|
||||||
|
inputUnit = '';
|
||||||
|
outputUnit = '';
|
||||||
|
dispatch('hasResult', null);
|
||||||
|
} finally {
|
||||||
|
isCalculating = false;
|
||||||
|
}
|
||||||
|
}, 3);
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
processInput(searchTerm);
|
||||||
|
});
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
dispatch('hasResult', null);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if result !== null}
|
||||||
|
<div class="p-2">
|
||||||
|
<p class="text-[0.85rem] p-1 pb-0.5 font-semibold text-zinc-500 dark:text-zinc-400">Calculator</p>
|
||||||
|
<div class="flex items-center justify-between gap-8 rounded-lg border border-transparent {isSelected ? 'bg-zinc-900/5 dark:bg-white/10 border-zinc-900/5 dark:border-zinc-100/5' : ''}">
|
||||||
|
<div class="flex flex-col flex-1 items-center py-4 pl-4">
|
||||||
|
<div class="text-3xl font-semibold text-zinc-900 dark:text-white">
|
||||||
|
{searchTerm}
|
||||||
|
</div>
|
||||||
|
<div class="px-3 py-1 mt-1 text-sm rounded-md text-zinc-600 dark:text-zinc-400 bg-zinc-100 dark:bg-zinc-800">
|
||||||
|
{inputUnit || 'Question'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col justify-center items-center w-12">
|
||||||
|
<div class="h-8 w-[1px] bg-zinc-900/5 dark:bg-zinc-100/5"></div>
|
||||||
|
<div class="text-2xl text-zinc-600 dark:text-zinc-400">
|
||||||
|
→
|
||||||
|
</div>
|
||||||
|
<div class="h-8 w-[1px] bg-zinc-900/5 dark:bg-zinc-100/5"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if !isCalculating}
|
||||||
|
<div class="flex flex-col flex-1 items-center py-4 pr-4">
|
||||||
|
<div class="text-3xl font-semibold text-zinc-900 dark:text-white">
|
||||||
|
{result}
|
||||||
|
</div>
|
||||||
|
<div class="px-3 py-1 mt-1 text-sm rounded-md text-zinc-600 dark:text-zinc-400 bg-zinc-100 dark:bg-zinc-800">
|
||||||
|
{outputUnit || 'Result'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="w-6 h-6 rounded-full border-2 animate-spin border-zinc-300 dark:border-zinc-700 border-t-zinc-600 dark:border-t-zinc-300"></div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
@@ -9,6 +9,7 @@
|
|||||||
import { createSearchIndexes, performSearch as doSearch } from './searchUtils';
|
import { createSearchIndexes, performSearch as doSearch } from './searchUtils';
|
||||||
import { highlightMatch, highlightSnippet } from './highlightUtils';
|
import { highlightMatch, highlightSnippet } from './highlightUtils';
|
||||||
import Fuse from 'fuse.js';
|
import Fuse from 'fuse.js';
|
||||||
|
import Calculator from './Calculator.svelte';
|
||||||
|
|
||||||
const { transparencyEffects } = $props<{ transparencyEffects: boolean }>();
|
const { transparencyEffects } = $props<{ transparencyEffects: boolean }>();
|
||||||
|
|
||||||
@@ -40,6 +41,12 @@
|
|||||||
let combinedResults = $state<CombinedResult[]>([]);
|
let combinedResults = $state<CombinedResult[]>([]);
|
||||||
let isLoading = $state(false);
|
let isLoading = $state(false);
|
||||||
let prevSearchTerm = $state('');
|
let prevSearchTerm = $state('');
|
||||||
|
let calculatorResult = $state<string | null>(null);
|
||||||
|
|
||||||
|
// Function to check if calculator has a result
|
||||||
|
const updateCalculatorState = (hasResult: string | null) => {
|
||||||
|
calculatorResult = hasResult;
|
||||||
|
};
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
setupSearchIndexes();
|
setupSearchIndexes();
|
||||||
@@ -107,15 +114,29 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
const selectNext = () => {
|
const selectNext = () => {
|
||||||
if (selectedIndex < combinedResults.length - 1) selectedIndex++;
|
if (calculatorResult && selectedIndex === -1) {
|
||||||
|
selectedIndex = 0; // Move from calculator to first search result
|
||||||
|
} else if (selectedIndex < combinedResults.length - 1) {
|
||||||
|
selectedIndex++;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const selectPrev = () => {
|
const selectPrev = () => {
|
||||||
if (selectedIndex > 0) selectedIndex--;
|
if (selectedIndex > 0) {
|
||||||
|
selectedIndex--;
|
||||||
|
} else if (selectedIndex === 0 && calculatorResult) {
|
||||||
|
selectedIndex = -1; // Move from first search result to calculator
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const executeSelected = () => {
|
const executeSelected = () => {
|
||||||
|
if (selectedIndex === -1 && calculatorResult) {
|
||||||
|
if (calculatorResult) {
|
||||||
|
navigator.clipboard.writeText(calculatorResult);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
combinedResults[selectedIndex]?.item.action();
|
combinedResults[selectedIndex]?.item.action();
|
||||||
|
}
|
||||||
commandPalleteOpen = false;
|
commandPalleteOpen = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -173,7 +194,12 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Unified results list -->
|
<Calculator
|
||||||
|
searchTerm={searchTerm}
|
||||||
|
isSelected={selectedIndex === -1}
|
||||||
|
on:hasResult={(e) => updateCalculatorState(e.detail)}
|
||||||
|
/>
|
||||||
|
|
||||||
{#if combinedResults.length > 0}
|
{#if combinedResults.length > 0}
|
||||||
<ul class="overflow-y-auto max-h-[32rem] text-base scroll-py-2 p-1 gap-0.5 flex flex-col">
|
<ul class="overflow-y-auto max-h-[32rem] text-base scroll-py-2 p-1 gap-0.5 flex flex-col">
|
||||||
{#each combinedResults as result, i (result.id)}
|
{#each combinedResults as result, i (result.id)}
|
||||||
@@ -225,7 +251,7 @@
|
|||||||
</li>
|
</li>
|
||||||
{/each}
|
{/each}
|
||||||
</ul>
|
</ul>
|
||||||
{:else}
|
{:else if !calculatorResult}
|
||||||
<div class="px-8 py-16 text-center text-zinc-900 dark:text-zinc-200 sm:px-16">
|
<div class="px-8 py-16 text-center text-zinc-900 dark:text-zinc-200 sm:px-16">
|
||||||
{#if isLoading}
|
{#if isLoading}
|
||||||
<div class="mx-auto w-8 h-8 rounded-full border-2 animate-spin border-zinc-300 dark:border-zinc-700 border-t-zinc-600 dark:border-t-zinc-300"></div>
|
<div class="mx-auto w-8 h-8 rounded-full border-2 animate-spin border-zinc-300 dark:border-zinc-700 border-t-zinc-600 dark:border-t-zinc-300"></div>
|
||||||
|
|||||||
@@ -107,7 +107,6 @@ const globalSearchPlugin: Plugin<typeof settings> = {
|
|||||||
if (titleElement.querySelector('.search-trigger')) {
|
if (titleElement.querySelector('.search-trigger')) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create search button
|
// Create search button
|
||||||
const searchButton = document.createElement('div');
|
const searchButton = document.createElement('div');
|
||||||
searchButton.className = 'search-trigger';
|
searchButton.className = 'search-trigger';
|
||||||
@@ -138,9 +137,13 @@ const globalSearchPlugin: Plugin<typeof settings> = {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Mount Svelte component in shadow DOM
|
// Mount Svelte component in shadow DOM
|
||||||
|
try {
|
||||||
app = renderSvelte(SearchBar, searchRootShadow, {
|
app = renderSvelte(SearchBar, searchRootShadow, {
|
||||||
transparencyEffects: api.settings.transparencyEffects,
|
transparencyEffects: api.settings.transparencyEffects ? true : false,
|
||||||
});
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error rendering Svelte component:', error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const title = document.querySelector('#title');
|
const title = document.querySelector('#title');
|
||||||
|
|||||||
Reference in New Issue
Block a user