Add About screen, list of recently opened vaults, category filtering

This commit is contained in:
aet
2022-01-02 00:53:57 -05:00
parent 5883adc2c1
commit d8f2cddb74
27 changed files with 1108 additions and 558 deletions

View File

@ -1,17 +1,13 @@
import styled from "@emotion/styled"
import { useEffect, useMemo, useState } from "react"
import type { Vault, Item } from "opvault.js"
import { Category } from "opvault.js"
import { AiOutlineStar } from "react-icons/ai"
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"
import { FilteredVaultView } from "../components/FilteredVaultView"
const Container = styled.div`
display: flex;
@ -21,9 +17,9 @@ const TabContainer = styled.div`
border-right: 1px solid var(--border-color);
display: flex;
flex-direction: column;
width: 55px;
overflow: hidden;
padding-bottom: 5px;
width: 54px;
@media (prefers-color-scheme: dark) {
background: #222;
border-right-color: transparent;
@ -42,6 +38,7 @@ const TabButton = styled.button<{ active?: boolean }>`
margin-bottom: 5px;
font-size: 22px;
padding: 10px 14px;
${p => p.active && "&:hover { background: var(--selected-background); }"}
@media (prefers-color-scheme: dark) {
--selected-background: #1c1c1c;
}
@ -49,111 +46,30 @@ const TabButton = styled.button<{ active?: boolean }>`
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 [tab, setTab] = useState(Tab.All)
const [items, setItems] = useState<Item[]>(() => [])
const [showSettings, setShowSettings] = useState(false)
const t = useTranslate()
const [items, setItems] = useState<Item[]>(() => [])
const [item, setItem] = useState<Item>()
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 (
<Container>
<TabContainer>
<TabContainerMain>
<TabButton active>
<TabButton active={tab === Tab.All} onClick={() => setTab(Tab.All)}>
<Si1Password />
</TabButton>
<TabButton active={tab === Tab.Favorites} onClick={() => setTab(Tab.Favorites)}>
<AiOutlineStar />
</TabButton>
</TabContainerMain>
<TabButton onClick={onLock} title={t.action.lock}>
<FiLock />
@ -163,38 +79,25 @@ export const VaultView: React.FC<{ vault: Vault; onLock(): void }> = ({
</TabButton>
</TabContainer>
<ListContainer className={scrollbar}>
<SearchContainer>
<SearchInput
type="search"
value={search}
onChange={e => setSearch(e.currentTarget.value)}
/>
<SearchIcon className={reactIconClass} />
</SearchContainer>
<SortContainer>
<select
style={{ width: "100%" }}
value={sortBy}
onChange={e => setSortBy(+e.currentTarget.value)}
>
<option value={SortBy.Name}>{t.options.sort_by_name}</option>
<option value={SortBy.CreatedAt}>{t.options.sort_by_created_at}</option>
<option value={SortBy.UpdatedAt}>{t.options.sort_by_updated_at}</option>
</select>
</SortContainer>
<ItemList items={filtered} onSelect={setItem} selected={item} />
</ListContainer>
<ItemContainer>
{item && <ItemView className={scrollbar} item={item} />}
</ItemContainer>
{tab === Tab.All ? (
<FilteredVaultView items={items} />
) : tab === Tab.Favorites ? (
<FavoriteItemsView items={items} />
) : null}
<Settings show={showSettings} onHide={() => setShowSettings(false)} />
</Container>
)
}
const FavoriteItemsView: React.FC<{ items: Item[] }> = ({ items }) => {
const favorites = useMemo(
() => items.filter(x => x.fave).sort((a, b) => a.fave - b.fave),
[items]
)
return <FilteredVaultView items={favorites} />
}
async function arrayFrom<T>(generator: AsyncGenerator<T, void, unknown>) {
const list: T[] = []
for await (const value of generator) {
@ -203,20 +106,7 @@ async function arrayFrom<T>(generator: AsyncGenerator<T, void, unknown>) {
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
enum Tab {
All,
Favorites,
}