Implement big text view and preliminary i18n

This commit is contained in:
aet
2021-11-06 21:49:54 -04:00
parent d2ae4be194
commit 7ee6990be1
22 changed files with 624 additions and 263 deletions

View File

@ -3,3 +3,4 @@ node_modules
dist
bundle
*.local
*.yml.d.ts

View File

@ -8,10 +8,7 @@ const args = process.argv.slice(2)
build({
bundle: true,
define: {},
entryPoints: [
"./src/electron/index.ts",
// "./src/electron/preload.ts"
],
entryPoints: ["./src/electron/index.ts"],
outdir: "./dist/main",
external: builtinModules.concat("electron"),
target: ["chrome90"],

263
packages/web/logo.svg Normal file
View File

@ -0,0 +1,263 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 128 128"
version="1.0"
id="svg11300"
height="128"
width="128"
>
<style>
.card-rect {
/* fill: #2ec27e; */
fill: #496ccf;
}
.barcode-1,
.barcode-2,
.barcode-3 {
/* stroke: #26a269; */
stroke: #7b95e1;z
}
#path1138 {
fill: #7b95e1;z
}
.barcode-1 {
stroke-width: 1.87082875;
}
.barcode-2 {
stroke-width: 3.7416575;
}
.barcode-3 {
stroke-width: 5.61248589;
}
</style>
<title id="title4162">Adwaita Icon Template</title>
<defs id="defs3">
<linearGradient id="linearGradient1296">
<stop id="stop1292" offset="0" style="stop-color: #77767b; stop-opacity: 1" />
<stop
style="stop-color: #c0bfbc; stop-opacity: 1"
offset="0.17589436"
id="stop1300"
/>
<stop
id="stop1302"
offset="0.4092612"
style="stop-color: #77767b; stop-opacity: 1"
/>
<stop id="stop1294" offset="1" style="stop-color: #3d3846; stop-opacity: 1" />
</linearGradient>
<linearGradient id="linearGradient969">
<stop style="stop-color: #f6f5f4; stop-opacity: 1" offset="0" id="stop963" />
<stop
id="stop965"
offset="0.25731823"
style="stop-color: #ffffff; stop-opacity: 1"
/>
<stop
style="stop-color: #c0bfbc; stop-opacity: 1"
offset="0.5999999"
id="stop1085"
/>
<stop
id="stop1087"
offset="0.70312482"
style="stop-color: #f6f5f4; stop-opacity: 1"
/>
<stop style="stop-color: #f6f5f4; stop-opacity: 1" offset="1" id="stop967" />
</linearGradient>
<linearGradient id="linearGradient1040">
<stop style="stop-color: #c0bfbc; stop-opacity: 1" offset="0" id="stop1036" />
<stop style="stop-color: #f6f5f4; stop-opacity: 1" offset="1" id="stop1038" />
</linearGradient>
<linearGradient
y2="249.87819"
x2="67.121834"
y1="238.30762"
x1="78.692398"
gradientTransform="translate(55.100502, 0.07106726)"
gradientUnits="userSpaceOnUse"
id="linearGradient1986"
xlink:href="#linearGradient969"
/>
<linearGradient
y2="70.300697"
x2="85.886963"
y1="67.679771"
x1="88.507896"
gradientTransform="translate(55.769701, 171.28412)"
gradientUnits="userSpaceOnUse"
id="linearGradient1988"
xlink:href="#linearGradient1040"
/>
<linearGradient
gradientUnits="userSpaceOnUse"
y2="268"
x2="198"
y1="268"
x1="142"
id="linearGradient1039"
xlink:href="#linearGradient1296"
/>
</defs>
<metadata id="metadata4">
<rdf:RDF>
<cc:Work rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:creator>
<cc:Agent>
<dc:title>GNOME Design Team</dc:title>
</cc:Agent>
</dc:creator>
<dc:source />
<cc:license rdf:resource="http://creativecommons.org/licenses/by-sa/4.0/" />
<dc:title>Adwaita Icon Template</dc:title>
<dc:subject>
<rdf:Bag />
</dc:subject>
<dc:date />
<dc:rights>
<cc:Agent>
<dc:title />
</cc:Agent>
</dc:rights>
<dc:publisher>
<cc:Agent>
<dc:title />
</cc:Agent>
</dc:publisher>
<dc:identifier />
<dc:relation />
<dc:language />
<dc:coverage />
<dc:description />
<dc:contributor>
<cc:Agent>
<dc:title />
</cc:Agent>
</dc:contributor>
</cc:Work>
<cc:License rdf:about="http://creativecommons.org/licenses/by-sa/4.0/">
<cc:permits rdf:resource="http://creativecommons.org/ns#Reproduction" />
<cc:permits rdf:resource="http://creativecommons.org/ns#Distribution" />
<cc:requires rdf:resource="http://creativecommons.org/ns#Notice" />
<cc:requires rdf:resource="http://creativecommons.org/ns#Attribution" />
<cc:permits rdf:resource="http://creativecommons.org/ns#DerivativeWorks" />
<cc:requires rdf:resource="http://creativecommons.org/ns#ShareAlike" />
</cc:License>
</rdf:RDF>
</metadata>
<g transform="translate(0, -172)" id="layer1">
<g id="layer9">
<rect
class="card-rect"
id="rect1027"
width="112"
height="63.999977"
x="8"
y="224"
rx="8"
ry="8"
/>
<g id="g1256" transform="matrix(1,0,0,1.1428571,-4.8522949e-8,-22.857143)">
<path class="barcode-1" d="m 27,230 v 14" id="path1164" />
<path class="barcode-2" d="m 32,230 v 14" id="path1166" />
<path class="barcode-1" d="m 37,230 v 14" id="path1168" />
<path class="barcode-1" d="m 41,230 v 14" id="path1170" />
<path class="barcode-2" d="m 46,230 v 14" id="path1172" />
<path class="barcode-2" d="m 56,230 v 14" id="path1174" />
<path class="barcode-1" d="m 51,230 v 14" id="path1176" />
<path class="barcode-3" d="m 63,230 v 14" id="path1178" />
<path class="barcode-1" d="m 69,230 v 14" id="path1180" />
<path class="barcode-1" d="m 73,230 v 14" id="path1182" />
<path class="barcode-1" d="m 77,230 v 14" id="path1184" />
<path class="barcode-2" d="m 82,230 v 14" id="path1186" />
<path class="barcode-1" d="m 87,230 v 14" id="path1188" />
<path class="barcode-1" d="m 99,230 v 14" id="path1190" />
<path class="barcode-3" d="m 93,230 v 14" id="path1192" />
<path class="barcode-1" d="m 103,230 v 14" id="path1194" />
<path class="barcode-1" d="m 107,230 v 14" id="path1196" />
</g>
<g
style="display: inline; fill: #f8faff; enable-background: new"
id="g1130"
transform="translate(-4.8522949e-8,12)"
>
<path
d="m 22.21582,254 1.197265,5.06836 -3.90039,-3.2793 -1.453125,2.5918 5.009765,1.6582 -5.042968,1.58985 1.433593,2.64062 3.955078,-3.30469 L 22.21582,266 h 3.410156 l -1.101562,-4.95898 3.833984,3.31445 1.476562,-2.61914 -4.978515,-1.68555 5.117187,-1.42578 -1.453125,-2.61914 -4.033203,3.08398 L 25.624023,254 Z"
id="path1940"
/>
<path
id="path1056"
d="m 38.21582,254 1.197265,5.06836 -3.90039,-3.2793 -1.453125,2.5918 5.009765,1.6582 -5.042968,1.58985 1.433593,2.64062 3.955078,-3.30469 L 38.21582,266 h 3.410156 l -1.101562,-4.95898 3.833984,3.31445 1.476562,-2.61914 -4.978515,-1.68555 5.117187,-1.42578 -1.453125,-2.61914 -4.033203,3.08398 L 41.624023,254 Z"
/>
<path
d="m 54.21582,254 1.197265,5.06836 -3.90039,-3.2793 -1.453125,2.5918 5.009765,1.6582 -5.042968,1.58985 1.433593,2.64062 3.955078,-3.30469 L 54.21582,266 h 3.410156 l -1.101562,-4.95898 3.833984,3.31445 1.476562,-2.61914 -4.978515,-1.68555 5.117187,-1.42578 -1.453125,-2.61914 -4.033203,3.08398 L 57.624023,254 Z"
id="path1062"
/>
<path
id="path1068"
d="m 70.21582,254 1.197265,5.06836 -3.90039,-3.2793 -1.453125,2.5918 5.009765,1.6582 -5.042968,1.58985 1.433593,2.64062 3.955078,-3.30469 L 70.21582,266 h 3.410156 l -1.101562,-4.95898 3.833984,3.31445 1.476562,-2.61914 -4.978515,-1.68555 5.117187,-1.42578 -1.453125,-2.61914 -4.033203,3.08398 L 73.624023,254 Z"
/>
<path
d="m 86.21582,254 1.197265,5.06836 -3.90039,-3.2793 -1.453125,2.5918 5.009765,1.6582 -5.042968,1.58985 1.433593,2.64062 3.955078,-3.30469 L 86.21582,266 h 3.410156 l -1.101562,-4.95898 3.833984,3.31445 1.476562,-2.61914 -4.978515,-1.68555 5.117187,-1.42578 -1.453125,-2.61914 -4.033203,3.08398 L 89.624023,254 Z"
id="path1074"
/>
<path
id="path1080"
d="m 102.21582,254 1.19726,5.06836 -3.900385,-3.2793 -1.453125,2.5918 5.00976,1.6582 -5.042963,1.58985 1.433593,2.64062 3.95508,-3.30469 L 102.21582,266 h 3.41016 l -1.10157,-4.95898 3.83399,3.31445 1.47656,-2.61914 -4.97851,-1.68555 5.11718,-1.42578 -1.45312,-2.61914 -4.03321,3.08398 L 105.62402,254 Z"
/>
</g>
<rect
ry="7.9999995"
rx="8"
y="200"
x="8"
height="40"
width="112"
id="rect954"
class="card-rect"
/>
<rect y="214" x="8" height="18" width="112" id="rect961" style="fill: #241f31" />
<path id="path1138" d="m 22,242 -7.2,6 7.2,6 z" />
</g>
<g id="g959-3" transform="rotate(-180,107.5,242)">
<path
style="fill: url(#linearGradient1039)"
d="m 170,296 a 28,28 0 0 1 -28,-28 28,28 0 0 1 28,-28 28,28 0 0 1 28,28 28,28 0 0 1 -28,28 z m 0.0312,-12 a 6.0312505,6.0000005 0 0 0 6.03125,-6 6.0312505,6.0000005 0 0 0 -6.03125,-6 6.0312505,6.0000005 0 0 0 -6.03125,6 6.0312505,6.0000005 0 0 0 6.03125,6 z"
id="path947-0"
/>
<g
transform="matrix(0.70710678,-0.70710678,-0.70710678,-0.70710678,243.95332,484.3158)"
id="g955-3"
>
<path
id="path1990"
d="m 125.41422,214.44366 -16.97055,16.97056 8.36742,8.36743 3.65338,-3.41768 4.24264,4.24264 h 2.82843 v 2.82843 l 2.12132,2.12132 h 4.24264 v 4.24264 h 2.82843 v 2.82842 h 5.65685 v 5.65686 h 2.82843 v 2.82843 h 12.72792 l 4.94974,-4.94976 v -4.24264 z"
style="fill: #77767b"
/>
<path
style="fill: url(#linearGradient1986)"
d="M 124,213.02944 107.02945,230 l 8.36742,8.36743 3.65338,-3.41768 4.24264,4.24264 h 2.82843 v 2.82843 l 2.12132,2.12132 h 4.24264 v 4.24264 h 2.82843 v 2.82842 h 5.65685 v 5.65686 h 2.82843 v 2.82843 h 12.72792 l 4.94974,-4.94976 v -4.24264 z"
id="path951-1"
/>
<path
style="fill: url(#linearGradient1988)"
d="m 125.74823,221.26459 c -1.79388,0.002 -2.67811,2.18243 -1.39257,3.43359 l 33.58547,33.5861 2.82844,-2.82843 -33.58579,-33.58579 c -0.37702,-0.38755 -0.89487,-0.60597 -1.43555,-0.60547 z"
id="path953-2"
/>
</g>
<path
id="path957-8"
d="m 170,298 a 28,28 0 0 0 28,-28 28,28 0 0 0 -28,-28 28,28 0 0 0 -28,28 28,28 0 0 0 28,28 z m -0.0312,-12 a 6.0312505,6.0000005 0 0 1 -6.03125,-6 6.0312505,6.0000005 0 0 1 6.03125,-6 6.0312505,6.0000005 0 0 1 6.03125,6 6.0312505,6.0000005 0 0 1 -6.03125,6 z"
style="fill: #f6f5f4"
/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 10 KiB

View File

@ -1,6 +1,6 @@
{
"name": "opvault-web",
"version": "0.0.0",
"version": "1.0.0",
"main": "dist/main/index.js",
"author": "proteria",
"license": "GPL-3.0-only",
@ -9,13 +9,14 @@
"dev": "vite",
"build": "vite build",
"serve": "vite preview",
"start": "NODE_ENV=development electron --enable-transparent-visuals --disable-gpu ./dist/main/index.js",
"start": "./esbuild.js && NODE_ENV=development electron --enable-transparent-visuals --disable-gpu ./dist/main/index.js",
"bundle": "./scripts/build.sh"
},
"devDependencies": {
"@emotion/css": "^11.5.0",
"@emotion/react": "^11.5.0",
"@emotion/styled": "^11.3.0",
"@rollup/plugin-yaml": "^3.1.0",
"@types/react": "^17.0.0",
"@types/react-dom": "^17.0.0",
"@vitejs/plugin-react": "^1.0.0",
@ -23,6 +24,7 @@
"electron": "^15.2.0",
"electron-builder": "^22.13.1",
"esbuild": "^0.13.6",
"js-yaml": "^4.1.0",
"opvault.js": "*",
"path-browserify": "^1.0.1",
"react": "^17.0.0",
@ -38,14 +40,19 @@
"files": [
"**/*"
],
"icon": "dist/512x512.png",
"directories": {
"output": "bundle",
"app": "dist"
"app": "dist",
"buildResources": "build"
},
"linux": {
"executableName": "opvault",
"icon": "1p.png",
"category": "Utility"
"category": "Utility",
"icon": "512x512.png",
"target": [
"AppImage"
]
}
}
}

View File

@ -0,0 +1,21 @@
#!/usr/bin/env node
const fs = require("fs")
const { resolve } = require("path")
const { load } = require("js-yaml")
const ymlPath = resolve(__dirname, "../src/i18n/texts.yml")
const json = load(fs.readFileSync(ymlPath, "utf-8"))
const dtsPath = ymlPath + ".d.ts"
fs.writeFileSync(
dtsPath,
`type Translation = Record<string, string>;
declare const exportee: {
${Object.keys(json)
.map(x => `${x}: Translation;`)
.join("\n ")}
};
export default exportee;
`
)

View File

@ -1,5 +1,5 @@
#!/bin/sh
yarn build
npx vite build
NODE_ENV=production ./esbuild.js
./scripts/build-package-json.js
./node_modules/.bin/electron-builder build

View File

@ -0,0 +1,51 @@
import styled from "@emotion/styled"
import { memo, useEffect } from "react"
const Container = styled.div`
background: rgba(255, 255, 255, 0.6);
backdrop-filter: blur(4px);
border-radius: 20px;
font-family: var(--monospace);
letter-spacing: 2px;
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 8em;
text-align: center;
padding: 20px 25px;
word-break: break-word;
@media (prefers-color-scheme: dark) {
background: rgba(0, 0, 0, 0.6);
}
`
const Letter = styled.span`
&:nth-of-type(even) {
opacity: 0.8;
}
`
interface BigTextViewProps {
onClose(): void
children: string
}
export const BigTextView = memo<BigTextViewProps>(({ onClose, children }) => {
useEffect(() => {
const fn = (e: KeyboardEvent) => {
if (e.code === "Escape") {
onClose()
}
}
document.addEventListener("keydown", fn)
return () => document.removeEventListener("keydown", fn)
}, [onClose])
return (
<Container>
{children.split("").map((letter, i) => (
<Letter key={i}>{letter}</Letter>
))}
</Container>
)
})

View File

@ -1,5 +1,6 @@
import styled from "@emotion/styled"
import type { Item } from "opvault.js"
import { useTranslate } from "../i18n"
const Container = styled.div`
text-align: center;
@ -8,9 +9,16 @@ const Container = styled.div`
opacity: 0.5;
`
export const ItemDates: React.FC<{ item: Item }> = ({ item }) => (
<Container>
<div>Last Updated: {new Date(item.updatedAt).toLocaleString()}</div>
<div>Created: {new Date(item.createdAt).toLocaleString()}</div>
</Container>
)
export const ItemDates: React.FC<{ item: Item }> = ({ item }) => {
const t = useTranslate()
return (
<Container>
<div>
{t.label_last_updated}: {new Date(item.updatedAt).toLocaleString()}
</div>
<div>
{t.label_created_at}: {new Date(item.createdAt).toLocaleString()}
</div>
</Container>
)
}

View File

@ -3,6 +3,7 @@ import type { ItemSection, ItemField } from "opvault.js"
import { FieldType } from "opvault.js"
import { useCallback, useMemo, useState } from "react"
import { parseMonthYear } from "../utils"
import { BigTextView } from "./BigTextView"
import { ErrorBoundary } from "./ErrorBoundary"
import { useItemFieldContextMenu } from "./ItemFieldContextMenu"
@ -12,11 +13,19 @@ const Password: React.FC<{
field: ItemSection.Concealed
}> = ({ field }) => {
const [show, setShow] = useState(false)
const [bigText, showBigText] = useState(false)
const { onRightClick, ContextMenuContainer, Item } = useItemFieldContextMenu()
const onToggle = useCallback(() => setShow(x => !x), [])
const onCopy = useCallback(() => {
navigator.clipboard.writeText(field.v)
}, [field.v])
const onOpenBigText = useCallback(() => {
showBigText(true)
}, [])
const onCloseBigText = useCallback(() => {
showBigText(false)
}, [])
return (
<>
@ -30,9 +39,11 @@ const Password: React.FC<{
>
{show ? field.v : "·".repeat(10)}
</Container>
{bigText && <BigTextView onClose={onCloseBigText}>{field.v}</BigTextView>}
<ContextMenuContainer>
<Item onClick={onCopy}>Copier</Item>
<Item onClick={onToggle}>{show ? "Cacher" : "Afficher"}</Item>
{!bigText && <Item onClick={onOpenBigText}>Afficher en gros caractères</Item>}
</ContextMenuContainer>
</>
)
@ -52,6 +63,22 @@ const DateView: React.FC<{ field: ItemSection.Date }> = ({ field }) => {
return <Container>{date.toLocaleDateString()}</Container>
}
const TextView: React.FC<{ value: string }> = ({ value }) => {
const { onRightClick, ContextMenuContainer, Item } = useItemFieldContextMenu()
const onCopy = useCallback(() => {
navigator.clipboard.writeText(value)
}, [value])
return (
<>
<Container onContextMenu={onRightClick}>{value}</Container>
<ContextMenuContainer>
<Item onClick={onCopy}>Copier</Item>
</ContextMenuContainer>
</>
)
}
export const ItemFieldValue: React.FC<{
field: ItemSection.Any
}> = ({ field }) => {
@ -80,7 +107,7 @@ export const ItemFieldValue: React.FC<{
return (
<ErrorBoundary>
<Container>{field.v}</Container>
<TextView value={field.v} />
</ErrorBoundary>
)
}

View File

@ -1,8 +1,11 @@
import { useCallback } from "react"
import { useTranslate } from "../i18n"
export const VaultPicker: React.FC<{
setHandle(handle: FileSystemDirectoryHandle): void
}> = ({ setHandle }) => {
const t = useTranslate()
const onClick = useCallback(async () => {
try {
const handle = await showDirectoryPicker()
@ -15,5 +18,5 @@ export const VaultPicker: React.FC<{
}
}, [setHandle])
return <button onClick={onClick}>Pick a vault here.</button>
return <button onClick={onClick}>{t.label_choose_a_vault}</button>
}

View File

@ -1,6 +1,6 @@
// @ts-check
// Modules to control application life and create native browser window
// import { join } from "path"
import { join } from "path"
import { app, BrowserWindow, Menu } from "electron"
function createWindow() {
@ -10,6 +10,7 @@ function createWindow() {
height: 650,
// frame: false,
// transparent: true,
icon: join(__dirname, "../512x512.png"),
webPreferences: {
contextIsolation: true,
// preload: join(__dirname, "preload.js"),

View File

@ -0,0 +1,73 @@
import { createContext, memo, useContext, useEffect, useMemo, useState } from "react"
import texts from "./texts.yml"
type Keys = keyof typeof texts
const ALLOWED = new Set(["en", "fr"])
const LOCALSTORAGE_KEY = "preferred-locale"
function getLocaleFromStorage() {
try {
const key = localStorage.getItem(LOCALSTORAGE_KEY)
if (key && ALLOWED.has(key)) {
return key
}
} catch {}
}
function getNavigatorLocale() {
if (typeof navigator !== "undefined") {
for (const lang of navigator.languages) {
if (ALLOWED.has(lang)) {
return lang
}
}
}
}
function getEnvLocale() {
return getLocaleFromStorage() ?? getNavigatorLocale() ?? "en"
}
const LocaleContext = createContext<{
locale: string
setLocale(locale: string): void
}>(undefined!)
export const useLocaleContext = () => useContext(LocaleContext)
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.`)
}
return (texts as any)[p][locale]
},
}
) as {
[key in Keys]: string
},
[locale]
)
return t
}
export const LocaleContextProvider = memo(({ children }) => {
const [locale, setLocale] = useState(getEnvLocale)
useEffect(() => {
try {
localStorage.setItem(LOCALSTORAGE_KEY, locale)
} catch {}
}, [locale])
const value = useMemo(() => ({ locale, setLocale }), [locale])
return <LocaleContext.Provider value={value}>{children}</LocaleContext.Provider>
})

View File

@ -0,0 +1,28 @@
# /* spellchecker: disable */
label_choose_a_vault:
en: Pick a vault here.
fr: Choisir un coffre ici.
label_no_vault_selected:
en: No vault is selected.
fr: Aucun coffre nest sélectionné.
label_last_updated:
en: Last Updated
fr: Dernière modification
label_created_at:
en: Created At
fr: Créé
noun_vault:
en: vault
fr: coffre
action_lock:
en: Lock
fr: Vérouiller
action_unlock:
en: Unlock
fr: Déverouiller

View File

@ -1,12 +1,15 @@
import React from "react"
import { render } from "react-dom"
import { App } from "./App"
import { LocaleContextProvider } from "./i18n"
import "./index.scss"
render(
<React.StrictMode>
{/* <TitleBar /> */}
<App />
<LocaleContextProvider>
<App />
</LocaleContextProvider>
</React.StrictMode>,
document.getElementById("root")
)

View File

@ -1,8 +1,8 @@
import styled from "@emotion/styled"
import { VaultPicker } from "../components/VaultPicker"
import { useTranslate } from "../i18n"
const Container = styled.div`
width: 800px;
padding: 100px;
text-align: center;
`
@ -12,9 +12,12 @@ const Info = styled.div`
export const PickOPVault: React.FC<{
setHandle(handle: FileSystemDirectoryHandle): void
}> = ({ setHandle }) => (
<Container>
<VaultPicker setHandle={setHandle} />
<Info>No vault is picked.</Info>
</Container>
)
}> = ({ setHandle }) => {
const t = useTranslate()
return (
<Container>
<VaultPicker setHandle={setHandle} />
<Info>{t.label_no_vault_selected}</Info>
</Container>
)
}

View File

@ -1,8 +1,9 @@
import type { OnePassword } from "opvault.js"
import styled from "@emotion/styled"
import { useCallback, useEffect, useState } from "react"
import { useTranslate } from "../i18n"
const Container = styled.div`
const Container = styled.form`
padding: 20px;
text-align: center;
`
@ -11,15 +12,21 @@ export const Unlock: React.FC<{
instance: OnePassword
onUnlock(profile: string, password: string): void
}> = ({ onUnlock, instance }) => {
const t = useTranslate()
const [profiles, setProfiles] = useState<string[]>(() => [])
const [profile, setProfile] = useState<string>()
const [password, setPassword] = useState("")
const unlock = useCallback(() => {
if (!profile) return
onUnlock(profile, password)
setPassword("")
}, [onUnlock, profile, password])
const unlock = useCallback(
(e: React.FormEvent) => {
e.preventDefault()
if (!profile) return
onUnlock(profile, password)
setPassword("")
},
[onUnlock, profile, password]
)
useEffect(() => {
instance.getProfileNames().then(profiles => {
@ -29,12 +36,12 @@ export const Unlock: React.FC<{
}, [instance])
return (
<Container>
<Container onSubmit={unlock}>
<div>
<select value={profile} onChange={e => setProfile(e.currentTarget.value)}>
{profiles.map(p => (
<option key={p} value={p}>
Vault: {p}
{t.noun_vault}: {p}
</option>
))}
</select>
@ -46,8 +53,8 @@ export const Unlock: React.FC<{
onChange={e => setPassword(e.currentTarget.value)}
/>
</div>
<button type="submit" disabled={!profile || !password} onClick={unlock}>
Unlock
<button type="submit" disabled={!profile || !password}>
{t.action_unlock}
</button>
</Container>
)

View File

@ -6,6 +6,7 @@ import { IoSearch } from "react-icons/io5"
import { ItemList } from "../components/ItemList"
import { ItemView } from "../components/Item"
import { reactIconClass } from "../components/CategoryIcon"
import { useTranslate } from "../i18n/index"
const Container = styled.div`
display: flex;
@ -54,6 +55,7 @@ export const VaultView: React.FC<{ vault: Vault; onLock(): void }> = ({
vault,
onLock,
}) => {
const t = useTranslate()
const [items, setItems] = useState<Item[]>(() => [])
const [item, setItem] = useState<Item>()
const [sortBy, setSortBy] = useState(SortBy.Name)
@ -99,7 +101,7 @@ export const VaultView: React.FC<{ vault: Vault; onLock(): void }> = ({
margin: "10px 10px",
}}
>
<button onClick={onLock}>Lock</button>
<button onClick={onLock}>{t.action_lock}</button>
</div>
<SearchContainer>
<SearchInput

View File

@ -1,10 +1,11 @@
import { defineConfig } from "vite"
import react from "@vitejs/plugin-react"
import yaml from "@rollup/plugin-yaml"
// https://vitejs.dev/config/
export default defineConfig({
base: "./",
plugins: [react()],
plugins: [react(), yaml()],
define: {
global: "globalThis",
"process.browser": "true",
@ -16,6 +17,7 @@ export default defineConfig({
resolve: {
alias: {
path: require.resolve("path-browserify"),
buffer: require.resolve("buffer"),
},
},
})