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; $text2: #ada9a1;
$mutedtext: #78756f; $mutedtext: #78756f;
.bg { $search-height: 56px;
width: 100%; $sidebar-width: 286px;
height: 100%; $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; background-color: $primary;
border: 1px solid $divider; border: 1px solid $divider;
display: flex;
flex-direction: column;
border-radius: 12px; border-radius: 12px;
z-index: -1; justify-content: space-between;
position: fixed;
outline: none;
} }
.search { .content {
height: 376px;
width: 100%; width: 100%;
position: fixed; display: flex;
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;
} }
.results { .results {
position: absolute;
width: 286px;
top: 55px;
left: 0;
height: 417px;
border-right: 1px solid $divider;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
padding-inline: 8px; padding: 14px 8px;
padding-bottom: 8px; gap: 8px;
overflow-y: auto; width: 286px;
overflow-x: hidden; border-right: 1px solid var(--border);
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;
}
}
.time-separator { .time-separator {
font-size: 12px; font-size: 12px;
color: $text2; color: $text2;
font-family: SFRoundedSemiBold; font-family: SFRoundedSemiBold;
padding-left: 8px; padding-left: 8px;
padding-bottom: 8px;
padding-top: 14px;
} }
.favicon { .group {
width: 18px; & + .group {
height: 18px; margin-top: 16px;
} }
.image { .time-separator {
width: 18px; margin-bottom: 8px;
height: 18px; }
.results-group {
display: flex;
flex-direction: column;
}
} }
.favicon,
.image,
.icon { .icon {
width: 18px; width: 18px;
height: 18px; height: 18px;
} }
} }
.content { // .bg {
position: absolute; // width: 100%;
top: 55px; // height: 100%;
left: 285px; // background-color: $primary;
height: 220px; // border: 1px solid $divider;
font-family: CommitMono !important; // border-radius: 12px;
font-size: 12px; // z-index: -1;
letter-spacing: 1; // position: fixed;
border-radius: 10px; // outline: none;
width: 465px; // display: flex;
white-space: pre-wrap; // flex-direction: column;
word-wrap: break-word; // }
display: flex;
flex-direction: column;
align-items: center;
overflow: hidden;
z-index: 2;
color: $text;
&:not(:has(.image)) { // .search {
padding: 8px; // 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 { // .main-container {
font-family: CommitMono !important; // display: flex;
} // flex: 1;
// }
.image { // .results {
width: 100%; // width: $sidebar-width;
height: 100%; // height: calc(100vh - $search-height - $bottom-bar-height);
object-fit: contain; // border-right: 1px solid $divider;
object-position: center; // display: flex;
} // flex-direction: column;
} // padding-inline: 8px;
// padding-bottom: 8px;
// overflow-y: auto;
// overflow-x: hidden;
// z-index: 3;
.bottom-bar { // .result {
height: 39px; // height: 40px;
width: calc(100vw - 2px); // font-size: 14px;
backdrop-filter: blur(18px); // display: flex;
background-color: hsla(40, 3%, 16%, 0.8); // align-items: center;
position: fixed; // padding: 10px;
bottom: 1px; // letter-spacing: 0.5px;
left: 1px; // gap: 10px;
z-index: 100; // overflow: hidden;
border-radius: 0 0 12px 12px; // text-overflow: clip;
display: flex; // white-space: nowrap;
flex-direction: row; // color: $text;
justify-content: space-between; // cursor: pointer;
padding-inline: 12px;
padding-right: 6px;
padding-top: 1px;
align-items: center;
font-size: 14px;
border-top: 1px solid $divider;
p { // &.selected {
color: $text2; // background-color: $divider;
} // }
// }
.left { // .time-separator {
display: flex; // font-size: 12px;
align-items: center; // color: $text2;
gap: 8px; // font-family: SFRoundedSemiBold;
// padding-left: 8px;
// padding-bottom: 8px;
// padding-top: 14px;
// }
.logo { // .favicon,
width: 18px; // .image,
height: 18px; // .icon {
} // width: 18px;
} // height: 18px;
// }
// }
.right { // .right-panel {
display: flex; // display: flex;
align-items: center; // flex-direction: column;
// flex: 1;
// }
.paste p { // .content {
color: $text; // 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 { // &:not(:has(.image)) {
display: flex; // padding: 8px;
align-items: center; // }
gap: 2px;
}
.divider { // span {
width: 2px; // font-family: CommitMono !important;
height: 12px; // }
background-color: $divider;
margin-left: 8px;
margin-right: 4px;
transition: all 0.2s;
}
.paste, // .image {
.actions { // width: 100%;
padding: 4px; // height: 100%;
padding-left: 8px; // object-fit: contain;
display: flex; // object-position: center;
align-items: center; // }
gap: 8px; // }
border-radius: 7px;
background-color: transparent;
transition: all 0.2s;
cursor: pointer;
}
.paste:hover, // .bottom-bar {
.actions:hover { // height: $bottom-bar-height;
background-color: $divider; // 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, // p {
&:hover .actions:hover ~ .divider { // color: $text2;
opacity: 0; // }
}
}
}
.information { // .left {
position: absolute; // display: flex;
display: flex; // align-items: center;
flex-direction: column; // gap: 8px;
gap: 14px;
bottom: 39px;
left: 285px;
height: 160px;
width: 465px;
border-top: 1px solid $divider;
background-color: $primary;
padding: 14px;
z-index: 1;
.title { // .logo {
font-family: SFRoundedSemiBold; // width: 18px;
font-size: 12px; // height: 18px;
letter-spacing: 0.6px; // }
color: $text; // }
}
.info-content { // .right {
display: flex; // display: flex;
gap: 0; // align-items: center;
flex-direction: column;
.info-row { // .paste p {
display: flex; // color: $text;
width: 100%; // }
font-size: 12px;
justify-content: space-between;
padding: 8px 0;
border-bottom: 1px solid $divider;
line-height: 1;
&:last-child { // .actions div {
border-bottom: none; // display: flex;
padding-bottom: 0; // align-items: center;
} // gap: 2px;
// }
&:first-child { // .divider {
padding-top: 22px; // width: 2px;
} // height: 12px;
// background-color: $divider;
// margin-left: 8px;
// margin-right: 4px;
// transition: all 0.2s;
// }
p { // .paste,
font-family: SFRoundedMedium; // .actions {
color: $text2; // padding: 4px;
font-weight: 500; // padding-left: 8px;
flex-shrink: 0; // display: flex;
} // align-items: center;
// gap: 8px;
// border-radius: 7px;
// background-color: transparent;
// transition: all 0.2s;
// cursor: pointer;
// }
span { // .paste:hover,
font-family: CommitMono; // .actions:hover {
color: $text; // background-color: $divider;
text-overflow: ellipsis; // }
overflow: hidden;
white-space: nowrap;
margin-left: 32px;
}
}
}
}
.clothoid-corner { // &:hover .paste:hover ~ .divider,
clip-path: polygon( // &:hover .actions:hover ~ .divider {
13.890123px 0px, // opacity: 0;
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, // .information {
calc(100% - 8.079275px) 0.382298px, // height: $info-panel-height;
calc(100% - 6.947448px) 0.662609px, // width: calc(100% - $sidebar-width);
calc(100% - 5.844179px) 1.039291px, // border-top: 1px solid $divider;
calc(100% - 4.793324px) 1.542842px, // background-color: $primary;
calc(100% - 3.811369px) 2.169728px, // padding: 14px;
calc(100% - 2.926417px) 2.926417px, // z-index: 1;
calc(100% - 2.169728px) 3.811369px, // display: flex;
calc(100% - 1.542842px) 4.793324px, // flex-direction: column;
calc(100% - 1.039291px) 5.844179px, // gap: 14px;
calc(100% - 0.662609px) 6.947448px,
calc(100% - 0.382298px) 8.079275px, // .title {
calc(100% - 0.199291px) 9.231074px, // font-family: SFRoundedSemiBold;
calc(100% - 0.085062px) 10.391895px, // font-size: 12px;
calc(100% - 0.025635px) 11.556933px, // letter-spacing: 0.6px;
calc(100% - 0.004211px) 12.723414px, // color: $text;
100% 13.890123px, // }
100% calc(100% - 13.890123px),
calc(100% - 0.004211px) calc(100% - 12.723414px), // .info-content {
calc(100% - 0.025635px) calc(100% - 11.556933px), // display: flex;
calc(100% - 0.085062px) calc(100% - 10.391895px), // gap: 0;
calc(100% - 0.199291px) calc(100% - 9.231074px), // flex-direction: column;
calc(100% - 0.382298px) calc(100% - 8.079275px),
calc(100% - 0.662609px) calc(100% - 6.947448px), // .info-row {
calc(100% - 1.039291px) calc(100% - 5.844179px), // display: flex;
calc(100% - 1.542842px) calc(100% - 4.793324px), // width: 100%;
calc(100% - 2.169728px) calc(100% - 3.811369px), // font-size: 12px;
calc(100% - 2.926417px) calc(100% - 2.926417px), // justify-content: space-between;
calc(100% - 3.811369px) calc(100% - 2.169728px), // padding: 8px 0;
calc(100% - 4.793324px) calc(100% - 1.542842px), // border-bottom: 1px solid $divider;
calc(100% - 5.844179px) calc(100% - 1.039291px), // line-height: 1;
calc(100% - 6.947448px) calc(100% - 0.662609px),
calc(100% - 8.079275px) calc(100% - 0.382298px), // &:last-child {
calc(100% - 9.231074px) calc(100% - 0.199291px), // border-bottom: none;
calc(100% - 10.391895px) calc(100% - 0.085062px), // padding-bottom: 0;
calc(100% - 11.556933px) calc(100% - 0.025635px), // }
calc(100% - 12.723414px) calc(100% - 0.004211px),
calc(100% - 13.890123px) 100%, // &:first-child {
13.890123px 100%, // padding-top: 22px;
12.723414px calc(100% - 0.004211px), // }
11.556933px calc(100% - 0.025635px),
10.391895px calc(100% - 0.085062px), // p {
9.231074px calc(100% - 0.199291px), // font-family: SFRoundedMedium;
8.079275px calc(100% - 0.382298px), // color: $text2;
6.947448px calc(100% - 0.662609px), // font-weight: 500;
5.844179px calc(100% - 1.039291px), // flex-shrink: 0;
4.793324px calc(100% - 1.542842px), // }
3.811369px calc(100% - 2.169728px),
2.926417px calc(100% - 2.926417px), // span {
2.169728px calc(100% - 3.811369px), // font-family: CommitMono;
1.542842px calc(100% - 4.793324px), // color: $text;
1.039291px calc(100% - 5.844179px), // text-overflow: ellipsis;
0.662609px calc(100% - 6.947448px), // overflow: hidden;
0.382298px calc(100% - 8.079275px), // white-space: nowrap;
0.199291px calc(100% - 9.231074px), // margin-left: 32px;
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, // .clothoid-corner {
0.025635px 11.556933px, // clip-path: polygon(
0.085062px 10.391895px, // 13.890123px 0px,
0.199291px 9.231074px, // calc(100% - 13.890123px) 0px,
0.382298px 8.079275px, // calc(100% - 12.723414px) 0.004211px,
0.662609px 6.947448px, // calc(100% - 11.556933px) 0.025635px,
1.039291px 5.844179px, // calc(100% - 10.391895px) 0.085062px,
1.542842px 4.793324px, // calc(100% - 9.231074px) 0.199291px,
2.169728px 3.811369px, // calc(100% - 8.079275px) 0.382298px,
2.926417px 2.926417px, // calc(100% - 6.947448px) 0.662609px,
3.811369px 2.169728px, // calc(100% - 5.844179px) 1.039291px,
4.793324px 1.542842px, // calc(100% - 4.793324px) 1.542842px,
5.844179px 1.039291px, // calc(100% - 3.811369px) 2.169728px,
6.947448px 0.662609px, // calc(100% - 2.926417px) 2.926417px,
8.079275px 0.382298px, // calc(100% - 2.169728px) 3.811369px,
9.231074px 0.199291px, // calc(100% - 1.542842px) 4.793324px,
10.391895px 0.085062px, // calc(100% - 1.039291px) 5.844179px,
11.556933px 0.025635px, // calc(100% - 0.662609px) 6.947448px,
12.723414px 0.004211px, // calc(100% - 0.382298px) 8.079275px,
13.890123px 0px // 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> <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 <input
ref="searchInput" ref="searchInput"
v-model="searchQuery" v-model="searchQuery"
@ -10,6 +46,130 @@
class="search" class="search"
type="text" type="text"
placeholder="Type to filter entries..." /> placeholder="Type to filter entries..." />
<div class="main-container">
<OverlayScrollbarsComponent
class="results"
ref="resultsContainer"
:options="{ scrollbars: { autoHide: 'scroll' } }">
<template v-for="(group, groupIndex) in groupedHistory" :key="groupIndex">
<div class="time-separator">{{ group.label }}</div>
<div
v-for="(item, index) in group.items"
:key="item.id"
:class="[
'result clothoid-corner',
{ selected: isSelected(groupIndex, index) },
]"
@click="selectItem(groupIndex, index)"
:ref="
(el: any) => {
if (isSelected(groupIndex, index))
selectedElement = el as HTMLElement;
}
">
<template v-if="item.content_type === 'image'">
<img
v-if="imageUrls[item.id]"
:src="imageUrls[item.id]"
alt="Image"
class="image"
@error="onImageError" />
<img v-else src="../public/icons/Image.svg" class="icon" />
</template>
<template v-else-if="hasFavicon(item.favicon ?? '')">
<img
:src="
item.favicon
? getFaviconFromDb(item.favicon)
: '../public/icons/Link.svg'
"
alt="Favicon"
class="favicon"
@error="
($event.target as HTMLImageElement).src =
'../public/icons/Link.svg'
" />
</template>
<img
src="../public/icons/File.svg"
class="icon"
v-else-if="item.content_type === ContentType.File" />
<img
src="../public/icons/Text.svg"
class="icon"
v-else-if="item.content_type === ContentType.Text" />
<div v-else-if="item.content_type === ContentType.Color">
<svg
width="18"
height="18"
viewBox="0 0 18 18"
fill="none"
xmlns="http://www.w3.org/2000/svg">
<g>
<rect width="18" height="18" />
<path
d="M9 18C12.2154 18 15.1865 16.2846 16.7942 13.5C18.4019 10.7154 18.4019 7.28461 16.7942 4.5C15.1865 1.71539 12.2154 -1.22615e-06 9 0C5.78461 0 2.81347 1.71539 1.20577 4.5C-0.401925 7.28461 -0.401923 10.7154 1.20577 13.5C2.81347 16.2846 5.78461 18 9 18Z"
fill="#E5DFD5" />
<path
d="M9 16C7.14348 16 5.36301 15.2625 4.05025 13.9497C2.7375 12.637 2 10.8565 2 9C2 7.14348 2.7375 5.36301 4.05025 4.05025C5.36301 2.7375 7.14348 2 9 2C10.8565 2 12.637 2.7375 13.9497 4.05025C15.2625 5.36301 16 7.14348 16 9C16 10.8565 15.2625 12.637 13.9497 13.9497C12.637 15.2625 10.8565 16 9 16Z"
:fill="item.content" />
</g>
</svg>
</div>
<img
src="../public/icons/Code.svg"
class="icon"
v-else-if="item.content_type === ContentType.Code" />
<span v-if="item.content_type === ContentType.Image">
Image ({{ imageDimensions[item.id] || "Loading..." }})
</span>
<span v-else>{{ truncateContent(item.content) }}</span>
</div>
</template>
</OverlayScrollbarsComponent>
<div class="right-panel">
<div
class="content"
v-if="selectedItem?.content_type === ContentType.Image">
<img :src="imageUrls[selectedItem.id]" alt="Image" class="image" />
</div>
<div
v-else-if="selectedItem && isYoutubeWatchUrl(selectedItem.content)"
class="content">
<img
class="image"
:src="getYoutubeThumbnail(selectedItem.content)"
alt="YouTube Thumbnail" />
</div>
<div
class="content"
v-else-if="
selectedItem?.content_type === ContentType.Link && pageOgImage
">
<img :src="pageOgImage" alt="Image" class="image" />
</div>
<OverlayScrollbarsComponent v-else class="content">
<span>{{ selectedItem?.content || "" }}</span>
</OverlayScrollbarsComponent>
<OverlayScrollbarsComponent
class="information"
:options="{ scrollbars: { autoHide: 'scroll' } }">
<div class="title">Information</div>
<div class="info-content" v-if="selectedItem && getInfo">
<div class="info-row" v-for="(row, index) in infoRows" :key="index">
<p class="label">{{ row.label }}</p>
<span :class="{ 'url-truncate': row.isUrl }" :data-text="row.value">
{{ row.value }}
</span>
</div>
</div>
</OverlayScrollbarsComponent>
</div>
</div>
<div class="bottom-bar"> <div class="bottom-bar">
<div class="left"> <div class="left">
<img class="logo" width="18px" src="../public/logo.png" alt="" /> <img class="logo" width="18px" src="../public/logo.png" alt="" />
@ -34,126 +194,8 @@
</div> </div>
</div> </div>
</div> </div>
<OverlayScrollbarsComponent
class="results"
ref="resultsContainer"
:options="{ scrollbars: { autoHide: 'scroll' } }">
<template v-for="(group, groupIndex) in groupedHistory" :key="groupIndex">
<div class="time-separator">{{ group.label }}</div>
<div
v-for="(item, index) in group.items"
:key="item.id"
:class="[
'result clothoid-corner',
{ selected: isSelected(groupIndex, index) },
]"
@click="selectItem(groupIndex, index)"
:ref="
(el: any) => {
if (isSelected(groupIndex, index))
selectedElement = el as HTMLElement;
}
">
<template v-if="item.content_type === 'image'">
<img
v-if="imageUrls[item.id]"
:src="imageUrls[item.id]"
alt="Image"
class="image"
@error="onImageError" />
<img v-else src="../public/icons/Image.svg" class="icon" />
</template>
<template v-else-if="hasFavicon(item.favicon ?? '')">
<img
:src="
item.favicon
? getFaviconFromDb(item.favicon)
: '../public/icons/Link.svg'
"
alt="Favicon"
class="favicon"
@error="
($event.target as HTMLImageElement).src =
'../public/icons/Link.svg'
" />
</template>
<img
src="../public/icons/File.svg"
class="icon"
v-else-if="item.content_type === ContentType.File" />
<img
src="../public/icons/Text.svg"
class="icon"
v-else-if="item.content_type === ContentType.Text" />
<div v-else-if="item.content_type === ContentType.Color">
<svg
width="18"
height="18"
viewBox="0 0 18 18"
fill="none"
xmlns="http://www.w3.org/2000/svg">
<g>
<rect width="18" height="18" />
<path
d="M9 18C12.2154 18 15.1865 16.2846 16.7942 13.5C18.4019 10.7154 18.4019 7.28461 16.7942 4.5C15.1865 1.71539 12.2154 -1.22615e-06 9 0C5.78461 0 2.81347 1.71539 1.20577 4.5C-0.401925 7.28461 -0.401923 10.7154 1.20577 13.5C2.81347 16.2846 5.78461 18 9 18Z"
fill="#E5DFD5" />
<path
d="M9 16C7.14348 16 5.36301 15.2625 4.05025 13.9497C2.7375 12.637 2 10.8565 2 9C2 7.14348 2.7375 5.36301 4.05025 4.05025C5.36301 2.7375 7.14348 2 9 2C10.8565 2 12.637 2.7375 13.9497 4.05025C15.2625 5.36301 16 7.14348 16 9C16 10.8565 15.2625 12.637 13.9497 13.9497C12.637 15.2625 10.8565 16 9 16Z"
:fill="item.content" />
</g>
</svg>
</div>
<img
src="../public/icons/Code.svg"
class="icon"
v-else-if="item.content_type === ContentType.Code" />
<span v-if="item.content_type === ContentType.Image">
Image ({{ imageDimensions[item.id] || "Loading..." }})
</span>
<span v-else>{{ truncateContent(item.content) }}</span>
</div>
</template>
</OverlayScrollbarsComponent>
<div
class="content"
v-if="selectedItem?.content_type === ContentType.Image">
<img :src="imageUrls[selectedItem.id]" alt="Image" class="image" />
</div>
<div
v-else-if="selectedItem && isYoutubeWatchUrl(selectedItem.content)"
class="content">
<img
class="image"
:src="getYoutubeThumbnail(selectedItem.content)"
alt="YouTube Thumbnail" />
</div>
<div
class="content"
v-else-if="
selectedItem?.content_type === ContentType.Link && pageOgImage
">
<img :src="pageOgImage" alt="Image" class="image" />
</div>
<OverlayScrollbarsComponent v-else class="content">
<span>{{ selectedItem?.content || "" }}</span>
</OverlayScrollbarsComponent>
<OverlayScrollbarsComponent
class="information"
:options="{ scrollbars: { autoHide: 'scroll' } }">
<div class="title">Information</div>
<div class="info-content" v-if="selectedItem && getInfo">
<div class="info-row" v-for="(row, index) in infoRows" :key="index">
<p class="label">{{ row.label }}</p>
<span :class="{ 'url-truncate': row.isUrl }" :data-text="row.value">
{{ row.value }}
</span>
</div>
</div>
</OverlayScrollbarsComponent>
<Noise /> <Noise />
</div> </div> -->
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@ -175,6 +217,7 @@ import type {
InfoCode, InfoCode,
} from "~/types/types"; } from "~/types/types";
import { Key, keyboard } from "wrdu-keyboard"; import { Key, keyboard } from "wrdu-keyboard";
import { selectedGroupIndex, selectedItemIndex, selectedElement, useSelectedResult } from '~/lib/selectedResult'
interface GroupedHistory { interface GroupedHistory {
label: string; label: string;
@ -182,8 +225,11 @@ interface GroupedHistory {
} }
const { $history } = useNuxtApp(); const { $history } = useNuxtApp();
const CHUNK_SIZE = 50; const CHUNK_SIZE = 50;
const SCROLL_THRESHOLD = 100; const SCROLL_THRESHOLD = 100;
const SCROLL_PADDING = 8;
const TOP_SCROLL_PADDING = 37;
const history = shallowRef<HistoryItem[]>([]); const history = shallowRef<HistoryItem[]>([]);
let offset = 0; let offset = 0;
@ -193,9 +239,6 @@ const resultsContainer = shallowRef<InstanceType<
typeof OverlayScrollbarsComponent typeof OverlayScrollbarsComponent
> | null>(null); > | null>(null);
const searchQuery = ref(""); const searchQuery = ref("");
const selectedGroupIndex = ref(0);
const selectedItemIndex = ref(0);
const selectedElement = shallowRef<HTMLElement | null>(null);
const searchInput = ref<HTMLInputElement | null>(null); const searchInput = ref<HTMLInputElement | null>(null);
const os = ref<string>(""); const os = ref<string>("");
const imageUrls = shallowRef<Record<string, string>>({}); const imageUrls = shallowRef<Record<string, string>>({});
@ -207,6 +250,8 @@ const imageLoading = ref<boolean>(false);
const pageTitle = ref<string>(""); const pageTitle = ref<string>("");
const pageOgImage = ref<string>(""); const pageOgImage = ref<string>("");
const topBar = ref<{ searchInput: HTMLInputElement | null } | null>(null)
const isSameDay = (date1: Date, date2: Date): boolean => { const isSameDay = (date1: Date, date2: Date): boolean => {
return ( return (
date1.getFullYear() === date2.getFullYear() && date1.getFullYear() === date2.getFullYear() &&
@ -268,10 +313,7 @@ const groupedHistory = computed<GroupedHistory[]>(() => {
.map(([label, items]) => ({ label, items })); .map(([label, items]) => ({ label, items }));
}); });
const selectedItem = computed<HistoryItem | null>(() => { const { selectedItem, isSelected, selectNext, selectPrevious, selectItem } = useSelectedResult(groupedHistory)
const group = groupedHistory.value[selectedGroupIndex.value];
return group?.items[selectedItemIndex.value] ?? null;
});
const loadHistoryChunk = async (): Promise<void> => { const loadHistoryChunk = async (): Promise<void> => {
if (isLoading) return; if (isLoading) return;
@ -352,81 +394,64 @@ const handleScroll = (): void => {
} }
}; };
const scrollToSelectedItem = (forceScrollTop: boolean = false): void => { const scrollToSelectedItem = (): void => {
nextTick(() => { nextTick(() => {
const osInstance = resultsContainer.value?.osInstance(); const viewport = resultsContainer.value?.osInstance()?.elements().viewport;
const viewport = osInstance?.elements().viewport;
if (!selectedElement.value || !viewport) return; if (!selectedElement.value || !viewport) return;
if (!forceScrollTop) { setTimeout(() => {
if (!selectedElement.value) return;
const viewportRect = viewport.getBoundingClientRect(); const viewportRect = viewport.getBoundingClientRect();
const elementRect = selectedElement.value.getBoundingClientRect(); const elementRect = selectedElement.value.getBoundingClientRect();
const isAbove = elementRect.top < viewportRect.top; const isFirstItemInGroup = selectedItemIndex.value === 0;
const isBelow = elementRect.bottom > viewportRect.bottom - 8; const isAbove = elementRect.top < viewportRect.top + SCROLL_PADDING;
const isBelow = elementRect.bottom > viewportRect.bottom - SCROLL_PADDING;
if (isAbove || isBelow) { if (isAbove) {
const scrollOffset = isAbove viewport.scrollTo({
? elementRect.top - top: viewport.scrollTop + (elementRect.top - viewportRect.top) - (isFirstItemInGroup ? TOP_SCROLL_PADDING : SCROLL_PADDING),
viewportRect.top - behavior: "smooth"
(selectedItemIndex.value === 0 ? 36 : 8) });
: elementRect.bottom - viewportRect.bottom + 9; } else if (isBelow) {
viewport.scrollTo({
viewport.scrollBy({ top: scrollOffset, behavior: "smooth" }); top: viewport.scrollTop + (elementRect.bottom - viewportRect.bottom) + SCROLL_PADDING,
behavior: "smooth"
});
} }
} }, 10);
}); });
}; };
const isSelected = (groupIndex: number, itemIndex: number): boolean => { const searchHistory = async (query: string): Promise<void> => {
return ( searchQuery.value = query
selectedGroupIndex.value === groupIndex && if (!query.trim()) {
selectedItemIndex.value === itemIndex 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) => history.value = results.map((item) =>
Object.assign( Object.assign(
new HistoryItem( new HistoryItem(
item.source, item.source,
item.content_type, item.content_type,
item.content, item.content,
item.favicon item.favicon,
item.source_icon,
item.language
), ),
{ id: item.id, timestamp: new Date(item.timestamp) } { id: item.id, timestamp: new Date(item.timestamp) }
) )
); )
};
const selectNext = (): void => { if (groupedHistory.value.length > 0) {
const currentGroup = groupedHistory.value[selectedGroupIndex.value]; handleSelection(0, 0, false)
if (selectedItemIndex.value < currentGroup.items.length - 1) {
selectedItemIndex.value++;
} else if (selectedGroupIndex.value < groupedHistory.value.length - 1) {
selectedGroupIndex.value++;
selectedItemIndex.value = 0;
} }
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> => { const pasteSelectedItem = async (): Promise<void> => {
if (!selectedItem.value) return; if (!selectedItem.value) return;
@ -554,10 +579,9 @@ const handleSelection = (
itemIndex: number, itemIndex: number,
shouldScroll: boolean = true shouldScroll: boolean = true
): void => { ): void => {
selectedGroupIndex.value = groupIndex; selectItem(groupIndex, itemIndex)
selectedItemIndex.value = itemIndex; if (shouldScroll) scrollToSelectedItem()
if (shouldScroll) scrollToSelectedItem(); }
};
const setupEventListeners = async (): Promise<void> => { const setupEventListeners = async (): Promise<void> => {
await listen("clipboard-content-updated", async () => { await listen("clipboard-content-updated", async () => {
@ -591,8 +615,7 @@ const setupEventListeners = async (): Promise<void> => {
} }
} }
focusSearchInput(); focusSearchInput();
// Re-register keyboard shortcuts on focus
keyboard.clear(); keyboard.clear();
keyboard.prevent.down([Key.DownArrow], () => { keyboard.prevent.down([Key.DownArrow], () => {
selectNext(); selectNext();
@ -666,9 +689,9 @@ const hideApp = async (): Promise<void> => {
const focusSearchInput = (): void => { const focusSearchInput = (): void => {
nextTick(() => { nextTick(() => {
searchInput.value?.focus(); topBar.value?.searchInput?.focus()
}); })
}; }
const onImageError = (): void => { const onImageError = (): void => {
imageLoadError.value = true; imageLoadError.value = true;
@ -677,10 +700,10 @@ const onImageError = (): void => {
watch([selectedGroupIndex, selectedItemIndex], () => { watch([selectedGroupIndex, selectedItemIndex], () => {
scrollToSelectedItem(); scrollToSelectedItem();
}); }, { flush: 'post' });
watch(searchQuery, () => { watch(searchQuery, () => {
searchHistory(); searchHistory(searchQuery.value);
}); });
onMounted(async () => { onMounted(async () => {
@ -699,10 +722,6 @@ onMounted(async () => {
} }
}); });
watch([selectedGroupIndex, selectedItemIndex], () =>
scrollToSelectedItem(false)
);
const getFormattedDate = computed(() => { const getFormattedDate = computed(() => {
if (!selectedItem.value?.timestamp) return ""; if (!selectedItem.value?.timestamp) return "";
return new Intl.DateTimeFormat("en-US", { return new Intl.DateTimeFormat("en-US", {