General improvements and bug fixes

This commit is contained in:
aet 2021-11-23 03:13:01 -05:00
parent bdd46a530c
commit 8f9ec73caf
26 changed files with 908 additions and 2122 deletions

View File

@ -14,35 +14,35 @@
"@types/chai-as-promised": "^7.1.4",
"@types/fs-extra": "^9.0.13",
"@types/mocha": "github:whitecolor/mocha-types#da22474cf43f48a56c86f8c23a5a0ea36e295768",
"@types/node": "^16.10.3",
"@types/sinon": "^10.0.4",
"@types/node": "^16.11.9",
"@types/sinon": "^10.0.6",
"@types/sinon-chai": "^3.2.5",
"@types/wicg-file-system-access": "^2020.9.4",
"@typescript-eslint/eslint-plugin": "4.33.0",
"@typescript-eslint/parser": "4.33.0",
"@typescript-eslint/eslint-plugin": "5.4.0",
"@typescript-eslint/parser": "5.4.0",
"chai": "^4.3.4",
"chai-as-promised": "^7.1.1",
"chalk": "^4.1.2",
"eslint": "7.32.0",
"eslint": "8.3.0",
"eslint-config-prettier": "8.3.0",
"eslint-import-resolver-typescript": "2.5.0",
"eslint-plugin-import": "2.24.2",
"eslint-plugin-react": "7.26.1",
"eslint-plugin-react-hooks": "4.2.0",
"eslint-plugin-import": "2.25.3",
"eslint-plugin-react": "7.27.1",
"eslint-plugin-react-hooks": "4.3.0",
"fs-extra": "^10.0.0",
"marked": "^3.0.8",
"mocha": "^9.1.2",
"mochawesome": "^6.3.0",
"marked": "^4.0.4",
"mocha": "^9.1.3",
"mochawesome": "^7.0.1",
"prettier": "^2.4.1",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"sass": "^1.43.2",
"sinon": "^11.1.2",
"sass": "^1.43.4",
"sinon": "^12.0.1",
"sinon-chai": "^3.7.0",
"tslib": "^2.3.1",
"ts-node": "^10.2.1",
"tsconfig-paths": "^3.11.0",
"typescript": "^4.4.3"
"ts-node": "^10.4.0",
"tsconfig-paths": "^3.12.0",
"typescript": "^4.5.2"
},
"prettier": {
"arrowParens": "avoid",

View File

@ -16,8 +16,8 @@
"@rollup/plugin-json": "^4.1.0",
"@rollup/plugin-replace": "^3.0.0",
"prettier": "^2.4.1",
"rollup": "^2.58.0",
"rollup-plugin-ts": "^1.4.7",
"typedoc": "^0.22.7"
"rollup": "^2.60.1",
"rollup-plugin-ts": "^2.0.4",
"typedoc": "^0.22.9"
}
}

View File

@ -21,6 +21,8 @@ export default () => ({
preventAssignment: true,
values: {
"process.env.NODE_ENV": '"production"',
'require("./adapter").nodeAdapter':
'import("./adapter").then(x => x.nodeAdapter)',
},
}),
],

View File

@ -30,7 +30,7 @@ export class OnePassword {
constructor({
path,
adapter = process.browser ? null! : import("./adapter").then(x => x.nodeAdapter),
adapter = process.browser ? null! : require("./adapter").nodeAdapter,
}: IOptions) {
this.#adapter = adapter
this.#path = path

View File

@ -8,7 +8,7 @@ icon: dist/512x512.png
directories:
output: bundle
app: dist
buildResources: build
buildResources: dist
mac:
category: public.app-category.productivity
target:

View File

@ -14,24 +14,25 @@
},
"devDependencies": {
"@emotion/css": "^11.5.0",
"@emotion/react": "^11.5.0",
"@emotion/styled": "^11.3.0",
"@emotion/react": "^11.6.0",
"@emotion/styled": "^11.6.0",
"@rollup/plugin-yaml": "^3.1.0",
"@types/react": "^17.0.0",
"@types/react-dom": "^17.0.0",
"@vitejs/plugin-react": "^1.0.0",
"@types/react": "^17.0.36",
"@types/react-dom": "^17.0.11",
"@vitejs/plugin-react": "^1.1.0",
"buffer": "^6.0.3",
"electron": "^15.2.0",
"electron-builder": "^22.13.1",
"esbuild": "^0.13.6",
"electron": "^16.0.1",
"electron-builder": "^22.14.5",
"esbuild": "^0.13.15",
"js-yaml": "^4.1.0",
"opvault.js": "*",
"path-browserify": "^1.0.1",
"react": "^17.0.0",
"react-dom": "^17.0.0",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-idle-timer": "4.6.4",
"react-icons": "^4.3.1",
"sass": "^1.43.4",
"typescript": "^4.3.2",
"vite": "^2.6.4"
"typescript": "^4.5.2",
"vite": "^2.6.14"
}
}

View File

@ -1,5 +1,6 @@
import { useCallback, useState } from "react"
import { useCallback, useEffect, useState } from "react"
import type { Vault, OnePassword } from "opvault.js"
import { useIdleTimer } from "react-idle-timer/modern"
import { VaultView } from "./pages/Vault"
import { VaultPicker } from "./pages/VaultPicker"
@ -12,6 +13,19 @@ export const App: React.FC = () => {
setVault(undefined)
}, [vault])
const { reset, pause } = useIdleTimer({
timeout: 60_000,
onIdle: onLock,
})
useEffect(() => {
if (vault) {
reset()
} else {
pause()
}
}, [vault])
if (!vault) {
return (
<VaultPicker

View File

@ -0,0 +1,14 @@
import { useEffect, memo } from "react"
import { useLocaleContext, useTranslate } from "./i18n"
export const SideEffect = memo(() => {
const { locale } = useLocaleContext()
const t = useTranslate()
useEffect(() => {
document.documentElement.lang = locale
document.title = t.label.app_name
}, [locale])
return null
})

View File

@ -11,10 +11,12 @@ const Container = styled.div`
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 8em;
font-size: 6em;
text-align: center;
padding: 20px 25px;
word-break: break-word;
min-width: 75vw;
z-index: 2;
@media (prefers-color-scheme: dark) {
background: rgba(0, 0, 0, 0.6);
}

View File

@ -1,3 +1,4 @@
import { memo } from "react"
import { Category } from "opvault.js"
import { cx, css } from "@emotion/css"
import { BsBank2, BsPeopleFill } from "react-icons/bs"
@ -77,14 +78,11 @@ interface CategoryIconProps {
category: Category
}
export const CategoryIcon: React.FC<CategoryIconProps> = ({
className,
category,
style,
fill,
}) => {
export const CategoryIcon = memo<CategoryIconProps>(
({ className, category, style, fill }) => {
const Component = getComponent(category)
return Component ? (
<Component className={cx(reactIconClass, className)} fill={fill} style={style} />
) : null
}
}
)

View File

@ -3,6 +3,7 @@ import type { Attachment, AttachmentMetadata, Item, ItemField } from "opvault.js
import type { ItemDetails } from "opvault.js/src/types"
import { memo, useEffect, useState } from "react"
import { useTranslate } from "../i18n"
import { ItemNoTitle } from "../styles"
import { CategoryIcon } from "./CategoryIcon"
import { ItemDates } from "./ItemDates"
import {
@ -59,7 +60,7 @@ const AttachmentContainer = styled.div`
margin: 5px 0;
`
const SectionsView: React.FC<{ sections?: ItemDetails["sections"] }> = ({ sections }) =>
const SectionsView = memo<{ sections?: ItemDetails["sections"] }>(({ sections }) =>
sections?.length ? (
<div style={{ marginBottom: 20 }}>
{sections
@ -74,8 +75,9 @@ const SectionsView: React.FC<{ sections?: ItemDetails["sections"] }> = ({ sectio
))}
</div>
) : null
)
const FieldsView: React.FC<{ fields?: ItemField[] }> = ({ fields }) =>
const FieldsView = memo<{ fields?: ItemField[] }>(({ fields }) =>
fields?.length ? (
<div style={{ marginBottom: 20 }}>
{fields.map((field, i) => (
@ -83,8 +85,9 @@ const FieldsView: React.FC<{ fields?: ItemField[] }> = ({ fields }) =>
))}
</div>
) : null
)
const TagsView: React.FC<{ tags?: string[] }> = ({ tags }) => {
const TagsView = memo<{ tags?: string[] }>(({ tags }) => {
const t = useTranslate()
if (!tags?.length) return null
return (
@ -97,7 +100,7 @@ const TagsView: React.FC<{ tags?: string[] }> = ({ tags }) => {
</div>
</ExtraField>
)
}
})
const JSONView = memo<{ item: Item }>(({ item }) => (
<details>
@ -108,7 +111,7 @@ const JSONView = memo<{ item: Item }>(({ item }) => (
</details>
))
export const ItemView: React.FC<ItemViewProps> = ({ className, item }) => {
export const ItemView = memo<ItemViewProps>(({ className, item }) => {
const t = useTranslate()
return (
<Container className={className}>
@ -117,7 +120,9 @@ export const ItemView: React.FC<ItemViewProps> = ({ className, item }) => {
<Header>
{item.details.fields == null}
<Icon category={item.category} />
<ItemTitle>{item.overview.title}</ItemTitle>
<ItemTitle>
{item.overview.title || <ItemNoTitle>{t.label.no_title}</ItemNoTitle>}
</ItemTitle>
</Header>
<JSONView item={item} />
@ -161,7 +166,7 @@ export const ItemView: React.FC<ItemViewProps> = ({ className, item }) => {
</Inner>
</Container>
)
}
})
function AttachmentView({ file }: { file: Attachment }) {
const [metadata, setMetadata] = useState<AttachmentMetadata>()

View File

@ -1,3 +1,4 @@
import { memo } from "react"
import styled from "@emotion/styled"
import type { Item } from "opvault.js"
import { useTranslate } from "../i18n"
@ -9,7 +10,7 @@ const Container = styled.div`
opacity: 0.5;
`
export const ItemDates: React.FC<{ item: Item }> = ({ item }) => {
export const ItemDates = memo<{ item: Item }>(({ item }) => {
const t = useTranslate()
return (
<Container>
@ -21,4 +22,4 @@ export const ItemDates: React.FC<{ item: Item }> = ({ item }) => {
</div>
</Container>
)
}
})

View File

@ -1,3 +1,4 @@
import { memo } from "react"
import styled from "@emotion/styled"
import type { ItemField, ItemSection } from "opvault.js"
import { ErrorBoundary } from "./ErrorBoundary"
@ -13,9 +14,9 @@ export const FieldTitle: React.FC = styled.div`
margin-bottom: 3px;
`
export const ItemFieldView: React.FC<{
export const ItemFieldView = memo<{
field: ItemSection.Any
}> = ({ field }) => {
}>(({ field }) => {
if (field.v == null) {
return null
}
@ -28,11 +29,11 @@ export const ItemFieldView: React.FC<{
</Container>
</ErrorBoundary>
)
}
})
export const ItemDetailsFieldView: React.FC<{
export const ItemDetailsFieldView = memo<{
field: ItemField
}> = ({ field }) => {
}>(({ field }) => {
if (field.value == null) {
return null
}
@ -45,4 +46,4 @@ export const ItemDetailsFieldView: React.FC<{
</Container>
</ErrorBoundary>
)
}
})

View File

@ -7,7 +7,7 @@ const Container = styled.menu`
box-shadow: #0004 0px 1px 4px;
left: 99%;
margin-block-start: 0;
min-width: 150px;
min-width: 180px;
padding-inline-start: 0;
position: absolute;
top: 0;

View File

@ -9,6 +9,12 @@ import { useItemFieldContextMenu } from "./ItemFieldContextMenu"
const Container = styled.div``
function useCopy(text: string) {
return useCallback(() => {
navigator.clipboard.writeText(text)
}, [text])
}
export { Password as PasswordFieldView }
const Password: React.FC<{
@ -19,9 +25,7 @@ const Password: React.FC<{
const { onRightClick, ContextMenuContainer, Item } = useItemFieldContextMenu()
const onToggle = useCallback(() => setShow(x => !x), [])
const onCopy = useCallback(() => {
navigator.clipboard.writeText(field.v)
}, [field.v])
const onCopy = useCopy(field.v)
const onOpenBigText = useCallback(() => {
showBigText(true)
}, [])
@ -67,9 +71,7 @@ const DateView: React.FC<{ field: ItemSection.Date }> = ({ field }) => {
const TextView: React.FC<{ value: string }> = ({ value }) => {
const { onRightClick, ContextMenuContainer, Item } = useItemFieldContextMenu()
const onCopy = useCallback(() => {
navigator.clipboard.writeText(value)
}, [value])
const onCopy = useCopy(value)
return (
<>
@ -126,7 +128,7 @@ export const ItemDetailsFieldValue: React.FC<{
return (
<ErrorBoundary>
<Container>{field.value}</Container>
<TextView value={field.value!} />
</ErrorBoundary>
)
}

View File

@ -1,7 +1,10 @@
import { memo } from "react"
import styled from "@emotion/styled"
import { cx } from "@emotion/css"
import type { Item } from "opvault.js"
import { CategoryIcon } from "./CategoryIcon"
import { useTranslate } from "../i18n"
import { ItemNoTitle } from "../styles"
interface ListProps {
items: Item[]
@ -36,6 +39,7 @@ const ItemTitle = styled.div`
font-weight: 600;
margin-bottom: 2px;
`
const ItemDescription = styled.div`
font-size: 95%;
white-space: nowrap;
@ -47,7 +51,9 @@ const Icon = styled(CategoryIcon)`
font-size: 1.5em;
`
export const ItemList: React.FC<ListProps> = ({ items, onSelect, selected }) => (
export const ItemList = memo<ListProps>(({ items, onSelect, selected }) => {
const t = useTranslate()
return (
<Container>
<List>
{items.map(item => (
@ -61,11 +67,14 @@ export const ItemList: React.FC<ListProps> = ({ items, onSelect, selected }) =>
>
<Icon fill="#FFF" category={item.category} />
<div>
<ItemTitle>{item.overview.title!}</ItemTitle>
<ItemTitle>
{item.overview.title || <ItemNoTitle>{t.label.no_title}</ItemNoTitle>}
</ItemTitle>
<ItemDescription>{item.overview.ainfo || " "}</ItemDescription>
</div>
</ItemView>
))}
</List>
</Container>
)
)
})

View File

@ -1,6 +1,6 @@
import styled from "@emotion/styled"
import type { Item } from "opvault.js"
import { useMemo } from "react"
import { useMemo, memo } from "react"
import { parseMonthYear } from "../utils"
const Container = styled.div`
@ -12,7 +12,7 @@ const Container = styled.div`
}
`
export const ItemWarning: React.FC<{ item: Item }> = ({ item }) => {
export const ItemWarning = memo<{ item: Item }>(({ item }) => {
const isExpired = useMemo(() => {
const fields = item.details.sections?.flatMap(x => x.fields ?? [])
if (!fields?.length) return false
@ -38,4 +38,4 @@ export const ItemWarning: React.FC<{ item: Item }> = ({ item }) => {
}
return null
}
})

View File

@ -1,3 +1,4 @@
import { memo } from "react"
import styled from "@emotion/styled"
const Container = styled.div`
@ -16,8 +17,8 @@ const Title = styled.div`
flex-grow: 1;
`
export const TitleBar = () => (
export const TitleBar = memo(() => (
<Container>
<Title>OPVault Viewer</Title>
</Container>
)
))

View File

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

View File

@ -1,5 +1,9 @@
# /* spellchecker: disable */
label:
app_name:
en: OPVault Viewer
fr: Lecteur de coffre OPVault
choose_a_vault:
en: Pick a vault
fr: Choisir un coffre
@ -28,6 +32,23 @@ label:
en: Password
fr: Mot de passe
no_title:
en: Untitled
fr: Sans titre
options:
sort_by_name:
en: Sort by Name
fr: Trier par nom
sort_by_created_at:
en: Sort by date created
fr: Trier par date de création
sort_by_updated_at:
en: Sort by date modified
fr: Trier par date de modification
noun:
vault:
en: vault

View File

@ -2,18 +2,21 @@ import React from "react"
import { render } from "react-dom"
import { App } from "./App"
import { LocaleContextProvider } from "./i18n"
import { SideEffect } from "./SideEffect"
import "./index.scss"
if (navigator.platform === "MacIntel") {
document.documentElement.classList.add("mac")
}
render(
const Root: React.FC = () => (
<React.StrictMode>
{/* <TitleBar /> */}
<LocaleContextProvider>
<SideEffect />
<App />
</LocaleContextProvider>
</React.StrictMode>,
document.getElementById("app")
</React.StrictMode>
)
render(<Root />, document.getElementById("app"))

3
packages/web/src/modules.d.ts vendored Normal file
View File

@ -0,0 +1,3 @@
declare module "react-idle-timer/modern" {
export * from "react-idle-timer/dist/modern"
}

View File

@ -136,9 +136,9 @@ export const VaultView: React.FC<{ vault: Vault; onLock(): void }> = ({
value={sortBy}
onChange={e => setSortBy(+e.currentTarget.value)}
>
<option value={SortBy.Name}>Sort by Name</option>
<option value={SortBy.CreatedAt}>Sort by Created Time</option>
<option value={SortBy.UpdatedAt}>Sort by Updated Time</option>
<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} />

View File

@ -1,4 +1,15 @@
import { css } from "@emotion/css"
import styled from "@emotion/styled"
export const ItemNoTitle = styled.span`
font-weight: normal;
font-style: italic;
[lang^="zh"],
[lang="ko"],
[lang="ja"] & {
font-style: normal;
}
`
export const scrollbar = css`
&&::-webkit-scrollbar {

View File

@ -13,6 +13,9 @@ export default defineConfig({
},
build: {
outDir: "dist/web",
rollupOptions: {
external: ["fs", ""],
},
},
resolve: {
alias: {

2678
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff