import styled from "@emotion/styled" import { useEffect, useMemo, useState } from "react" import type { Vault, Item } from "opvault.js" import { Category } from "opvault.js" import { FiLock } from "react-icons/fi" import { IoSearch } from "react-icons/io5" import { Si1Password } from "react-icons/si" import { BsGear } from "react-icons/bs" import { ItemList } from "../components/ItemList" import { ItemView } from "../components/Item" import { reactIconClass } from "../components/CategoryIcon" import { useTranslate } from "../i18n/index" import { scrollbar } from "../styles" import { Settings } from "../settings" const Container = styled.div` display: flex; height: calc(100vh - var(--titlebar-height)); ` const TabContainer = styled.div` border-right: 1px solid var(--border-color); display: flex; flex-direction: column; width: 55px; overflow: hidden; padding-bottom: 5px; @media (prefers-color-scheme: dark) { background: #222; border-right-color: transparent; } &&::-webkit-scrollbar { display: none; } ` const TabButton = styled.button<{ active?: boolean }>` align-items: center; background: ${p => (p.active ? "var(--selected-background)" : "transparent")}; border-radius: ${p => (p.active ? 0 : 3)}px; border: transparent; box-shadow: none; display: inline-flex; margin-bottom: 5px; font-size: 22px; padding: 10px 14px; @media (prefers-color-scheme: dark) { --selected-background: #1c1c1c; } ` const TabContainerMain = styled.div` flex-grow: 1; ` const ListContainer = styled.div` border-right: 1px solid var(--border-color); width: 350px; margin-right: 10px; overflow-y: scroll; overflow-x: hidden; @media (prefers-color-scheme: dark) { background: #202020; border-right-color: transparent; } ` const ItemContainer = styled.div` width: calc(100% - 300px); overflow: hidden; ` const SearchContainer = styled.div` text-align: center; position: relative; flex-grow: 1; margin: 10px 0; margin-right: 10px; ` const SortContainer = styled.div` margin: 10px 10px; ` const SearchInput = styled.input` --margin: 10px; width: calc(100% - var(--margin) * 2 + 9px); margin: 0 var(--margin); padding-left: 2em !important; ` const SearchIcon = styled(IoSearch)` position: absolute; top: 9px; left: 20px; ` const enum SortBy { Name, CreatedAt, UpdatedAt, } export const VaultView: React.FC<{ vault: Vault; onLock(): void }> = ({ vault, onLock, }) => { const [showSettings, setShowSettings] = useState(false) const t = useTranslate() const [items, setItems] = useState(() => []) const [item, setItem] = useState() const [sortBy, setSortBy] = useState(SortBy.Name) const [search, setSearch] = useState("") const compareFn = useMemo((): ((a: Item, b: Item) => number) => { switch (sortBy) { case SortBy.Name: return (a, b) => (a.overview.title ?? "").localeCompare(b?.overview.title ?? "") case SortBy.CreatedAt: return (a, b) => b.createdAt - a.createdAt case SortBy.UpdatedAt: return (a, b) => b.updatedAt - a.updatedAt } }, [sortBy]) const sortedItem = useMemo(() => items.slice().sort(compareFn), [items, compareFn]) useEffect(() => { setItem(undefined) arrayFrom(vault.values()).then(setItems) }, [vault]) const filtered = useMemo(() => { const items = sortedItem.filter(x => x.category !== Category.Tombstone) let res: Item[] = items if (search) { res = [] for (const x of items) { const compare = Math.max( stringCompare(search, x.overview.title), stringCompare(search, x.overview.ainfo) ) as CompareResult switch (compare) { case CompareResult.NoMatch: continue case CompareResult.Includes: res.push(x) break case CompareResult.Equals: res.unshift(x) break } } } return res }, [sortedItem, search]) return ( setShowSettings(true)} title={t.label.settings}> setSearch(e.currentTarget.value)} /> {item && } setShowSettings(false)} /> ) } async function arrayFrom(generator: AsyncGenerator) { const list: T[] = [] for await (const value of generator) { list.push(value) } return list } enum CompareResult { NoMatch, Includes, Equals, } function stringCompare(search: string, source?: string) { if (!search) return CompareResult.Includes if (!source) return CompareResult.NoMatch source = source.toLocaleLowerCase() search = search.toLocaleUpperCase() const includes = source.includes(search.toLocaleLowerCase()) if (includes) { return source.length === search.length ? CompareResult.Equals : CompareResult.Includes } return CompareResult.NoMatch }