feat: implement selected result management with selection logic and UI integration

This commit is contained in:
PandaDEV 2025-03-15 00:03:47 +01:00
parent 2d3d92f3c8
commit 7b624bd352
No known key found for this signature in database
GPG key ID: 13EFF9BAF70EE75C
3 changed files with 627 additions and 505 deletions

View file

@ -6,363 +6,412 @@ $text: #e5dfd5;
$text2: #ada9a1;
$mutedtext: #78756f;
.bg {
width: 100%;
height: 100%;
$search-height: 56px;
$sidebar-width: 286px;
$bottom-bar-height: 39px;
$info-panel-height: 160px;
$content-view-height: calc(
100% - $search-height - $info-panel-height - $bottom-bar-height
);
main {
width: 100vw;
height: 100vh;
background-color: $primary;
border: 1px solid $divider;
display: flex;
flex-direction: column;
border-radius: 12px;
z-index: -1;
position: fixed;
outline: none;
justify-content: space-between;
}
.search {
.content {
height: 376px;
width: 100%;
position: fixed;
top: 0;
left: 0;
height: 56px;
background-color: transparent;
outline: none;
border: none;
font-size: 18px;
color: $text;
padding-inline: 16px;
border-bottom: 1px solid $divider;
font-family: SFRoundedMedium;
display: flex;
}
.results {
position: absolute;
width: 286px;
top: 55px;
left: 0;
height: 417px;
border-right: 1px solid $divider;
display: flex;
flex-direction: column;
padding-inline: 8px;
padding-bottom: 8px;
overflow-y: auto;
overflow-x: hidden;
z-index: 3;
.result {
height: 40px;
font-size: 14px;
align-items: center;
display: flex;
padding: 10px;
padding-inline: 10px;
letter-spacing: 0.5px;
gap: 10px;
overflow: hidden;
text-overflow: clip;
white-space: nowrap;
color: $text;
}
.result {
cursor: pointer;
&.selected {
background-color: $divider;
}
}
padding: 14px 8px;
gap: 8px;
width: 286px;
border-right: 1px solid var(--border);
.time-separator {
font-size: 12px;
color: $text2;
font-family: SFRoundedSemiBold;
padding-left: 8px;
padding-bottom: 8px;
padding-top: 14px;
}
.favicon {
width: 18px;
height: 18px;
.group {
& + .group {
margin-top: 16px;
}
.image {
width: 18px;
height: 18px;
.time-separator {
margin-bottom: 8px;
}
.results-group {
display: flex;
flex-direction: column;
}
}
.favicon,
.image,
.icon {
width: 18px;
height: 18px;
}
}
.content {
position: absolute;
top: 55px;
left: 285px;
height: 220px;
font-family: CommitMono !important;
font-size: 12px;
letter-spacing: 1;
border-radius: 10px;
width: 465px;
white-space: pre-wrap;
word-wrap: break-word;
display: flex;
flex-direction: column;
align-items: center;
overflow: hidden;
z-index: 2;
color: $text;
// .bg {
// width: 100%;
// height: 100%;
// background-color: $primary;
// border: 1px solid $divider;
// border-radius: 12px;
// z-index: -1;
// position: fixed;
// outline: none;
// display: flex;
// flex-direction: column;
// }
&:not(:has(.image)) {
padding: 8px;
}
// .search {
// width: 100%;
// height: $search-height;
// background-color: transparent;
// outline: none;
// border: none;
// font-size: 18px;
// color: $text;
// padding-inline: 16px;
// border-bottom: 1px solid $divider;
// font-family: SFRoundedMedium;
// }
span {
font-family: CommitMono !important;
}
// .main-container {
// display: flex;
// flex: 1;
// }
.image {
width: 100%;
height: 100%;
object-fit: contain;
object-position: center;
}
}
// .results {
// width: $sidebar-width;
// height: calc(100vh - $search-height - $bottom-bar-height);
// border-right: 1px solid $divider;
// display: flex;
// flex-direction: column;
// padding-inline: 8px;
// padding-bottom: 8px;
// overflow-y: auto;
// overflow-x: hidden;
// z-index: 3;
.bottom-bar {
height: 39px;
width: calc(100vw - 2px);
backdrop-filter: blur(18px);
background-color: hsla(40, 3%, 16%, 0.8);
position: fixed;
bottom: 1px;
left: 1px;
z-index: 100;
border-radius: 0 0 12px 12px;
display: flex;
flex-direction: row;
justify-content: space-between;
padding-inline: 12px;
padding-right: 6px;
padding-top: 1px;
align-items: center;
font-size: 14px;
border-top: 1px solid $divider;
// .result {
// height: 40px;
// font-size: 14px;
// display: flex;
// align-items: center;
// padding: 10px;
// letter-spacing: 0.5px;
// gap: 10px;
// overflow: hidden;
// text-overflow: clip;
// white-space: nowrap;
// color: $text;
// cursor: pointer;
p {
color: $text2;
}
// &.selected {
// background-color: $divider;
// }
// }
.left {
display: flex;
align-items: center;
gap: 8px;
// .time-separator {
// font-size: 12px;
// color: $text2;
// font-family: SFRoundedSemiBold;
// padding-left: 8px;
// padding-bottom: 8px;
// padding-top: 14px;
// }
.logo {
width: 18px;
height: 18px;
}
}
// .favicon,
// .image,
// .icon {
// width: 18px;
// height: 18px;
// }
// }
.right {
display: flex;
align-items: center;
// .right-panel {
// display: flex;
// flex-direction: column;
// flex: 1;
// }
.paste p {
color: $text;
}
// .content {
// height: $content-view-height;
// font-family: CommitMono !important;
// font-size: 12px;
// letter-spacing: 1;
// border-radius: 10px;
// width: calc(100% - $sidebar-width);
// white-space: pre-wrap;
// word-wrap: break-word;
// display: flex;
// flex-direction: column;
// align-items: center;
// overflow: hidden;
// z-index: 2;
// color: $text;
.actions div {
display: flex;
align-items: center;
gap: 2px;
}
// &:not(:has(.image)) {
// padding: 8px;
// }
.divider {
width: 2px;
height: 12px;
background-color: $divider;
margin-left: 8px;
margin-right: 4px;
transition: all 0.2s;
}
// span {
// font-family: CommitMono !important;
// }
.paste,
.actions {
padding: 4px;
padding-left: 8px;
display: flex;
align-items: center;
gap: 8px;
border-radius: 7px;
background-color: transparent;
transition: all 0.2s;
cursor: pointer;
}
// .image {
// width: 100%;
// height: 100%;
// object-fit: contain;
// object-position: center;
// }
// }
.paste:hover,
.actions:hover {
background-color: $divider;
}
// .bottom-bar {
// height: $bottom-bar-height;
// width: 100%;
// backdrop-filter: blur(18px);
// background-color: hsla(40, 3%, 16%, 0.8);
// z-index: 100;
// border-radius: 0 0 12px 12px;
// display: flex;
// flex-direction: row;
// justify-content: space-between;
// padding-inline: 12px;
// padding-right: 6px;
// padding-top: 1px;
// align-items: center;
// font-size: 14px;
// border-top: 1px solid $divider;
&:hover .paste:hover ~ .divider,
&:hover .actions:hover ~ .divider {
opacity: 0;
}
}
}
// p {
// color: $text2;
// }
.information {
position: absolute;
display: flex;
flex-direction: column;
gap: 14px;
bottom: 39px;
left: 285px;
height: 160px;
width: 465px;
border-top: 1px solid $divider;
background-color: $primary;
padding: 14px;
z-index: 1;
// .left {
// display: flex;
// align-items: center;
// gap: 8px;
.title {
font-family: SFRoundedSemiBold;
font-size: 12px;
letter-spacing: 0.6px;
color: $text;
}
// .logo {
// width: 18px;
// height: 18px;
// }
// }
.info-content {
display: flex;
gap: 0;
flex-direction: column;
// .right {
// display: flex;
// align-items: center;
.info-row {
display: flex;
width: 100%;
font-size: 12px;
justify-content: space-between;
padding: 8px 0;
border-bottom: 1px solid $divider;
line-height: 1;
// .paste p {
// color: $text;
// }
&:last-child {
border-bottom: none;
padding-bottom: 0;
}
// .actions div {
// display: flex;
// align-items: center;
// gap: 2px;
// }
&:first-child {
padding-top: 22px;
}
// .divider {
// width: 2px;
// height: 12px;
// background-color: $divider;
// margin-left: 8px;
// margin-right: 4px;
// transition: all 0.2s;
// }
p {
font-family: SFRoundedMedium;
color: $text2;
font-weight: 500;
flex-shrink: 0;
}
// .paste,
// .actions {
// padding: 4px;
// padding-left: 8px;
// display: flex;
// align-items: center;
// gap: 8px;
// border-radius: 7px;
// background-color: transparent;
// transition: all 0.2s;
// cursor: pointer;
// }
span {
font-family: CommitMono;
color: $text;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
margin-left: 32px;
}
}
}
}
// .paste:hover,
// .actions:hover {
// background-color: $divider;
// }
.clothoid-corner {
clip-path: polygon(
13.890123px 0px,
calc(100% - 13.890123px) 0px,
calc(100% - 12.723414px) 0.004211px,
calc(100% - 11.556933px) 0.025635px,
calc(100% - 10.391895px) 0.085062px,
calc(100% - 9.231074px) 0.199291px,
calc(100% - 8.079275px) 0.382298px,
calc(100% - 6.947448px) 0.662609px,
calc(100% - 5.844179px) 1.039291px,
calc(100% - 4.793324px) 1.542842px,
calc(100% - 3.811369px) 2.169728px,
calc(100% - 2.926417px) 2.926417px,
calc(100% - 2.169728px) 3.811369px,
calc(100% - 1.542842px) 4.793324px,
calc(100% - 1.039291px) 5.844179px,
calc(100% - 0.662609px) 6.947448px,
calc(100% - 0.382298px) 8.079275px,
calc(100% - 0.199291px) 9.231074px,
calc(100% - 0.085062px) 10.391895px,
calc(100% - 0.025635px) 11.556933px,
calc(100% - 0.004211px) 12.723414px,
100% 13.890123px,
100% calc(100% - 13.890123px),
calc(100% - 0.004211px) calc(100% - 12.723414px),
calc(100% - 0.025635px) calc(100% - 11.556933px),
calc(100% - 0.085062px) calc(100% - 10.391895px),
calc(100% - 0.199291px) calc(100% - 9.231074px),
calc(100% - 0.382298px) calc(100% - 8.079275px),
calc(100% - 0.662609px) calc(100% - 6.947448px),
calc(100% - 1.039291px) calc(100% - 5.844179px),
calc(100% - 1.542842px) calc(100% - 4.793324px),
calc(100% - 2.169728px) calc(100% - 3.811369px),
calc(100% - 2.926417px) calc(100% - 2.926417px),
calc(100% - 3.811369px) calc(100% - 2.169728px),
calc(100% - 4.793324px) calc(100% - 1.542842px),
calc(100% - 5.844179px) calc(100% - 1.039291px),
calc(100% - 6.947448px) calc(100% - 0.662609px),
calc(100% - 8.079275px) calc(100% - 0.382298px),
calc(100% - 9.231074px) calc(100% - 0.199291px),
calc(100% - 10.391895px) calc(100% - 0.085062px),
calc(100% - 11.556933px) calc(100% - 0.025635px),
calc(100% - 12.723414px) calc(100% - 0.004211px),
calc(100% - 13.890123px) 100%,
13.890123px 100%,
12.723414px calc(100% - 0.004211px),
11.556933px calc(100% - 0.025635px),
10.391895px calc(100% - 0.085062px),
9.231074px calc(100% - 0.199291px),
8.079275px calc(100% - 0.382298px),
6.947448px calc(100% - 0.662609px),
5.844179px calc(100% - 1.039291px),
4.793324px calc(100% - 1.542842px),
3.811369px calc(100% - 2.169728px),
2.926417px calc(100% - 2.926417px),
2.169728px calc(100% - 3.811369px),
1.542842px calc(100% - 4.793324px),
1.039291px calc(100% - 5.844179px),
0.662609px calc(100% - 6.947448px),
0.382298px calc(100% - 8.079275px),
0.199291px calc(100% - 9.231074px),
0.085062px calc(100% - 10.391895px),
0.025635px calc(100% - 11.556933px),
0.004211px calc(100% - 12.723414px),
0px calc(100% - 13.890123px),
0px 13.890123px,
0.004211px 12.723414px,
0.025635px 11.556933px,
0.085062px 10.391895px,
0.199291px 9.231074px,
0.382298px 8.079275px,
0.662609px 6.947448px,
1.039291px 5.844179px,
1.542842px 4.793324px,
2.169728px 3.811369px,
2.926417px 2.926417px,
3.811369px 2.169728px,
4.793324px 1.542842px,
5.844179px 1.039291px,
6.947448px 0.662609px,
8.079275px 0.382298px,
9.231074px 0.199291px,
10.391895px 0.085062px,
11.556933px 0.025635px,
12.723414px 0.004211px,
13.890123px 0px
);
}
// &:hover .paste:hover ~ .divider,
// &:hover .actions:hover ~ .divider {
// opacity: 0;
// }
// }
// }
// .information {
// height: $info-panel-height;
// width: calc(100% - $sidebar-width);
// border-top: 1px solid $divider;
// background-color: $primary;
// padding: 14px;
// z-index: 1;
// display: flex;
// flex-direction: column;
// gap: 14px;
// .title {
// font-family: SFRoundedSemiBold;
// font-size: 12px;
// letter-spacing: 0.6px;
// color: $text;
// }
// .info-content {
// display: flex;
// gap: 0;
// flex-direction: column;
// .info-row {
// display: flex;
// width: 100%;
// font-size: 12px;
// justify-content: space-between;
// padding: 8px 0;
// border-bottom: 1px solid $divider;
// line-height: 1;
// &:last-child {
// border-bottom: none;
// padding-bottom: 0;
// }
// &:first-child {
// padding-top: 22px;
// }
// p {
// font-family: SFRoundedMedium;
// color: $text2;
// font-weight: 500;
// flex-shrink: 0;
// }
// span {
// font-family: CommitMono;
// color: $text;
// text-overflow: ellipsis;
// overflow: hidden;
// white-space: nowrap;
// margin-left: 32px;
// }
// }
// }
// }
// .clothoid-corner {
// clip-path: polygon(
// 13.890123px 0px,
// calc(100% - 13.890123px) 0px,
// calc(100% - 12.723414px) 0.004211px,
// calc(100% - 11.556933px) 0.025635px,
// calc(100% - 10.391895px) 0.085062px,
// calc(100% - 9.231074px) 0.199291px,
// calc(100% - 8.079275px) 0.382298px,
// calc(100% - 6.947448px) 0.662609px,
// calc(100% - 5.844179px) 1.039291px,
// calc(100% - 4.793324px) 1.542842px,
// calc(100% - 3.811369px) 2.169728px,
// calc(100% - 2.926417px) 2.926417px,
// calc(100% - 2.169728px) 3.811369px,
// calc(100% - 1.542842px) 4.793324px,
// calc(100% - 1.039291px) 5.844179px,
// calc(100% - 0.662609px) 6.947448px,
// calc(100% - 0.382298px) 8.079275px,
// calc(100% - 0.199291px) 9.231074px,
// calc(100% - 0.085062px) 10.391895px,
// calc(100% - 0.025635px) 11.556933px,
// calc(100% - 0.004211px) 12.723414px,
// 100% 13.890123px,
// 100% calc(100% - 13.890123px),
// calc(100% - 0.004211px) calc(100% - 12.723414px),
// calc(100% - 0.025635px) calc(100% - 11.556933px),
// calc(100% - 0.085062px) calc(100% - 10.391895px),
// calc(100% - 0.199291px) calc(100% - 9.231074px),
// calc(100% - 0.382298px) calc(100% - 8.079275px),
// calc(100% - 0.662609px) calc(100% - 6.947448px),
// calc(100% - 1.039291px) calc(100% - 5.844179px),
// calc(100% - 1.542842px) calc(100% - 4.793324px),
// calc(100% - 2.169728px) calc(100% - 3.811369px),
// calc(100% - 2.926417px) calc(100% - 2.926417px),
// calc(100% - 3.811369px) calc(100% - 2.169728px),
// calc(100% - 4.793324px) calc(100% - 1.542842px),
// calc(100% - 5.844179px) calc(100% - 1.039291px),
// calc(100% - 6.947448px) calc(100% - 0.662609px),
// calc(100% - 8.079275px) calc(100% - 0.382298px),
// calc(100% - 9.231074px) calc(100% - 0.199291px),
// calc(100% - 10.391895px) calc(100% - 0.085062px),
// calc(100% - 11.556933px) calc(100% - 0.025635px),
// calc(100% - 12.723414px) calc(100% - 0.004211px),
// calc(100% - 13.890123px) 100%,
// 13.890123px 100%,
// 12.723414px calc(100% - 0.004211px),
// 11.556933px calc(100% - 0.025635px),
// 10.391895px calc(100% - 0.085062px),
// 9.231074px calc(100% - 0.199291px),
// 8.079275px calc(100% - 0.382298px),
// 6.947448px calc(100% - 0.662609px),
// 5.844179px calc(100% - 1.039291px),
// 4.793324px calc(100% - 1.542842px),
// 3.811369px calc(100% - 2.169728px),
// 2.926417px calc(100% - 2.926417px),
// 2.169728px calc(100% - 3.811369px),
// 1.542842px calc(100% - 4.793324px),
// 1.039291px calc(100% - 5.844179px),
// 0.662609px calc(100% - 6.947448px),
// 0.382298px calc(100% - 8.079275px),
// 0.199291px calc(100% - 9.231074px),
// 0.085062px calc(100% - 10.391895px),
// 0.025635px calc(100% - 11.556933px),
// 0.004211px calc(100% - 12.723414px),
// 0px calc(100% - 13.890123px),
// 0px 13.890123px,
// 0.004211px 12.723414px,
// 0.025635px 11.556933px,
// 0.085062px 10.391895px,
// 0.199291px 9.231074px,
// 0.382298px 8.079275px,
// 0.662609px 6.947448px,
// 1.039291px 5.844179px,
// 1.542842px 4.793324px,
// 2.169728px 3.811369px,
// 2.926417px 2.926417px,
// 3.811369px 2.169728px,
// 4.793324px 1.542842px,
// 5.844179px 1.039291px,
// 6.947448px 0.662609px,
// 8.079275px 0.382298px,
// 9.231074px 0.199291px,
// 10.391895px 0.085062px,
// 11.556933px 0.025635px,
// 12.723414px 0.004211px,
// 13.890123px 0px
// );
// }

54
lib/selectedResult.ts Normal file
View file

@ -0,0 +1,54 @@
import type { HistoryItem } from '~/types/types'
interface GroupedHistory {
label: string
items: HistoryItem[]
}
export const selectedGroupIndex = ref(0)
export const selectedItemIndex = ref(0)
export const selectedElement = ref<HTMLElement | null>(null)
export const useSelectedResult = (groupedHistory: Ref<GroupedHistory[]>) => {
const selectedItem = computed<HistoryItem | null>(() => {
const group = groupedHistory.value[selectedGroupIndex.value]
return group?.items[selectedItemIndex.value] ?? null
})
const isSelected = (groupIndex: number, itemIndex: number): boolean => {
return selectedGroupIndex.value === groupIndex && selectedItemIndex.value === itemIndex
}
const selectNext = (): void => {
const currentGroup = groupedHistory.value[selectedGroupIndex.value]
if (selectedItemIndex.value < currentGroup.items.length - 1) {
selectedItemIndex.value++
} else if (selectedGroupIndex.value < groupedHistory.value.length - 1) {
selectedGroupIndex.value++
selectedItemIndex.value = 0
}
}
const selectPrevious = (): void => {
if (selectedItemIndex.value > 0) {
selectedItemIndex.value--
} else if (selectedGroupIndex.value > 0) {
selectedGroupIndex.value--
selectedItemIndex.value = groupedHistory.value[selectedGroupIndex.value].items.length - 1
}
}
const selectItem = (groupIndex: number, itemIndex: number): void => {
selectedGroupIndex.value = groupIndex
selectedItemIndex.value = itemIndex
}
return {
selectedItem,
isSelected,
selectNext,
selectPrevious,
selectItem,
selectedElement
}
}

View file

@ -1,5 +1,41 @@
<template>
<div class="bg" tabindex="0">
<main>
<TopBar
ref="topBar"
@search="searchHistory" />
<div class="content">
<OverlayScrollbarsComponent
class="results"
ref="resultsContainer"
:options="{ scrollbars: { autoHide: 'scroll' } }">
<div
v-for="(group, groupIndex) in groupedHistory"
:key="groupIndex"
class="group">
<div class="time-separator">{{ group.label }}</div>
<div class="results-group">
<Result
v-for="(item, index) in group.items"
:key="item.id"
:item="item"
:selected="isSelected(groupIndex, index)"
:image-url="imageUrls[item.id]"
:dimensions="imageDimensions[item.id]"
@select="selectItem(groupIndex, index)"
@image-error="onImageError"
@setRef="el => selectedElement = el" />
</div>
</div>
</OverlayScrollbarsComponent>
<div class="right">
<div class="content"></div>
<div class="information"></div>
</div>
</div>
<BottomBar />
</main>
<!-- <div class="bg" tabindex="0">
<input
ref="searchInput"
v-model="searchQuery"
@ -10,30 +46,8 @@
class="search"
type="text"
placeholder="Type to filter entries..." />
<div class="bottom-bar">
<div class="left">
<img class="logo" width="18px" src="../public/logo.png" alt="" />
<p>Qopy</p>
</div>
<div class="right">
<div class="paste" @click="pasteSelectedItem">
<p>Paste</p>
<img src="../public/enter.svg" alt="" />
</div>
<div class="divider"></div>
<div class="actions">
<p>Actions</p>
<div>
<img
v-if="os === 'windows' || os === 'linux'"
src="../public/ctrl.svg"
alt="" />
<img v-if="os === 'macos'" src="../public/cmd.svg" alt="" />
<img src="../public/k.svg" alt="" />
</div>
</div>
</div>
</div>
<div class="main-container">
<OverlayScrollbarsComponent
class="results"
ref="resultsContainer"
@ -115,6 +129,7 @@
</template>
</OverlayScrollbarsComponent>
<div class="right-panel">
<div
class="content"
v-if="selectedItem?.content_type === ContentType.Image">
@ -152,8 +167,35 @@
</div>
</div>
</OverlayScrollbarsComponent>
<Noise />
</div>
</div>
<div class="bottom-bar">
<div class="left">
<img class="logo" width="18px" src="../public/logo.png" alt="" />
<p>Qopy</p>
</div>
<div class="right">
<div class="paste" @click="pasteSelectedItem">
<p>Paste</p>
<img src="../public/enter.svg" alt="" />
</div>
<div class="divider"></div>
<div class="actions">
<p>Actions</p>
<div>
<img
v-if="os === 'windows' || os === 'linux'"
src="../public/ctrl.svg"
alt="" />
<img v-if="os === 'macos'" src="../public/cmd.svg" alt="" />
<img src="../public/k.svg" alt="" />
</div>
</div>
</div>
</div>
<Noise />
</div> -->
</template>
<script setup lang="ts">
@ -175,6 +217,7 @@ import type {
InfoCode,
} from "~/types/types";
import { Key, keyboard } from "wrdu-keyboard";
import { selectedGroupIndex, selectedItemIndex, selectedElement, useSelectedResult } from '~/lib/selectedResult'
interface GroupedHistory {
label: string;
@ -182,8 +225,11 @@ interface GroupedHistory {
}
const { $history } = useNuxtApp();
const CHUNK_SIZE = 50;
const SCROLL_THRESHOLD = 100;
const SCROLL_PADDING = 8;
const TOP_SCROLL_PADDING = 37;
const history = shallowRef<HistoryItem[]>([]);
let offset = 0;
@ -193,9 +239,6 @@ const resultsContainer = shallowRef<InstanceType<
typeof OverlayScrollbarsComponent
> | null>(null);
const searchQuery = ref("");
const selectedGroupIndex = ref(0);
const selectedItemIndex = ref(0);
const selectedElement = shallowRef<HTMLElement | null>(null);
const searchInput = ref<HTMLInputElement | null>(null);
const os = ref<string>("");
const imageUrls = shallowRef<Record<string, string>>({});
@ -207,6 +250,8 @@ const imageLoading = ref<boolean>(false);
const pageTitle = ref<string>("");
const pageOgImage = ref<string>("");
const topBar = ref<{ searchInput: HTMLInputElement | null } | null>(null)
const isSameDay = (date1: Date, date2: Date): boolean => {
return (
date1.getFullYear() === date2.getFullYear() &&
@ -268,10 +313,7 @@ const groupedHistory = computed<GroupedHistory[]>(() => {
.map(([label, items]) => ({ label, items }));
});
const selectedItem = computed<HistoryItem | null>(() => {
const group = groupedHistory.value[selectedGroupIndex.value];
return group?.items[selectedItemIndex.value] ?? null;
});
const { selectedItem, isSelected, selectNext, selectPrevious, selectItem } = useSelectedResult(groupedHistory)
const loadHistoryChunk = async (): Promise<void> => {
if (isLoading) return;
@ -352,81 +394,64 @@ const handleScroll = (): void => {
}
};
const scrollToSelectedItem = (forceScrollTop: boolean = false): void => {
const scrollToSelectedItem = (): void => {
nextTick(() => {
const osInstance = resultsContainer.value?.osInstance();
const viewport = osInstance?.elements().viewport;
const viewport = resultsContainer.value?.osInstance()?.elements().viewport;
if (!selectedElement.value || !viewport) return;
if (!forceScrollTop) {
setTimeout(() => {
if (!selectedElement.value) return;
const viewportRect = viewport.getBoundingClientRect();
const elementRect = selectedElement.value.getBoundingClientRect();
const isAbove = elementRect.top < viewportRect.top;
const isBelow = elementRect.bottom > viewportRect.bottom - 8;
const isFirstItemInGroup = selectedItemIndex.value === 0;
const isAbove = elementRect.top < viewportRect.top + SCROLL_PADDING;
const isBelow = elementRect.bottom > viewportRect.bottom - SCROLL_PADDING;
if (isAbove || isBelow) {
const scrollOffset = isAbove
? elementRect.top -
viewportRect.top -
(selectedItemIndex.value === 0 ? 36 : 8)
: elementRect.bottom - viewportRect.bottom + 9;
viewport.scrollBy({ top: scrollOffset, behavior: "smooth" });
}
if (isAbove) {
viewport.scrollTo({
top: viewport.scrollTop + (elementRect.top - viewportRect.top) - (isFirstItemInGroup ? TOP_SCROLL_PADDING : SCROLL_PADDING),
behavior: "smooth"
});
} else if (isBelow) {
viewport.scrollTo({
top: viewport.scrollTop + (elementRect.bottom - viewportRect.bottom) + SCROLL_PADDING,
behavior: "smooth"
});
}
}, 10);
});
};
const isSelected = (groupIndex: number, itemIndex: number): boolean => {
return (
selectedGroupIndex.value === groupIndex &&
selectedItemIndex.value === itemIndex
);
};
const searchHistory = async (query: string): Promise<void> => {
searchQuery.value = query
if (!query.trim()) {
history.value = []
offset = 0
await loadHistoryChunk()
return
}
const searchHistory = async (): Promise<void> => {
const results = await $history.searchHistory(searchQuery.value);
const results = await $history.searchHistory(query)
history.value = results.map((item) =>
Object.assign(
new HistoryItem(
item.source,
item.content_type,
item.content,
item.favicon
item.favicon,
item.source_icon,
item.language
),
{ id: item.id, timestamp: new Date(item.timestamp) }
)
);
};
)
const selectNext = (): void => {
const currentGroup = groupedHistory.value[selectedGroupIndex.value];
if (selectedItemIndex.value < currentGroup.items.length - 1) {
selectedItemIndex.value++;
} else if (selectedGroupIndex.value < groupedHistory.value.length - 1) {
selectedGroupIndex.value++;
selectedItemIndex.value = 0;
if (groupedHistory.value.length > 0) {
handleSelection(0, 0, false)
}
scrollToSelectedItem();
};
const selectPrevious = (): void => {
if (selectedItemIndex.value > 0) {
selectedItemIndex.value--;
} else if (selectedGroupIndex.value > 0) {
selectedGroupIndex.value--;
selectedItemIndex.value =
groupedHistory.value[selectedGroupIndex.value].items.length - 1;
}
scrollToSelectedItem();
};
const selectItem = (groupIndex: number, itemIndex: number): void => {
selectedGroupIndex.value = groupIndex;
selectedItemIndex.value = itemIndex;
scrollToSelectedItem();
};
}
const pasteSelectedItem = async (): Promise<void> => {
if (!selectedItem.value) return;
@ -554,10 +579,9 @@ const handleSelection = (
itemIndex: number,
shouldScroll: boolean = true
): void => {
selectedGroupIndex.value = groupIndex;
selectedItemIndex.value = itemIndex;
if (shouldScroll) scrollToSelectedItem();
};
selectItem(groupIndex, itemIndex)
if (shouldScroll) scrollToSelectedItem()
}
const setupEventListeners = async (): Promise<void> => {
await listen("clipboard-content-updated", async () => {
@ -592,7 +616,6 @@ const setupEventListeners = async (): Promise<void> => {
}
focusSearchInput();
// Re-register keyboard shortcuts on focus
keyboard.clear();
keyboard.prevent.down([Key.DownArrow], () => {
selectNext();
@ -666,9 +689,9 @@ const hideApp = async (): Promise<void> => {
const focusSearchInput = (): void => {
nextTick(() => {
searchInput.value?.focus();
});
};
topBar.value?.searchInput?.focus()
})
}
const onImageError = (): void => {
imageLoadError.value = true;
@ -677,10 +700,10 @@ const onImageError = (): void => {
watch([selectedGroupIndex, selectedItemIndex], () => {
scrollToSelectedItem();
});
}, { flush: 'post' });
watch(searchQuery, () => {
searchHistory();
searchHistory(searchQuery.value);
});
onMounted(async () => {
@ -699,10 +722,6 @@ onMounted(async () => {
}
});
watch([selectedGroupIndex, selectedItemIndex], () =>
scrollToSelectedItem(false)
);
const getFormattedDate = computed(() => {
if (!selectedItem.value?.timestamp) return "";
return new Intl.DateTimeFormat("en-US", {