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
17 changes: 13 additions & 4 deletions src/renderer/components/editor/Editor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ import 'codemirror/theme/neo.css'
import 'codemirror/theme/oceanic-next.css'

const { settings, cursorPosition } = useEditor()
const { selectedSnippetContent, selectedSnippet, isEmpty } = useSnippets()
const { selectedSnippetContent, selectedSnippet, isEmpty, selectedSnippetIds }
= useSnippets()

const { addToUpdateContentQueue } = useSnippetUpdate()

Expand Down Expand Up @@ -140,19 +141,27 @@ onMounted(() => {
data-editor
class="mt-[var(--title-bar-height)] grid grid-rows-[auto_1fr_auto] overflow-hidden"
>
<EditorHeader v-if="!isEmpty" />
<EditorHeader v-if="!isEmpty && selectedSnippetIds.length <= 1" />
<div
v-show="!isEmpty"
v-show="!isEmpty && selectedSnippetIds.length <= 1"
ref="editorRef"
class="overflow-auto"
/>
<EditorFooter v-if="!isEmpty" />
<EditorFooter v-if="!isEmpty && selectedSnippetIds.length <= 1" />
<div
v-if="isEmpty"
class="row-span-full flex items-center justify-center"
>
{{ i18n.t("snippet.noSelected") }}
</div>
<div
v-if="selectedSnippetIds.length > 1"
class="row-span-full flex items-center justify-center"
>
{{
i18n.t("snippet.selectedMultiple", { count: selectedSnippetIds.length })
}}
</div>
</div>
</template>

Expand Down
21 changes: 12 additions & 9 deletions src/renderer/components/sidebar/Sidebar.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<script setup lang="ts">
import type { Node } from '@/components/sidebar/folders/types'
import type { FoldersTreeResponse } from '@/services/api/generated'
import { ScrollArea } from '@/components/ui/shadcn/scroll-area'
import { useApp, useGutter, useSnippets } from '@/composables'
import { LibraryFilter } from '@/composables/types'
import { i18n, store } from '@/electron'
Expand Down Expand Up @@ -169,15 +170,17 @@ watch(width, () => {
<Plus class="h-4 w-4" />
</UiButton>
</div>
<div class="flex-grow overflow-auto">
<Tree
v-if="folders"
v-model="folders"
@click-node="onFolderClick"
@toggle-node="onFolderToggle"
@drag-node="onFolderDrag"
/>
</div>
<ScrollArea>
<div class="flex-grow">
<Tree
v-if="folders"
v-model="folders"
@click-node="onFolderClick"
@toggle-node="onFolderToggle"
@drag-node="onFolderDrag"
/>
</div>
</ScrollArea>
<UiGutter ref="gutterRef" />
</div>
</template>
34 changes: 31 additions & 3 deletions src/renderer/components/sidebar/folders/TreeNode.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<script setup lang="ts">
import type { CSSProperties } from 'vue'
import type { Node, Position } from './types'
import { useApp } from '@/composables'
import { useApp, useSnippets } from '@/composables'
import { onClickOutside } from '@vueuse/core'
import { ChevronRight, Folder } from 'lucide-vue-next'
import { isAllowed, store } from './composables'
Expand All @@ -25,7 +25,8 @@ const props = withDefaults(defineProps<Props>(), {
const { clickNode, dragNode, toggleNode, isHoveredByIdDisabled, focusHandler }
= inject(treeKeys)!

const { highlightedFolderId, selectedFolderId } = useApp()
const { highlightedFolderId, selectedFolderId, selectedSnippetId } = useApp()
const { displayedSnippets, updateSnippets, selectFirstSnippet } = useSnippets()

const hoveredId = ref()
const overPosition = ref<Position>()
Expand Down Expand Up @@ -151,7 +152,34 @@ function onDragLeave() {
overPosition.value = undefined
}

function onDrop() {
async function onDrop(e: DragEvent) {
const snippetIds = JSON.parse(e.dataTransfer?.getData('snippetIds') || '[]')
const snippets = displayedSnippets.value?.filter(s =>
snippetIds.includes(s.id),
)

// Если все сниппеты уже в папке
if (snippets && snippets.every(s => s.folder?.id === props.node.id)) {
return
}

if (snippets) {
const ids = snippets.map(s => s.id)
const data = snippets.map(s => ({
name: s.name,
folderId: props.node.id,
description: s.description,
isDeleted: s.isDeleted,
isFavorites: s.isFavorites,
}))

await updateSnippets(ids, data)

if (selectedSnippetId.value && ids.includes(selectedSnippetId.value)) {
selectFirstSnippet()
}
}

if (!store.dragNode || !isAllowed.value)
return

Expand Down
60 changes: 54 additions & 6 deletions src/renderer/components/snippet/Item.vue
Original file line number Diff line number Diff line change
Expand Up @@ -24,18 +24,24 @@ const {
deleteSnippet,
selectFirstSnippet,
duplicateSnippet,
selectedSnippetIds,
} = useSnippets()

const isFocused = ref(false)
const snippetRef = ref<HTMLDivElement>()

const isSelected = computed(() => selectedSnippetId.value === props.snippet.id)
const isInMultiSelection = computed(
() =>
selectedSnippetIds.value.length > 1
&& selectedSnippetIds.value.includes(props.snippet.id),
)
const isHighlighted = computed(
() => highlightedSnippetId.value === props.snippet.id,
)

function onSnippetClick(id: number) {
selectSnippet(id)
function onSnippetClick(id: number, event: MouseEvent) {
selectSnippet(id, event.shiftKey)

if (!isSearch.value) {
selectedSnippetIdBeforeSearch.value = id
Expand All @@ -62,6 +68,38 @@ async function onDuplicate() {
isFocusedSnippetName.value = true
}

function onDragStart(event: DragEvent) {
const ids
= selectedSnippetIds.value.length > 1
? selectedSnippetIds.value
: [props.snippet.id]

event.dataTransfer?.setData('snippetIds', JSON.stringify(ids))

const el = document.createElement('div')

if (selectedSnippetIds.value.length > 1) {
el.className
= 'fixed left-[-100%] text-fg truncate max-w-[200px] flex items-center'
el.id = 'ghost'
el.innerHTML = `
<span class="rounded-full bg-primary text-white px-2 py-0.5 text-xs ml-3">
${selectedSnippetIds.value.length}
</span>
`
}
else {
el.className = 'fixed left-[-100%] text-fg truncate max-w-[200px]'
el.id = 'ghost'
el.innerHTML = props.snippet.name
}

document.body.appendChild(el)
event.dataTransfer?.setDragImage(el, 0, 0)

setTimeout(() => el.remove(), 0)
}

onClickOutside(snippetRef, () => {
isFocused.value = false
highlightedSnippetId.value = undefined
Expand All @@ -72,14 +110,17 @@ onClickOutside(snippetRef, () => {
<div
ref="snippetRef"
data-snippet-item
class="border-border relative not-first:border-t [&+.is-selected+div]:border-transparent"
class="border-border relative not-first:border-t focus-visible:outline-none [&+.is-selected+div]:border-transparent"
:class="{
'is-selected': isSelected,
'is-multi-selected': isInMultiSelection,
'is-focused': isFocused,
'is-highlighted': isHighlighted,
}"
@click="onSnippetClick(snippet.id)"
draggable="true"
@click="(event) => onSnippetClick(snippet.id, event)"
@contextmenu="onClickContextMenu"
@dragstart.stop="onDragStart"
>
<ContextMenu.Root>
<ContextMenu.Trigger>
Expand Down Expand Up @@ -120,7 +161,13 @@ onClickOutside(snippetRef, () => {
@apply text-list-selection-fg;
}
}
&.is-focused {
&.is-multi-selected {
@apply bg-list-selection/80 text-list-selection-fg rounded-md;
.meta {
@apply text-list-selection-fg;
}
}
&.is-focused:not(.is-multi-selected) {
@apply bg-list-focus text-list-focus-fg rounded-md;
.meta {
@apply text-list-focus-fg;
Expand All @@ -129,7 +176,8 @@ onClickOutside(snippetRef, () => {
&.is-highlighted {
@apply outline-list-focus rounded-md outline-2 -outline-offset-2;
&.is-focused,
&.is-selected {
&.is-selected,
&.is-multi-selected {
@apply bg-bg text-list-selection-fg;
.meta {
@apply text-list-selection-fg;
Expand Down
19 changes: 11 additions & 8 deletions src/renderer/components/snippet/List.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<script setup lang="ts">
import type { SnippetsQuery } from '@/services/api/generated'
import { ScrollArea } from '@/components/ui/shadcn/scroll-area'
import { useApp, useGutter, useSnippets } from '@/composables'
import { LibraryFilter } from '@/composables/types'
import { store } from '@/electron'
Expand All @@ -10,7 +11,7 @@ const gutterRef = ref<{ $el: HTMLElement }>()

const { snippetListWidth, sidebarWidth, selectedFolderId, selectedLibrary }
= useApp()
const { snippets, snippetsBySearch, isSearch, getSnippets } = useSnippets()
const { displayedSnippets, getSnippets } = useSnippets()

async function initGetSnippets() {
const query: SnippetsQuery = {}
Expand Down Expand Up @@ -67,13 +68,15 @@ watch(width, () => {
<div>
<SnippetHeader />
</div>
<div class="flex-grow overflow-y-auto">
<SnippetItem
v-for="snippet in isSearch ? snippetsBySearch : snippets"
:key="snippet.id"
:snippet="snippet"
/>
</div>
<ScrollArea>
<div class="flex-grow overflow-y-auto">
<SnippetItem
v-for="snippet in displayedSnippets"
:key="snippet.id"
:snippet="snippet"
/>
</div>
</ScrollArea>
<UiGutter ref="gutterRef" />
</div>
</template>
Loading