Implement big text view and preliminary i18n
This commit is contained in:
@ -8,6 +8,7 @@
|
||||
"build:docs": "typedoc --out docs src/index.ts --excludePrivate"
|
||||
},
|
||||
"dependencies": {
|
||||
"buffer": "^6.0.3",
|
||||
"tiny-invariant": "1.2.0",
|
||||
"tslib": "2.3.1"
|
||||
},
|
||||
|
@ -14,11 +14,6 @@ function splitPath(path: string) {
|
||||
export class FileSystem implements IFileSystem {
|
||||
constructor(private handle: FileSystemDirectoryHandle) {}
|
||||
|
||||
static async create() {
|
||||
const handle = await showDirectoryPicker()
|
||||
return new FileSystem(handle)
|
||||
}
|
||||
|
||||
private async getDirectoryHandle(segments: string[]) {
|
||||
if (!segments.length || (segments.length === 1 && !segments[0])) {
|
||||
return this.handle
|
||||
|
53
packages/opvault.js/src/adapters/webkit.ts
Normal file
53
packages/opvault.js/src/adapters/webkit.ts
Normal file
@ -0,0 +1,53 @@
|
||||
import { Buffer } from "buffer"
|
||||
import type { IAdapter, IFileSystem } from "./index"
|
||||
|
||||
export class FileSystem implements IFileSystem {
|
||||
private paths = new Set<string>()
|
||||
private pathMap = new Map<string, File>()
|
||||
|
||||
constructor(list: FileList) {
|
||||
for (const file of list) {
|
||||
this.pathMap.set(file.webkitRelativePath, file)
|
||||
this.paths.add(file.webkitRelativePath)
|
||||
}
|
||||
}
|
||||
|
||||
async readFile(path: string) {
|
||||
return this.pathMap.get(path)!.text()
|
||||
}
|
||||
|
||||
async exists(path: string): Promise<boolean> {
|
||||
return this.pathMap.has(path)
|
||||
}
|
||||
|
||||
async readBuffer(path: string): Promise<Buffer> {
|
||||
const arrayBuffer = await this.pathMap.get(path)!.arrayBuffer()
|
||||
return Buffer.from(arrayBuffer)
|
||||
}
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
async writeFile(path: string, data: string): Promise<void> {
|
||||
throw new Error("fs.writeFile is not supported with webkitdirectory")
|
||||
}
|
||||
|
||||
async readdir(path: string): Promise<string[]> {
|
||||
const paths = [...this.paths]
|
||||
return paths
|
||||
.filter(_ => _.startsWith(`${path}/`))
|
||||
.map(_ => _.slice(path.length + 1))
|
||||
.map(_ => _.split("/")[0])
|
||||
}
|
||||
|
||||
async isDirectory(path: string) {
|
||||
const paths = [...this.paths]
|
||||
return paths.some(_ => _.startsWith(`${path}/`)) && !paths.includes(path)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Default Browser adapter.
|
||||
*/
|
||||
export const getBrowserAdapter = (list: FileList): IAdapter => ({
|
||||
fs: new FileSystem(list),
|
||||
subtle: crypto.subtle,
|
||||
})
|
1
packages/web/.gitignore
vendored
1
packages/web/.gitignore
vendored
@ -3,3 +3,4 @@ node_modules
|
||||
dist
|
||||
bundle
|
||||
*.local
|
||||
*.yml.d.ts
|
||||
|
@ -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
263
packages/web/logo.svg
Normal 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 |
@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
21
packages/web/scripts/build-i18n-yml-typedef.js
Executable file
21
packages/web/scripts/build-i18n-yml-typedef.js
Executable 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;
|
||||
`
|
||||
)
|
@ -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
|
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
|
||||
|
@ -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"),
|
||||
},
|
||||
},
|
||||
})
|
||||
|
Reference in New Issue
Block a user