Restructure files

This commit is contained in:
aet
2021-11-11 23:56:06 -05:00
parent b4b21561ed
commit bdd46a530c
29 changed files with 271 additions and 179 deletions

View File

@ -1,6 +1,7 @@
import styled from "@emotion/styled"
import type { Attachment, AttachmentMetadata, Item, ItemField } from "opvault.js"
import { useEffect, useState } from "react"
import type { ItemDetails } from "opvault.js/src/types"
import { memo, useEffect, useState } from "react"
import { useTranslate } from "../i18n"
import { CategoryIcon } from "./CategoryIcon"
import { ItemDates } from "./ItemDates"
@ -10,6 +11,7 @@ import {
FieldTitle,
ItemDetailsFieldView,
} from "./ItemField"
import { PasswordFieldView } from "./ItemFieldValue"
import { ItemWarning } from "./ItemWarning"
interface ItemViewProps {
@ -57,6 +59,22 @@ const AttachmentContainer = styled.div`
margin: 5px 0;
`
const SectionsView: React.FC<{ sections?: ItemDetails["sections"] }> = ({ sections }) =>
sections?.length ? (
<div style={{ marginBottom: 20 }}>
{sections
.filter(s => s.fields?.some(x => x.v != null))
.map((section, i) => (
<div key={i}>
{section.title != null && <SectionTitle>{section.title}</SectionTitle>}
{section.fields?.map((field, j) => (
<ItemFieldView key={j} field={field} />
))}
</div>
))}
</div>
) : null
const FieldsView: React.FC<{ fields?: ItemField[] }> = ({ fields }) =>
fields?.length ? (
<div style={{ marginBottom: 20 }}>
@ -71,7 +89,7 @@ const TagsView: React.FC<{ tags?: string[] }> = ({ tags }) => {
if (!tags?.length) return null
return (
<ExtraField>
<FieldTitle>{t.noun_tags}</FieldTitle>
<FieldTitle>{t.noun.tags}</FieldTitle>
<div>
{tags.map((tag, i) => (
<Tag key={i}>{tag}</Tag>
@ -81,65 +99,69 @@ const TagsView: React.FC<{ tags?: string[] }> = ({ tags }) => {
)
}
export const ItemView: React.FC<ItemViewProps> = ({ className, item }) => (
<Container className={className}>
<Inner>
<ItemWarning item={item} />
<Header>
{item.details.fields == null}
<Icon category={item.category} />
<ItemTitle>{item.overview.title}</ItemTitle>
</Header>
<details>
<summary>JSON</summary>
<pre>
{JSON.stringify({ overview: item.overview, details: item.details }, null, 2)}
</pre>
</details>
const JSONView = memo<{ item: Item }>(({ item }) => (
<details>
<summary>JSON</summary>
<pre>
{JSON.stringify({ overview: item.overview, details: item.details }, null, 2)}
</pre>
</details>
))
<div style={{ marginBottom: 20 }}>
{item.details.sections
?.filter(s => s.fields?.some(x => x.v != null))
.map((section, i) => (
<div key={i}>
{section.title != null && <SectionTitle>{section.title}</SectionTitle>}
{section.fields?.map((field, j) => (
<ItemFieldView key={j} field={field} />
export const ItemView: React.FC<ItemViewProps> = ({ className, item }) => {
const t = useTranslate()
return (
<Container className={className}>
<Inner>
<ItemWarning item={item} />
<Header>
{item.details.fields == null}
<Icon category={item.category} />
<ItemTitle>{item.overview.title}</ItemTitle>
</Header>
<JSONView item={item} />
<div style={{ height: 10 }}></div>
<SectionsView sections={item.details.sections} />
<FieldsView fields={item.details.fields} />
{item.details.notesPlain != null && (
<ExtraField>
<FieldTitle>notes</FieldTitle>
<div>
<p>{item.details.notesPlain}</p>
</div>
</ExtraField>
)}
{item.details.password != null && (
<ExtraField>
<FieldTitle>{t.label.password}</FieldTitle>
<PasswordFieldView field={{ v: item.details.password }} />
</ExtraField>
)}
<TagsView tags={item.overview.tags} />
{item.attachments.length > 0 && (
<ExtraField>
<FieldTitle>attachments</FieldTitle>
<div>
{item.attachments.map((file, i) => (
<AttachmentView key={i} file={file} />
))}
</div>
))}
</div>
</ExtraField>
)}
<FieldsView fields={item.details.fields} />
{item.details.notesPlain != null && (
<ExtraField>
<FieldTitle>notes</FieldTitle>
<div>
<p>{item.details.notesPlain}</p>
</div>
<ItemDates item={item} />
</ExtraField>
)}
<TagsView tags={item.overview.tags} />
{item.attachments.length > 0 && (
<ExtraField>
<FieldTitle>attachments</FieldTitle>
<div>
{item.attachments.map((file, i) => (
<AttachmentView key={i} file={file} />
))}
</div>
</ExtraField>
)}
<ExtraField>
<ItemDates item={item} />
</ExtraField>
</Inner>
</Container>
)
</Inner>
</Container>
)
}
function AttachmentView({ file }: { file: Attachment }) {
const [metadata, setMetadata] = useState<AttachmentMetadata>()

View File

@ -14,10 +14,10 @@ export const ItemDates: React.FC<{ item: Item }> = ({ item }) => {
return (
<Container>
<div>
{t.label_last_updated}: {new Date(item.updatedAt).toLocaleString()}
{t.label.last_updated}: {new Date(item.updatedAt).toLocaleString()}
</div>
<div>
{t.label_created_at}: {new Date(item.createdAt).toLocaleString()}
{t.label.created_at}: {new Date(item.createdAt).toLocaleString()}
</div>
</Container>
)

View File

@ -7,7 +7,7 @@ const Container = styled.menu`
box-shadow: #0004 0px 1px 4px;
left: 99%;
margin-block-start: 0;
min-width: 120px;
min-width: 150px;
padding-inline-start: 0;
position: absolute;
top: 0;
@ -39,6 +39,7 @@ const Item = styled.div`
height: 2.5em;
align-items: center;
padding-left: 1em;
padding-right: 5px;
position: relative;
&:hover {
background-color: #ddd;

View File

@ -9,8 +9,10 @@ import { useItemFieldContextMenu } from "./ItemFieldContextMenu"
const Container = styled.div``
export { Password as PasswordFieldView }
const Password: React.FC<{
field: ItemSection.Concealed
field: Pick<ItemSection.Concealed, "v">
}> = ({ field }) => {
const [show, setShow] = useState(false)
const [bigText, showBigText] = useState(false)

View File

@ -1,7 +1,7 @@
import { createContext, memo, useContext, useEffect, useMemo, useState } from "react"
import texts from "./texts.yml"
type Keys = keyof typeof texts
const categories = Object.keys(texts)
const ALLOWED = new Set(["en", "fr"])
const LOCALSTORAGE_KEY = "preferred-locale"
@ -40,21 +40,29 @@ export function useTranslate() {
const { locale } = useContext(LocaleContext)
const t = useMemo(
() =>
new Proxy(
{},
{
get(_, p: string) {
if (
process.env.NODE_ENV === "development" &&
!Object.prototype.hasOwnProperty.call(texts, p)
) {
throw new Error(`t.${p} does not exist.`)
Object.fromEntries(
categories.map(category => [
category,
new Proxy(
{},
{
get(_, p: string) {
const obj = (texts as any)[category]
if (
process.env.NODE_ENV === "development" &&
!Object.prototype.hasOwnProperty.call(obj, p)
) {
throw new Error(`t.${p} does not exist.`)
}
return obj[p][locale]
},
}
return (texts as any)[p][locale]
},
}
),
])
) as {
[key in Keys]: string
[category in keyof typeof texts]: {
[key in keyof typeof texts[category]]: string
}
},
[locale]
)

View File

@ -1,40 +1,51 @@
# /* spellchecker: disable */
label_choose_a_vault:
en: Pick a vault
fr: Choisir un coffre
label:
choose_a_vault:
en: Pick a vault
fr: Choisir un coffre
label_no_vault_selected:
en: No vault is selected.
fr: Aucun coffre nest sélectionné.
no_vault_selected:
en: No vault is selected.
fr: Aucun coffre nest sélectionné.
label_last_updated:
en: Last Updated
fr: Dernière modification
last_updated:
en: Last Updated
fr: Dernière modification
label_created_at:
en: Created At
fr: Créé
created_at:
en: Created At
fr: Créé
label_password_placeholder:
en: Master Password
fr: Mot de passe principal
password_placeholder:
en: Master Password
fr: Mot de passe principal
noun_vault:
en: vault
fr: coffre
username:
en: Username
fr: Nom dutilisateur
noun_tags:
en: tags
fr: mots-clés
password:
en: Password
fr: Mot de passe
action_lock:
en: Lock
fr: Vérouiller
noun:
vault:
en: vault
fr: coffre
action_unlock:
en: Unlock
fr: Déverouiller
tags:
en: tags
fr: mots-clés
action_go_back:
en: Back
fr: Revenir
action:
lock:
en: Lock
fr: Vérouiller
unlock:
en: Unlock
fr: Déverouiller
go_back:
en: Back
fr: Revenir

View File

@ -102,17 +102,17 @@ export const Unlock: React.FC<{
return (
<Container>
<div style={{ marginBottom: 10 }}>
<BackButton onClick={onReturn} title={t.action_go_back}>
<BackButton onClick={onReturn} title={t.action.go_back}>
<IoMdArrowRoundBack />
</BackButton>
<Select
title={t.noun_vault}
title={t.noun.vault}
value={profile}
onChange={e => setProfile(e.currentTarget.value)}
>
{profiles.map(p => (
<option key={p} value={p}>
{t.noun_vault}: {p}
{t.noun.vault}: {p}
</option>
))}
</Select>
@ -122,14 +122,14 @@ export const Unlock: React.FC<{
type="password"
value={password}
onChange={e => setPassword(e.currentTarget.value)}
placeholder={t.label_password_placeholder}
placeholder={t.label.password_placeholder}
onKeyUp={onKeyUp}
/>
<Submit
type="submit"
disabled={!profile || !password}
onClick={unlock}
title={t.action_unlock}
title={t.action.unlock}
>
<FaUnlock />
</Submit>

View File

@ -88,25 +88,36 @@ export const VaultView: React.FC<{ vault: Vault; onLock(): void }> = ({
arrayFrom(vault.values()).then(setItems)
}, [vault])
const filtered = useMemo(
() =>
sortedItem
.filter(x => x.category !== Category.Tombstone)
.filter(
search
? x =>
stringCompare(search, x.overview.title) ||
stringCompare(search, x.overview.ainfo)
: () => true
),
[sortedItem, search]
)
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>
<ListContainer className={scrollbar}>
<div style={{ margin: "10px 10px", display: "flex" }}>
<LockButton onClick={onLock} title={t.action_lock}>
<LockButton onClick={onLock} title={t.action.lock}>
<FiLock />
</LockButton>
<SearchContainer>
@ -147,8 +158,20 @@ async function arrayFrom<T>(generator: AsyncGenerator<T, void, unknown>) {
return list
}
function stringCompare(search: string, source?: string) {
if (!search) return true
if (!source) return false
return source.toLocaleLowerCase().includes(search.toLocaleLowerCase())
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
}

View File

@ -87,8 +87,8 @@ const PickOPVault: React.FC<{
return (
<PickOPVaultContainer>
<button onClick={onClick}>{t.label_choose_a_vault}</button>
<PickOPVaultInfo>{t.label_no_vault_selected}</PickOPVaultInfo>
<button onClick={onClick}>{t.label.choose_a_vault}</button>
<PickOPVaultInfo>{t.label.no_vault_selected}</PickOPVaultInfo>
</PickOPVaultContainer>
)
}