General improvements and bug fixes
This commit is contained in:
parent
bdd46a530c
commit
8f9ec73caf
32
package.json
32
package.json
@ -14,35 +14,35 @@
|
|||||||
"@types/chai-as-promised": "^7.1.4",
|
"@types/chai-as-promised": "^7.1.4",
|
||||||
"@types/fs-extra": "^9.0.13",
|
"@types/fs-extra": "^9.0.13",
|
||||||
"@types/mocha": "github:whitecolor/mocha-types#da22474cf43f48a56c86f8c23a5a0ea36e295768",
|
"@types/mocha": "github:whitecolor/mocha-types#da22474cf43f48a56c86f8c23a5a0ea36e295768",
|
||||||
"@types/node": "^16.10.3",
|
"@types/node": "^16.11.9",
|
||||||
"@types/sinon": "^10.0.4",
|
"@types/sinon": "^10.0.6",
|
||||||
"@types/sinon-chai": "^3.2.5",
|
"@types/sinon-chai": "^3.2.5",
|
||||||
"@types/wicg-file-system-access": "^2020.9.4",
|
"@types/wicg-file-system-access": "^2020.9.4",
|
||||||
"@typescript-eslint/eslint-plugin": "4.33.0",
|
"@typescript-eslint/eslint-plugin": "5.4.0",
|
||||||
"@typescript-eslint/parser": "4.33.0",
|
"@typescript-eslint/parser": "5.4.0",
|
||||||
"chai": "^4.3.4",
|
"chai": "^4.3.4",
|
||||||
"chai-as-promised": "^7.1.1",
|
"chai-as-promised": "^7.1.1",
|
||||||
"chalk": "^4.1.2",
|
"chalk": "^4.1.2",
|
||||||
"eslint": "7.32.0",
|
"eslint": "8.3.0",
|
||||||
"eslint-config-prettier": "8.3.0",
|
"eslint-config-prettier": "8.3.0",
|
||||||
"eslint-import-resolver-typescript": "2.5.0",
|
"eslint-import-resolver-typescript": "2.5.0",
|
||||||
"eslint-plugin-import": "2.24.2",
|
"eslint-plugin-import": "2.25.3",
|
||||||
"eslint-plugin-react": "7.26.1",
|
"eslint-plugin-react": "7.27.1",
|
||||||
"eslint-plugin-react-hooks": "4.2.0",
|
"eslint-plugin-react-hooks": "4.3.0",
|
||||||
"fs-extra": "^10.0.0",
|
"fs-extra": "^10.0.0",
|
||||||
"marked": "^3.0.8",
|
"marked": "^4.0.4",
|
||||||
"mocha": "^9.1.2",
|
"mocha": "^9.1.3",
|
||||||
"mochawesome": "^6.3.0",
|
"mochawesome": "^7.0.1",
|
||||||
"prettier": "^2.4.1",
|
"prettier": "^2.4.1",
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^17.0.2",
|
||||||
"sass": "^1.43.2",
|
"sass": "^1.43.4",
|
||||||
"sinon": "^11.1.2",
|
"sinon": "^12.0.1",
|
||||||
"sinon-chai": "^3.7.0",
|
"sinon-chai": "^3.7.0",
|
||||||
"tslib": "^2.3.1",
|
"tslib": "^2.3.1",
|
||||||
"ts-node": "^10.2.1",
|
"ts-node": "^10.4.0",
|
||||||
"tsconfig-paths": "^3.11.0",
|
"tsconfig-paths": "^3.12.0",
|
||||||
"typescript": "^4.4.3"
|
"typescript": "^4.5.2"
|
||||||
},
|
},
|
||||||
"prettier": {
|
"prettier": {
|
||||||
"arrowParens": "avoid",
|
"arrowParens": "avoid",
|
||||||
|
@ -16,8 +16,8 @@
|
|||||||
"@rollup/plugin-json": "^4.1.0",
|
"@rollup/plugin-json": "^4.1.0",
|
||||||
"@rollup/plugin-replace": "^3.0.0",
|
"@rollup/plugin-replace": "^3.0.0",
|
||||||
"prettier": "^2.4.1",
|
"prettier": "^2.4.1",
|
||||||
"rollup": "^2.58.0",
|
"rollup": "^2.60.1",
|
||||||
"rollup-plugin-ts": "^1.4.7",
|
"rollup-plugin-ts": "^2.0.4",
|
||||||
"typedoc": "^0.22.7"
|
"typedoc": "^0.22.9"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -21,6 +21,8 @@ export default () => ({
|
|||||||
preventAssignment: true,
|
preventAssignment: true,
|
||||||
values: {
|
values: {
|
||||||
"process.env.NODE_ENV": '"production"',
|
"process.env.NODE_ENV": '"production"',
|
||||||
|
'require("./adapter").nodeAdapter':
|
||||||
|
'import("./adapter").then(x => x.nodeAdapter)',
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
@ -30,7 +30,7 @@ export class OnePassword {
|
|||||||
|
|
||||||
constructor({
|
constructor({
|
||||||
path,
|
path,
|
||||||
adapter = process.browser ? null! : import("./adapter").then(x => x.nodeAdapter),
|
adapter = process.browser ? null! : require("./adapter").nodeAdapter,
|
||||||
}: IOptions) {
|
}: IOptions) {
|
||||||
this.#adapter = adapter
|
this.#adapter = adapter
|
||||||
this.#path = path
|
this.#path = path
|
||||||
|
@ -8,7 +8,7 @@ icon: dist/512x512.png
|
|||||||
directories:
|
directories:
|
||||||
output: bundle
|
output: bundle
|
||||||
app: dist
|
app: dist
|
||||||
buildResources: build
|
buildResources: dist
|
||||||
mac:
|
mac:
|
||||||
category: public.app-category.productivity
|
category: public.app-category.productivity
|
||||||
target:
|
target:
|
||||||
|
@ -14,24 +14,25 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@emotion/css": "^11.5.0",
|
"@emotion/css": "^11.5.0",
|
||||||
"@emotion/react": "^11.5.0",
|
"@emotion/react": "^11.6.0",
|
||||||
"@emotion/styled": "^11.3.0",
|
"@emotion/styled": "^11.6.0",
|
||||||
"@rollup/plugin-yaml": "^3.1.0",
|
"@rollup/plugin-yaml": "^3.1.0",
|
||||||
"@types/react": "^17.0.0",
|
"@types/react": "^17.0.36",
|
||||||
"@types/react-dom": "^17.0.0",
|
"@types/react-dom": "^17.0.11",
|
||||||
"@vitejs/plugin-react": "^1.0.0",
|
"@vitejs/plugin-react": "^1.1.0",
|
||||||
"buffer": "^6.0.3",
|
"buffer": "^6.0.3",
|
||||||
"electron": "^15.2.0",
|
"electron": "^16.0.1",
|
||||||
"electron-builder": "^22.13.1",
|
"electron-builder": "^22.14.5",
|
||||||
"esbuild": "^0.13.6",
|
"esbuild": "^0.13.15",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"opvault.js": "*",
|
"opvault.js": "*",
|
||||||
"path-browserify": "^1.0.1",
|
"path-browserify": "^1.0.1",
|
||||||
"react": "^17.0.0",
|
"react": "^17.0.2",
|
||||||
"react-dom": "^17.0.0",
|
"react-dom": "^17.0.2",
|
||||||
|
"react-idle-timer": "4.6.4",
|
||||||
"react-icons": "^4.3.1",
|
"react-icons": "^4.3.1",
|
||||||
"sass": "^1.43.4",
|
"sass": "^1.43.4",
|
||||||
"typescript": "^4.3.2",
|
"typescript": "^4.5.2",
|
||||||
"vite": "^2.6.4"
|
"vite": "^2.6.14"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { useCallback, useState } from "react"
|
import { useCallback, useEffect, useState } from "react"
|
||||||
import type { Vault, OnePassword } from "opvault.js"
|
import type { Vault, OnePassword } from "opvault.js"
|
||||||
|
import { useIdleTimer } from "react-idle-timer/modern"
|
||||||
import { VaultView } from "./pages/Vault"
|
import { VaultView } from "./pages/Vault"
|
||||||
import { VaultPicker } from "./pages/VaultPicker"
|
import { VaultPicker } from "./pages/VaultPicker"
|
||||||
|
|
||||||
@ -12,6 +13,19 @@ export const App: React.FC = () => {
|
|||||||
setVault(undefined)
|
setVault(undefined)
|
||||||
}, [vault])
|
}, [vault])
|
||||||
|
|
||||||
|
const { reset, pause } = useIdleTimer({
|
||||||
|
timeout: 60_000,
|
||||||
|
onIdle: onLock,
|
||||||
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (vault) {
|
||||||
|
reset()
|
||||||
|
} else {
|
||||||
|
pause()
|
||||||
|
}
|
||||||
|
}, [vault])
|
||||||
|
|
||||||
if (!vault) {
|
if (!vault) {
|
||||||
return (
|
return (
|
||||||
<VaultPicker
|
<VaultPicker
|
||||||
|
14
packages/web/src/SideEffect.ts
Normal file
14
packages/web/src/SideEffect.ts
Normal 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
|
||||||
|
})
|
@ -11,10 +11,12 @@ const Container = styled.div`
|
|||||||
top: 50%;
|
top: 50%;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
transform: translate(-50%, -50%);
|
transform: translate(-50%, -50%);
|
||||||
font-size: 8em;
|
font-size: 6em;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 20px 25px;
|
padding: 20px 25px;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
|
min-width: 75vw;
|
||||||
|
z-index: 2;
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
background: rgba(0, 0, 0, 0.6);
|
background: rgba(0, 0, 0, 0.6);
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { memo } from "react"
|
||||||
import { Category } from "opvault.js"
|
import { Category } from "opvault.js"
|
||||||
import { cx, css } from "@emotion/css"
|
import { cx, css } from "@emotion/css"
|
||||||
import { BsBank2, BsPeopleFill } from "react-icons/bs"
|
import { BsBank2, BsPeopleFill } from "react-icons/bs"
|
||||||
@ -77,14 +78,11 @@ interface CategoryIconProps {
|
|||||||
category: Category
|
category: Category
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CategoryIcon: React.FC<CategoryIconProps> = ({
|
export const CategoryIcon = memo<CategoryIconProps>(
|
||||||
className,
|
({ className, category, style, fill }) => {
|
||||||
category,
|
const Component = getComponent(category)
|
||||||
style,
|
return Component ? (
|
||||||
fill,
|
<Component className={cx(reactIconClass, className)} fill={fill} style={style} />
|
||||||
}) => {
|
) : null
|
||||||
const Component = getComponent(category)
|
}
|
||||||
return Component ? (
|
)
|
||||||
<Component className={cx(reactIconClass, className)} fill={fill} style={style} />
|
|
||||||
) : null
|
|
||||||
}
|
|
||||||
|
@ -3,6 +3,7 @@ import type { Attachment, AttachmentMetadata, Item, ItemField } from "opvault.js
|
|||||||
import type { ItemDetails } from "opvault.js/src/types"
|
import type { ItemDetails } from "opvault.js/src/types"
|
||||||
import { memo, useEffect, useState } from "react"
|
import { memo, useEffect, useState } from "react"
|
||||||
import { useTranslate } from "../i18n"
|
import { useTranslate } from "../i18n"
|
||||||
|
import { ItemNoTitle } from "../styles"
|
||||||
import { CategoryIcon } from "./CategoryIcon"
|
import { CategoryIcon } from "./CategoryIcon"
|
||||||
import { ItemDates } from "./ItemDates"
|
import { ItemDates } from "./ItemDates"
|
||||||
import {
|
import {
|
||||||
@ -59,7 +60,7 @@ const AttachmentContainer = styled.div`
|
|||||||
margin: 5px 0;
|
margin: 5px 0;
|
||||||
`
|
`
|
||||||
|
|
||||||
const SectionsView: React.FC<{ sections?: ItemDetails["sections"] }> = ({ sections }) =>
|
const SectionsView = memo<{ sections?: ItemDetails["sections"] }>(({ sections }) =>
|
||||||
sections?.length ? (
|
sections?.length ? (
|
||||||
<div style={{ marginBottom: 20 }}>
|
<div style={{ marginBottom: 20 }}>
|
||||||
{sections
|
{sections
|
||||||
@ -74,8 +75,9 @@ const SectionsView: React.FC<{ sections?: ItemDetails["sections"] }> = ({ sectio
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : null
|
) : null
|
||||||
|
)
|
||||||
|
|
||||||
const FieldsView: React.FC<{ fields?: ItemField[] }> = ({ fields }) =>
|
const FieldsView = memo<{ fields?: ItemField[] }>(({ fields }) =>
|
||||||
fields?.length ? (
|
fields?.length ? (
|
||||||
<div style={{ marginBottom: 20 }}>
|
<div style={{ marginBottom: 20 }}>
|
||||||
{fields.map((field, i) => (
|
{fields.map((field, i) => (
|
||||||
@ -83,8 +85,9 @@ const FieldsView: React.FC<{ fields?: ItemField[] }> = ({ fields }) =>
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : null
|
) : null
|
||||||
|
)
|
||||||
|
|
||||||
const TagsView: React.FC<{ tags?: string[] }> = ({ tags }) => {
|
const TagsView = memo<{ tags?: string[] }>(({ tags }) => {
|
||||||
const t = useTranslate()
|
const t = useTranslate()
|
||||||
if (!tags?.length) return null
|
if (!tags?.length) return null
|
||||||
return (
|
return (
|
||||||
@ -97,7 +100,7 @@ const TagsView: React.FC<{ tags?: string[] }> = ({ tags }) => {
|
|||||||
</div>
|
</div>
|
||||||
</ExtraField>
|
</ExtraField>
|
||||||
)
|
)
|
||||||
}
|
})
|
||||||
|
|
||||||
const JSONView = memo<{ item: Item }>(({ item }) => (
|
const JSONView = memo<{ item: Item }>(({ item }) => (
|
||||||
<details>
|
<details>
|
||||||
@ -108,7 +111,7 @@ const JSONView = memo<{ item: Item }>(({ item }) => (
|
|||||||
</details>
|
</details>
|
||||||
))
|
))
|
||||||
|
|
||||||
export const ItemView: React.FC<ItemViewProps> = ({ className, item }) => {
|
export const ItemView = memo<ItemViewProps>(({ className, item }) => {
|
||||||
const t = useTranslate()
|
const t = useTranslate()
|
||||||
return (
|
return (
|
||||||
<Container className={className}>
|
<Container className={className}>
|
||||||
@ -117,7 +120,9 @@ export const ItemView: React.FC<ItemViewProps> = ({ className, item }) => {
|
|||||||
<Header>
|
<Header>
|
||||||
{item.details.fields == null}
|
{item.details.fields == null}
|
||||||
<Icon category={item.category} />
|
<Icon category={item.category} />
|
||||||
<ItemTitle>{item.overview.title}</ItemTitle>
|
<ItemTitle>
|
||||||
|
{item.overview.title || <ItemNoTitle>{t.label.no_title}</ItemNoTitle>}
|
||||||
|
</ItemTitle>
|
||||||
</Header>
|
</Header>
|
||||||
|
|
||||||
<JSONView item={item} />
|
<JSONView item={item} />
|
||||||
@ -161,7 +166,7 @@ export const ItemView: React.FC<ItemViewProps> = ({ className, item }) => {
|
|||||||
</Inner>
|
</Inner>
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
}
|
})
|
||||||
|
|
||||||
function AttachmentView({ file }: { file: Attachment }) {
|
function AttachmentView({ file }: { file: Attachment }) {
|
||||||
const [metadata, setMetadata] = useState<AttachmentMetadata>()
|
const [metadata, setMetadata] = useState<AttachmentMetadata>()
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { memo } from "react"
|
||||||
import styled from "@emotion/styled"
|
import styled from "@emotion/styled"
|
||||||
import type { Item } from "opvault.js"
|
import type { Item } from "opvault.js"
|
||||||
import { useTranslate } from "../i18n"
|
import { useTranslate } from "../i18n"
|
||||||
@ -9,7 +10,7 @@ const Container = styled.div`
|
|||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
`
|
`
|
||||||
|
|
||||||
export const ItemDates: React.FC<{ item: Item }> = ({ item }) => {
|
export const ItemDates = memo<{ item: Item }>(({ item }) => {
|
||||||
const t = useTranslate()
|
const t = useTranslate()
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
@ -21,4 +22,4 @@ export const ItemDates: React.FC<{ item: Item }> = ({ item }) => {
|
|||||||
</div>
|
</div>
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
}
|
})
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { memo } from "react"
|
||||||
import styled from "@emotion/styled"
|
import styled from "@emotion/styled"
|
||||||
import type { ItemField, ItemSection } from "opvault.js"
|
import type { ItemField, ItemSection } from "opvault.js"
|
||||||
import { ErrorBoundary } from "./ErrorBoundary"
|
import { ErrorBoundary } from "./ErrorBoundary"
|
||||||
@ -13,9 +14,9 @@ export const FieldTitle: React.FC = styled.div`
|
|||||||
margin-bottom: 3px;
|
margin-bottom: 3px;
|
||||||
`
|
`
|
||||||
|
|
||||||
export const ItemFieldView: React.FC<{
|
export const ItemFieldView = memo<{
|
||||||
field: ItemSection.Any
|
field: ItemSection.Any
|
||||||
}> = ({ field }) => {
|
}>(({ field }) => {
|
||||||
if (field.v == null) {
|
if (field.v == null) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@ -28,11 +29,11 @@ export const ItemFieldView: React.FC<{
|
|||||||
</Container>
|
</Container>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
)
|
)
|
||||||
}
|
})
|
||||||
|
|
||||||
export const ItemDetailsFieldView: React.FC<{
|
export const ItemDetailsFieldView = memo<{
|
||||||
field: ItemField
|
field: ItemField
|
||||||
}> = ({ field }) => {
|
}>(({ field }) => {
|
||||||
if (field.value == null) {
|
if (field.value == null) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@ -45,4 +46,4 @@ export const ItemDetailsFieldView: React.FC<{
|
|||||||
</Container>
|
</Container>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
)
|
)
|
||||||
}
|
})
|
||||||
|
@ -7,7 +7,7 @@ const Container = styled.menu`
|
|||||||
box-shadow: #0004 0px 1px 4px;
|
box-shadow: #0004 0px 1px 4px;
|
||||||
left: 99%;
|
left: 99%;
|
||||||
margin-block-start: 0;
|
margin-block-start: 0;
|
||||||
min-width: 150px;
|
min-width: 180px;
|
||||||
padding-inline-start: 0;
|
padding-inline-start: 0;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
|
@ -9,6 +9,12 @@ import { useItemFieldContextMenu } from "./ItemFieldContextMenu"
|
|||||||
|
|
||||||
const Container = styled.div``
|
const Container = styled.div``
|
||||||
|
|
||||||
|
function useCopy(text: string) {
|
||||||
|
return useCallback(() => {
|
||||||
|
navigator.clipboard.writeText(text)
|
||||||
|
}, [text])
|
||||||
|
}
|
||||||
|
|
||||||
export { Password as PasswordFieldView }
|
export { Password as PasswordFieldView }
|
||||||
|
|
||||||
const Password: React.FC<{
|
const Password: React.FC<{
|
||||||
@ -19,9 +25,7 @@ const Password: React.FC<{
|
|||||||
|
|
||||||
const { onRightClick, ContextMenuContainer, Item } = useItemFieldContextMenu()
|
const { onRightClick, ContextMenuContainer, Item } = useItemFieldContextMenu()
|
||||||
const onToggle = useCallback(() => setShow(x => !x), [])
|
const onToggle = useCallback(() => setShow(x => !x), [])
|
||||||
const onCopy = useCallback(() => {
|
const onCopy = useCopy(field.v)
|
||||||
navigator.clipboard.writeText(field.v)
|
|
||||||
}, [field.v])
|
|
||||||
const onOpenBigText = useCallback(() => {
|
const onOpenBigText = useCallback(() => {
|
||||||
showBigText(true)
|
showBigText(true)
|
||||||
}, [])
|
}, [])
|
||||||
@ -67,9 +71,7 @@ const DateView: React.FC<{ field: ItemSection.Date }> = ({ field }) => {
|
|||||||
|
|
||||||
const TextView: React.FC<{ value: string }> = ({ value }) => {
|
const TextView: React.FC<{ value: string }> = ({ value }) => {
|
||||||
const { onRightClick, ContextMenuContainer, Item } = useItemFieldContextMenu()
|
const { onRightClick, ContextMenuContainer, Item } = useItemFieldContextMenu()
|
||||||
const onCopy = useCallback(() => {
|
const onCopy = useCopy(value)
|
||||||
navigator.clipboard.writeText(value)
|
|
||||||
}, [value])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -126,7 +128,7 @@ export const ItemDetailsFieldValue: React.FC<{
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<Container>{field.value}</Container>
|
<TextView value={field.value!} />
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
|
import { memo } from "react"
|
||||||
import styled from "@emotion/styled"
|
import styled from "@emotion/styled"
|
||||||
import { cx } from "@emotion/css"
|
import { cx } from "@emotion/css"
|
||||||
import type { Item } from "opvault.js"
|
import type { Item } from "opvault.js"
|
||||||
import { CategoryIcon } from "./CategoryIcon"
|
import { CategoryIcon } from "./CategoryIcon"
|
||||||
|
import { useTranslate } from "../i18n"
|
||||||
|
import { ItemNoTitle } from "../styles"
|
||||||
|
|
||||||
interface ListProps {
|
interface ListProps {
|
||||||
items: Item[]
|
items: Item[]
|
||||||
@ -36,6 +39,7 @@ const ItemTitle = styled.div`
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
margin-bottom: 2px;
|
margin-bottom: 2px;
|
||||||
`
|
`
|
||||||
|
|
||||||
const ItemDescription = styled.div`
|
const ItemDescription = styled.div`
|
||||||
font-size: 95%;
|
font-size: 95%;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
@ -47,25 +51,30 @@ const Icon = styled(CategoryIcon)`
|
|||||||
font-size: 1.5em;
|
font-size: 1.5em;
|
||||||
`
|
`
|
||||||
|
|
||||||
export const ItemList: React.FC<ListProps> = ({ items, onSelect, selected }) => (
|
export const ItemList = memo<ListProps>(({ items, onSelect, selected }) => {
|
||||||
<Container>
|
const t = useTranslate()
|
||||||
<List>
|
return (
|
||||||
{items.map(item => (
|
<Container>
|
||||||
<ItemView
|
<List>
|
||||||
key={item.uuid}
|
{items.map(item => (
|
||||||
onClick={() => onSelect(item)}
|
<ItemView
|
||||||
className={cx({
|
key={item.uuid}
|
||||||
selected: selected?.uuid === item.uuid,
|
onClick={() => onSelect(item)}
|
||||||
trashed: item.isDeleted,
|
className={cx({
|
||||||
})}
|
selected: selected?.uuid === item.uuid,
|
||||||
>
|
trashed: item.isDeleted,
|
||||||
<Icon fill="#FFF" category={item.category} />
|
})}
|
||||||
<div>
|
>
|
||||||
<ItemTitle>{item.overview.title!}</ItemTitle>
|
<Icon fill="#FFF" category={item.category} />
|
||||||
<ItemDescription>{item.overview.ainfo || " "}</ItemDescription>
|
<div>
|
||||||
</div>
|
<ItemTitle>
|
||||||
</ItemView>
|
{item.overview.title || <ItemNoTitle>{t.label.no_title}</ItemNoTitle>}
|
||||||
))}
|
</ItemTitle>
|
||||||
</List>
|
<ItemDescription>{item.overview.ainfo || " "}</ItemDescription>
|
||||||
</Container>
|
</div>
|
||||||
)
|
</ItemView>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import styled from "@emotion/styled"
|
import styled from "@emotion/styled"
|
||||||
import type { Item } from "opvault.js"
|
import type { Item } from "opvault.js"
|
||||||
import { useMemo } from "react"
|
import { useMemo, memo } from "react"
|
||||||
import { parseMonthYear } from "../utils"
|
import { parseMonthYear } from "../utils"
|
||||||
|
|
||||||
const Container = styled.div`
|
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 isExpired = useMemo(() => {
|
||||||
const fields = item.details.sections?.flatMap(x => x.fields ?? [])
|
const fields = item.details.sections?.flatMap(x => x.fields ?? [])
|
||||||
if (!fields?.length) return false
|
if (!fields?.length) return false
|
||||||
@ -38,4 +38,4 @@ export const ItemWarning: React.FC<{ item: Item }> = ({ item }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return null
|
return null
|
||||||
}
|
})
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { memo } from "react"
|
||||||
import styled from "@emotion/styled"
|
import styled from "@emotion/styled"
|
||||||
|
|
||||||
const Container = styled.div`
|
const Container = styled.div`
|
||||||
@ -16,8 +17,8 @@ const Title = styled.div`
|
|||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
`
|
`
|
||||||
|
|
||||||
export const TitleBar = () => (
|
export const TitleBar = memo(() => (
|
||||||
<Container>
|
<Container>
|
||||||
<Title>OPVault Viewer</Title>
|
<Title>OPVault Viewer</Title>
|
||||||
</Container>
|
</Container>
|
||||||
)
|
))
|
||||||
|
@ -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"
|
import texts from "./texts.yml"
|
||||||
|
|
||||||
const categories = Object.keys(texts)
|
const categories = Object.keys(texts)
|
||||||
|
|
||||||
const ALLOWED = new Set(["en", "fr"])
|
const ALLOWED = new Set(["en", "fr"])
|
||||||
|
const SKIP_ITALIC = new Set(["zh", "ko", "ja"])
|
||||||
const LOCALSTORAGE_KEY = "preferred-locale"
|
const LOCALSTORAGE_KEY = "preferred-locale"
|
||||||
|
|
||||||
function getLocaleFromStorage() {
|
function getLocaleFromStorage() {
|
||||||
@ -38,33 +47,33 @@ export const useLocaleContext = () => useContext(LocaleContext)
|
|||||||
|
|
||||||
export function useTranslate() {
|
export function useTranslate() {
|
||||||
const { locale } = useContext(LocaleContext)
|
const { locale } = useContext(LocaleContext)
|
||||||
const t = useMemo(
|
const getter = useCallback(
|
||||||
() =>
|
(category: string, key: string) => {
|
||||||
|
const obj = (texts as any)[category]
|
||||||
|
if (
|
||||||
|
process.env.NODE_ENV === "development" &&
|
||||||
|
!Object.prototype.hasOwnProperty.call(obj, key)
|
||||||
|
) {
|
||||||
|
throw new Error(`t.${key} does not exist.`)
|
||||||
|
}
|
||||||
|
return obj[key][locale]
|
||||||
|
},
|
||||||
|
[locale]
|
||||||
|
)
|
||||||
|
|
||||||
|
const t: {
|
||||||
|
[category in keyof typeof texts]: {
|
||||||
|
[key in keyof typeof texts[category]]: string
|
||||||
|
}
|
||||||
|
} = useMemo(
|
||||||
|
(): any =>
|
||||||
Object.fromEntries(
|
Object.fromEntries(
|
||||||
categories.map(category => [
|
categories.map(category => [
|
||||||
category,
|
category,
|
||||||
new Proxy(
|
new Proxy({}, { get: (_, p: string) => getter(category, p) }),
|
||||||
{},
|
|
||||||
{
|
|
||||||
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]
|
|
||||||
},
|
|
||||||
}
|
|
||||||
),
|
|
||||||
])
|
])
|
||||||
) as {
|
),
|
||||||
[category in keyof typeof texts]: {
|
[getter]
|
||||||
[key in keyof typeof texts[category]]: string
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[locale]
|
|
||||||
)
|
)
|
||||||
return t
|
return t
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,9 @@
|
|||||||
# /* spellchecker: disable */
|
# /* spellchecker: disable */
|
||||||
label:
|
label:
|
||||||
|
app_name:
|
||||||
|
en: OPVault Viewer
|
||||||
|
fr: Lecteur de coffre OPVault
|
||||||
|
|
||||||
choose_a_vault:
|
choose_a_vault:
|
||||||
en: Pick a vault
|
en: Pick a vault
|
||||||
fr: Choisir un coffre
|
fr: Choisir un coffre
|
||||||
@ -28,6 +32,23 @@ label:
|
|||||||
en: Password
|
en: Password
|
||||||
fr: Mot de passe
|
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:
|
noun:
|
||||||
vault:
|
vault:
|
||||||
en: vault
|
en: vault
|
||||||
|
@ -2,18 +2,21 @@ import React from "react"
|
|||||||
import { render } from "react-dom"
|
import { render } from "react-dom"
|
||||||
import { App } from "./App"
|
import { App } from "./App"
|
||||||
import { LocaleContextProvider } from "./i18n"
|
import { LocaleContextProvider } from "./i18n"
|
||||||
|
import { SideEffect } from "./SideEffect"
|
||||||
import "./index.scss"
|
import "./index.scss"
|
||||||
|
|
||||||
if (navigator.platform === "MacIntel") {
|
if (navigator.platform === "MacIntel") {
|
||||||
document.documentElement.classList.add("mac")
|
document.documentElement.classList.add("mac")
|
||||||
}
|
}
|
||||||
|
|
||||||
render(
|
const Root: React.FC = () => (
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
{/* <TitleBar /> */}
|
{/* <TitleBar /> */}
|
||||||
<LocaleContextProvider>
|
<LocaleContextProvider>
|
||||||
|
<SideEffect />
|
||||||
<App />
|
<App />
|
||||||
</LocaleContextProvider>
|
</LocaleContextProvider>
|
||||||
</React.StrictMode>,
|
</React.StrictMode>
|
||||||
document.getElementById("app")
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
render(<Root />, document.getElementById("app"))
|
||||||
|
3
packages/web/src/modules.d.ts
vendored
Normal file
3
packages/web/src/modules.d.ts
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
declare module "react-idle-timer/modern" {
|
||||||
|
export * from "react-idle-timer/dist/modern"
|
||||||
|
}
|
@ -136,9 +136,9 @@ export const VaultView: React.FC<{ vault: Vault; onLock(): void }> = ({
|
|||||||
value={sortBy}
|
value={sortBy}
|
||||||
onChange={e => setSortBy(+e.currentTarget.value)}
|
onChange={e => setSortBy(+e.currentTarget.value)}
|
||||||
>
|
>
|
||||||
<option value={SortBy.Name}>Sort by Name</option>
|
<option value={SortBy.Name}>{t.options.sort_by_name}</option>
|
||||||
<option value={SortBy.CreatedAt}>Sort by Created Time</option>
|
<option value={SortBy.CreatedAt}>{t.options.sort_by_created_at}</option>
|
||||||
<option value={SortBy.UpdatedAt}>Sort by Updated Time</option>
|
<option value={SortBy.UpdatedAt}>{t.options.sort_by_updated_at}</option>
|
||||||
</select>
|
</select>
|
||||||
</SortContainer>
|
</SortContainer>
|
||||||
<ItemList items={filtered} onSelect={setItem} selected={item} />
|
<ItemList items={filtered} onSelect={setItem} selected={item} />
|
||||||
|
@ -1,4 +1,15 @@
|
|||||||
import { css } from "@emotion/css"
|
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`
|
export const scrollbar = css`
|
||||||
&&::-webkit-scrollbar {
|
&&::-webkit-scrollbar {
|
||||||
|
@ -13,6 +13,9 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
build: {
|
build: {
|
||||||
outDir: "dist/web",
|
outDir: "dist/web",
|
||||||
|
rollupOptions: {
|
||||||
|
external: ["fs", ""],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
|
2678
pnpm-lock.yaml
generated
2678
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user