Restructure files
This commit is contained in:
@ -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>()
|
||||
|
@ -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>
|
||||
)
|
||||
|
@ -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;
|
||||
|
@ -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)
|
||||
|
@ -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]
|
||||
)
|
||||
|
@ -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 n’est sélectionné.
|
||||
no_vault_selected:
|
||||
en: No vault is selected.
|
||||
fr: Aucun coffre n’est 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 d’utilisateur
|
||||
|
||||
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
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
Reference in New Issue
Block a user