Skip to content
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
11 changes: 10 additions & 1 deletion src/main/i18n/locales/en_US/ui.json
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,16 @@
}
},
"folder": {
"untitled": "Untitled folder"
"untitled": "Untitled folder",
"iconPicker": {
"searchPlaceholder": "Search icons...",
"emptyResults": "No icons found",
"filters": {
"all": "All",
"material": "Material",
"lucide": "Lucide"
}
}
},
"button": {
"back": "Back",
Expand Down
11 changes: 10 additions & 1 deletion src/main/i18n/locales/ru_RU/ui.json
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,16 @@
}
},
"folder": {
"untitled": "Папка без названия"
"untitled": "Папка без названия",
"iconPicker": {
"searchPlaceholder": "Поиск иконок...",
"emptyResults": "Иконки не найдены",
"filters": {
"all": "Все",
"material": "Material",
"lucide": "Lucide"
}
}
},
"button": {
"back": "Назад",
Expand Down
149 changes: 114 additions & 35 deletions src/renderer/components/sidebar/folders/custom-icons/CustomIcons.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
<script setup lang="ts">
import UiInput from '~/renderer/components/ui/input/Input.vue'
import {
type FolderIconFilter,
type FolderIconSource,
getFilteredFolderIcons,
groupFolderIcons,
} from '@/components/ui/folder-icon/icons'
import { i18n } from '@/electron'
import { useFolders } from '~/renderer/composables'
import { icons, iconsSet } from './icons'

interface Props {
nodeId: number
Expand All @@ -13,20 +18,28 @@ const props = defineProps<Props>()
const { updateFolder, getFolders } = useFolders()

const search = ref('')
const filter = ref<FolderIconFilter>('material')

const containerRef = useTemplateRef('containerRef')
const listRef = useTemplateRef('listRef')

const iconsBySearch = computed(() => {
if (search.value === '') {
return icons
}
return icons.filter(i => i.name?.includes(search.value.toLowerCase()))
})
const filterOptions = computed<
Array<{ label: string, value: FolderIconFilter }>
>(() => [
{ label: i18n.t('folder.iconPicker.filters.material'), value: 'material' },
{ label: i18n.t('folder.iconPicker.filters.lucide'), value: 'lucide' },
])

const visibleIcons = computed(() =>
getFilteredFolderIcons(search.value, filter.value),
)

const iconSections = computed(() => groupFolderIcons(visibleIcons.value))

const selectedIndex = ref(-1)

function onKeydown(e: KeyboardEvent) {
const len = iconsBySearch.value.length
const len = visibleIcons.value.length

if (e.key === 'ArrowDown') {
e.preventDefault()
Expand All @@ -42,19 +55,28 @@ function onKeydown(e: KeyboardEvent) {
}
else if (e.key === 'Enter') {
e.preventDefault()
onSet(iconsBySearch.value[selectedIndex.value].name!)

const icon = visibleIcons.value[selectedIndex.value]

if (icon) {
onSet(icon.value)
}
}
}

async function onSet(name: string) {
function getSectionTitle(source: FolderIconSource) {
return i18n.t(`folder.iconPicker.filters.${source}`)
}

async function onSet(value: string) {
if (!props.nodeId)
return

if (props.onSetIcon) {
await props.onSetIcon(props.nodeId, name)
await props.onSetIcon(props.nodeId, value)
}
else {
await updateFolder(props.nodeId, { icon: name })
await updateFolder(props.nodeId, { icon: value })
await getFolders()
}

Expand All @@ -63,17 +85,21 @@ async function onSet(name: string) {
)
}

watch(
() => search.value,
() => {
selectedIndex.value = -1
},
)
watch(search, () => {
selectedIndex.value = -1
})

watch(filter, () => {
selectedIndex.value = -1
nextTick(() => {
listRef.value?.scrollTo({ top: 0 })
})
})

watch(
() => iconsBySearch.value,
visibleIcons,
() => {
if (selectedIndex.value >= iconsBySearch.value.length) {
if (selectedIndex.value >= visibleIcons.value.length) {
selectedIndex.value = -1
}
},
Expand All @@ -96,29 +122,82 @@ watch(selectedIndex, () => {
ref="containerRef"
class="space-y-5"
>
<div>
<div class="space-y-3">
<UiInput
v-model="search"
placeholder="Search..."
:placeholder="i18n.t('folder.iconPicker.searchPlaceholder')"
@keydown="onKeydown"
/>
<UiShadcnTabs
v-model="filter"
class="gap-0"
>
<UiShadcnTabsList>
<UiShadcnTabsTrigger
v-for="item in filterOptions"
:key="item.value"
:value="item.value"
>
{{ item.label }}
</UiShadcnTabsTrigger>
</UiShadcnTabsList>
</UiShadcnTabs>
</div>
<div class="scrollbar max-h-[200px] overflow-y-auto">
<div class="grid auto-rows-[36px] grid-cols-8 gap-2">
<div
ref="listRef"
class="scrollbar max-h-[280px] overflow-y-auto"
>
<div
v-if="iconSections.length"
class="space-y-4"
>
<div
v-for="(icon, index) in iconsBySearch"
:id="`icon-${index}`"
:key="icon.name"
class="user-select-none flex items-center justify-center rounded-md"
:class="index === selectedIndex ? 'bg-muted' : 'hover:bg-muted'"
@click="onSet(icon.name!)"
v-for="section in iconSections"
:key="section.source"
class="space-y-2"
>
<span
class="*:size-5"
v-html="iconsSet[icon.name!]"
/>
<UiText
as="div"
variant="xs"
weight="medium"
muted
uppercase
class="px-1 tracking-[0.08em]"
>
{{ getSectionTitle(section.source) }}
</UiText>
<div class="grid auto-rows-[36px] grid-cols-8 gap-2">
<div
v-for="icon in section.items"
:id="`icon-${visibleIcons.findIndex((item) => item.value === icon.value)}`"
:key="icon.value"
class="user-select-none flex items-center justify-center rounded-md"
:class="
visibleIcons[selectedIndex]?.value === icon.value
? 'bg-muted'
: 'hover:bg-muted'
"
@click="onSet(icon.value)"
>
<UiFolderIcon
:name="icon.value"
class="size-5"
/>
</div>
</div>
</div>
</div>
<div
v-else
class="flex min-h-24 items-center justify-center px-4 text-center"
>
<UiText
variant="sm"
muted
>
{{ i18n.t("folder.iconPicker.emptyResults") }}
</UiText>
</div>
</div>
</div>
</template>
20 changes: 0 additions & 20 deletions src/renderer/components/sidebar/folders/custom-icons/icons.ts

This file was deleted.

24 changes: 21 additions & 3 deletions src/renderer/components/ui/folder-icon/FolderIcon.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
<script setup lang="ts">
import type { Variants } from './variants'
import { cn } from '@/utils'
import { iconsSet } from './icons'
import { Folder } from 'lucide-vue-next'
import { materialIconInnerSvgClass, resolveFolderIcon } from './icons'
import { variants } from './variants'

interface Props {
Expand All @@ -11,12 +12,29 @@ interface Props {
}

const props = defineProps<Props>()

const resolvedIcon = computed(() => resolveFolderIcon(props.name))
const iconClass = computed(() =>
cn(variants({ size: props.size }), props.class),
)
</script>

<template>
<component
:is="resolvedIcon.component"
v-if="resolvedIcon?.component"
:class="iconClass"
:data-icon-name="resolvedIcon.name"
/>
<div
:class="cn(variants({ size }), props.class)"
v-else-if="resolvedIcon?.svg"
:class="cn(iconClass, materialIconInnerSvgClass)"
:data-icon-name="resolvedIcon.name"
v-html="resolvedIcon.svg"
/>
<Folder
v-else
:class="iconClass"
:data-icon-name="name"
v-html="iconsSet[name]"
/>
</template>
Loading