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

@ -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