Implement big text view and preliminary i18n
This commit is contained in:
51
packages/web/src/components/BigTextView.tsx
Normal file
51
packages/web/src/components/BigTextView.tsx
Normal 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>
|
||||
)
|
||||
})
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
@ -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>
|
||||
}
|
||||
|
@ -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"),
|
||||
|
73
packages/web/src/i18n/index.tsx
Normal file
73
packages/web/src/i18n/index.tsx
Normal 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>
|
||||
})
|
28
packages/web/src/i18n/texts.yml
Normal file
28
packages/web/src/i18n/texts.yml
Normal 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 n’est 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
|
@ -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")
|
||||
)
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
@ -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>
|
||||
)
|
||||
|
@ -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
|
||||
|
Reference in New Issue
Block a user