mirror of
https://github.com/cupcakearmy/cryptgeon.git
synced 2026-06-10 11:16:48 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7e70c81d81 |
@@ -10,10 +10,10 @@ jobs:
|
|||||||
cli:
|
cli:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
- uses: pnpm/action-setup@v6
|
- uses: pnpm/action-setup@v2
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
node-version-file: '.nvmrc'
|
node-version-file: '.nvmrc'
|
||||||
@@ -31,14 +31,14 @@ jobs:
|
|||||||
docker:
|
docker:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v3
|
||||||
- uses: docker/setup-qemu-action@v4
|
- uses: docker/setup-qemu-action@v2
|
||||||
- uses: docker/setup-buildx-action@v4
|
- uses: docker/setup-buildx-action@v2
|
||||||
with:
|
with:
|
||||||
install: true
|
install: true
|
||||||
- name: Docker Labels
|
- name: Docker Labels
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v6
|
uses: docker/metadata-action@v4
|
||||||
with:
|
with:
|
||||||
images: cupcakearmy/cryptgeon
|
images: cupcakearmy/cryptgeon
|
||||||
tags: |
|
tags: |
|
||||||
@@ -46,12 +46,12 @@ jobs:
|
|||||||
type=semver,pattern={{major}}.{{minor}}
|
type=semver,pattern={{major}}.{{minor}}
|
||||||
type=semver,pattern={{major}}
|
type=semver,pattern={{major}}
|
||||||
- name: Login to DockerHub
|
- name: Login to DockerHub
|
||||||
uses: docker/login-action@v4
|
uses: docker/login-action@v2
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
- name: Build and push
|
- name: Build and push
|
||||||
uses: docker/build-push-action@v7
|
uses: docker/build-push-action@v4
|
||||||
with:
|
with:
|
||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
push: true
|
push: true
|
||||||
|
|||||||
@@ -13,15 +13,15 @@ jobs:
|
|||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
# Node
|
# Node
|
||||||
- uses: pnpm/action-setup@v6
|
- uses: pnpm/action-setup@v4
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
node-version-file: '.nvmrc'
|
node-version-file: '.nvmrc'
|
||||||
|
|
||||||
# Docker
|
# Docker
|
||||||
- uses: docker/setup-qemu-action@v4
|
- uses: docker/setup-qemu-action@v3
|
||||||
- uses: docker/setup-buildx-action@v4
|
- uses: docker/setup-buildx-action@v3
|
||||||
with:
|
with:
|
||||||
install: true
|
install: true
|
||||||
|
|
||||||
|
|||||||
@@ -1,47 +0,0 @@
|
|||||||
# Contributing
|
|
||||||
|
|
||||||
## Requirements
|
|
||||||
|
|
||||||
- [mise](https://mise.jdx.dev) — manages pnpm, rust, node (see `mise.toml`)
|
|
||||||
- docker or [colima](https://github.com/abiosoft/colima) (for redis)
|
|
||||||
|
|
||||||
## Setup
|
|
||||||
|
|
||||||
```bash
|
|
||||||
mise install
|
|
||||||
pnpm install
|
|
||||||
```
|
|
||||||
|
|
||||||
## Development
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pnpm run dev
|
|
||||||
```
|
|
||||||
|
|
||||||
Make sure docker/colima is running. This starts redis, the rust backend, the web client, and the CLI. The app is at [localhost:3000](http://localhost:3000).
|
|
||||||
|
|
||||||
## Tests
|
|
||||||
|
|
||||||
End-to-end tests with Playwright.
|
|
||||||
|
|
||||||
```sh
|
|
||||||
pnpm run test:prepare
|
|
||||||
pnpm run test:local
|
|
||||||
```
|
|
||||||
|
|
||||||
## Release
|
|
||||||
|
|
||||||
1. Update version across packages:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
./version.mjs <semver>
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Create and push the tag:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
git tag v<semver>
|
|
||||||
git push --tags
|
|
||||||
```
|
|
||||||
|
|
||||||
The CI workflow publishes the CLI to npm and the Docker image to Docker Hub automatically.
|
|
||||||
+3
-3
@@ -1,5 +1,5 @@
|
|||||||
# FRONTEND
|
# FRONTEND
|
||||||
FROM node:24-alpine AS client
|
FROM node:22-alpine as client
|
||||||
ENV PNPM_HOME="/pnpm"
|
ENV PNPM_HOME="/pnpm"
|
||||||
ENV PATH="$PNPM_HOME:$PATH"
|
ENV PATH="$PNPM_HOME:$PATH"
|
||||||
RUN corepack enable
|
RUN corepack enable
|
||||||
@@ -11,7 +11,7 @@ RUN pnpm run build
|
|||||||
|
|
||||||
|
|
||||||
# BACKEND
|
# BACKEND
|
||||||
FROM rust:1.95-alpine AS backend
|
FROM rust:1.85-alpine as backend
|
||||||
WORKDIR /tmp
|
WORKDIR /tmp
|
||||||
RUN apk add --no-cache libc-dev openssl-dev alpine-sdk
|
RUN apk add --no-cache libc-dev openssl-dev alpine-sdk
|
||||||
COPY ./packages/backend ./
|
COPY ./packages/backend ./
|
||||||
@@ -19,7 +19,7 @@ RUN RUSTFLAGS="-Ctarget-feature=-crt-static" cargo build --release
|
|||||||
|
|
||||||
|
|
||||||
# RUNNER
|
# RUNNER
|
||||||
FROM alpine:3
|
FROM alpine:3.19
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
RUN apk add --no-cache curl libgcc
|
RUN apk add --no-cache curl libgcc
|
||||||
COPY --from=backend /tmp/target/release/cryptgeon .
|
COPY --from=backend /tmp/target/release/cryptgeon .
|
||||||
|
|||||||
@@ -86,7 +86,6 @@ of the notes even if it tried to.
|
|||||||
| `THEME_PAGE_TITLE` | `""` | Custom text the page title |
|
| `THEME_PAGE_TITLE` | `""` | Custom text the page title |
|
||||||
| `THEME_FAVICON` | `""` | Custom url for the favicon. Must be publicly reachable |
|
| `THEME_FAVICON` | `""` | Custom url for the favicon. Must be publicly reachable |
|
||||||
| `THEME_NEW_NOTE_NOTICE` | `true` | Show the message about how notes are stored in the memory and may be evicted after creating a new note. Defaults to `true`. |
|
| `THEME_NEW_NOTE_NOTICE` | `true` | Show the message about how notes are stored in the memory and may be evicted after creating a new note. Defaults to `true`. |
|
||||||
| `THEME_HOME_LINK` | `true` | Show the `/home` link in the footer. Defaults to `true`. |
|
|
||||||
| `IMPRINT_URL` | `""` | Custom url for an Imprint hosted somewhere else. Must be publicly reachable. Takes precedence above `IMPRINT_HTML`. |
|
| `IMPRINT_URL` | `""` | Custom url for an Imprint hosted somewhere else. Must be publicly reachable. Takes precedence above `IMPRINT_HTML`. |
|
||||||
| `IMPRINT_HTML` | `""` | Alternative to `IMPRINT_URL`, this can be used to specify the HTML code to show on `/imprint`. Only `IMPRINT_HTML` or `IMPRINT_URL` should be specified, not both. |
|
| `IMPRINT_HTML` | `""` | Alternative to `IMPRINT_URL`, this can be used to specify the HTML code to show on `/imprint`. Only `IMPRINT_HTML` or `IMPRINT_URL` should be specified, not both. |
|
||||||
## Deployment
|
## Deployment
|
||||||
@@ -106,12 +105,12 @@ version: '3.8'
|
|||||||
|
|
||||||
services:
|
services:
|
||||||
redis:
|
redis:
|
||||||
image: redis:7-alpine
|
image: valkey/valkey:7-alpine
|
||||||
# This is required to stay in RAM only.
|
# This is required to stay in RAM only.
|
||||||
command: redis-server --save "" --appendonly no
|
command: valkey-server --save "" --appendonly no
|
||||||
# Set a size limit. See link below on how to customise.
|
# Set a size limit. See link below on how to customise.
|
||||||
# https://redis.io/docs/latest/operate/rs/databases/memory-performance/eviction-policy/
|
# https://valkey.io/topics/lru-cache/
|
||||||
# --maxmemory 1gb --maxmemory-policy allkeys-lrulpine
|
# --maxmemory 1gb --maxmemory-policy allkeys-lru
|
||||||
# This prevents the creation of an anonymous volume.
|
# This prevents the creation of an anonymous volume.
|
||||||
tmpfs:
|
tmpfs:
|
||||||
- /data
|
- /data
|
||||||
@@ -163,9 +162,53 @@ There is a [guide](https://mariushosting.com/how-to-install-cryptgeon-on-your-sy
|
|||||||
- Italian by [@nicfab](https://notes.nicfab.eu/it/posts/cryptgeon/)
|
- Italian by [@nicfab](https://notes.nicfab.eu/it/posts/cryptgeon/)
|
||||||
- English by [@nicfab](https://notes.nicfab.eu/en/posts/cryptgeon/)
|
- English by [@nicfab](https://notes.nicfab.eu/en/posts/cryptgeon/)
|
||||||
|
|
||||||
## Contributing
|
## Development
|
||||||
|
|
||||||
See [CONTRIBUTING.md](./CONTRIBUTING.md).
|
**Requirements**
|
||||||
|
|
||||||
|
- `pnpm`: `>=9`
|
||||||
|
- `node`: `>=22`
|
||||||
|
- `rust`: edition `2021`
|
||||||
|
|
||||||
|
**Install**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm install
|
||||||
|
|
||||||
|
# Also you need cargo watch if you don't already have it installed.
|
||||||
|
# https://lib.rs/crates/cargo-watch
|
||||||
|
cargo install cargo-watch
|
||||||
|
```
|
||||||
|
|
||||||
|
**Run**
|
||||||
|
|
||||||
|
Make sure you have docker running.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Running `pnpm run dev` in the root folder will start the following things:
|
||||||
|
|
||||||
|
- valkey docker container
|
||||||
|
- rust backend
|
||||||
|
- client
|
||||||
|
- cli
|
||||||
|
|
||||||
|
You can see the app under [localhost:3000](http://localhost:3000).
|
||||||
|
|
||||||
|
> There is a Postman collection with some example requests [available in the repo](./Cryptgeon.postman_collection.json)
|
||||||
|
|
||||||
|
### Tests
|
||||||
|
|
||||||
|
Tests are end to end tests written with Playwright.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
pnpm run test:prepare
|
||||||
|
|
||||||
|
# Use the test or test:local script. The local version only runs in one browser for quicker development.
|
||||||
|
pnpm run test:local
|
||||||
|
```
|
||||||
|
|
||||||
## Security
|
## Security
|
||||||
|
|
||||||
|
|||||||
+56
-13
@@ -43,7 +43,7 @@ Puedes revisar la documentación sobre el CLI en este [readme](./packages/cli/RE
|
|||||||
|
|
||||||
- enviar texto o archivos
|
- enviar texto o archivos
|
||||||
- el servidor no puede desencriptar el contenido debido a que la encriptación se hace del lado del cliente
|
- el servidor no puede desencriptar el contenido debido a que la encriptación se hace del lado del cliente
|
||||||
- restricción de vistas o de tiempo
|
- restriccion de vistas o de tiempo
|
||||||
- en memoria, sin persistencia
|
- en memoria, sin persistencia
|
||||||
- compatibilidad obligatoria con el modo oscuro
|
- compatibilidad obligatoria con el modo oscuro
|
||||||
|
|
||||||
@@ -66,37 +66,36 @@ se usa para guardar y recuperar la nota. Después la nota es encriptada con la <
|
|||||||
| `MAX_VIEWS` | `100` | Número máximo de vistas. |
|
| `MAX_VIEWS` | `100` | Número máximo de vistas. |
|
||||||
| `MAX_EXPIRATION` | `360` | Tiempo máximo de expiración en minutos. |
|
| `MAX_EXPIRATION` | `360` | Tiempo máximo de expiración en minutos. |
|
||||||
| `ALLOW_ADVANCED` | `true` | Permitir configuración personalizada. Si se establece en `false` todas las notas serán de una sola vista. |
|
| `ALLOW_ADVANCED` | `true` | Permitir configuración personalizada. Si se establece en `false` todas las notas serán de una sola vista. |
|
||||||
| `ID_LENGTH` | `32` | Establece el tamaño en bytes de la `id` de la nota. Por defecto es de `32` bytes. Esto es útil para reducir el tamaño del link. _Esta configuración no afecta el nivel de encriptación_. |
|
| `ID_LENGTH` | `32` | Establece el tamaño en bytes de la `id` de la nota. Por defecto es de `32` bytes. Esto es util para reducir el tamaño del link. _Esta configuración no afecta el nivel de encriptación_. |
|
||||||
| `VERBOSITY` | `warn` | Nivel de verbosidad del backend. [Posibles valores](https://docs.rs/env_logger/latest/env_logger/#enabling-logging): `error`, `warn`, `info`, `debug`, `trace` |
|
| `VERBOSITY` | `warn` | Nivel de verbosidad del backend. [Posibles valores](https://docs.rs/env_logger/latest/env_logger/#enabling-logging): `error`, `warn`, `info`, `debug`, `trace` |
|
||||||
| `THEME_IMAGE` | `""` | Imagen personalizada para reemplazar el logo. Debe ser accesible públicamente. |
|
| `THEME_IMAGE` | `""` | Imagen personalizada para reemplazar el logo. Debe ser accesible públicamente. |
|
||||||
| `THEME_TEXT` | `""` | Texto personalizado para reemplazar la descripción bajo el logo. |
|
| `THEME_TEXT` | `""` | Texto personalizado para reemplazar la descripción bajo el logo. |
|
||||||
| `THEME_PAGE_TITLE` | `""` | Texto personalizado para el título |
|
| `THEME_PAGE_TITLE` | `""` | Texto personalizado para el título |
|
||||||
| `THEME_FAVICON` | `""` | Url personalizada para el favicon. Debe ser accesible públicamente. |
|
| `THEME_FAVICON` | `""` | Url personalizada para el favicon. Debe ser accesible públicamente. |
|
||||||
| `THEME_HOME_LINK` | `true` | Mostrar el enlace `/home` en el pie de página. El valor predeterminado es `true`. |
|
|
||||||
|
|
||||||
## Despliegue
|
## Despliegue
|
||||||
|
|
||||||
> ℹ️ Se requiere `https` de lo contrario el navegador no soportará las funciones de encriptación.
|
> ℹ️ Se requiere `https` de lo contrario el navegador no soportará las funciones de encriptacón.
|
||||||
|
|
||||||
> ℹ️ Hay un endpoint para verificar el estado, lo encontramos en `/api/health/`. Regresa un código 200 o 503.
|
> ℹ️ Hay un endpoint para verificar el estado, lo encontramos en `/api/health/`. Regresa un código 200 o 503.
|
||||||
|
|
||||||
### Docker
|
### Docker
|
||||||
|
|
||||||
Docker es la manera más fácil. Aquí encontramos [la imagen oficial](https://hub.docker.com/r/cupcakearmy/cryptgeon).
|
Docker es la manera más fácil. Aquí encontramos [la imágen oficial](https://hub.docker.com/r/cupcakearmy/cryptgeon).
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
# docker-compose.yml
|
# docker-compose.yml
|
||||||
|
|
||||||
version: "3.8"
|
version: '3.8'
|
||||||
|
|
||||||
services:
|
services:
|
||||||
redis:
|
redis:
|
||||||
image: redis:7-alpine
|
image: valkey/valkey:7-alpine
|
||||||
# This is required to stay in RAM only.
|
# This is required to stay in RAM only.
|
||||||
command: redis-server --save "" --appendonly no
|
command: valkey-server --save "" --appendonly no
|
||||||
# Set a size limit. See link below on how to customise.
|
# Set a size limit. See link below on how to customise.
|
||||||
# https://redis.io/docs/latest/operate/rs/databases/memory-performance/eviction-policy/
|
# https://valkey.io/topics/lru-cache/
|
||||||
# --maxmemory 1gb --maxmemory-policy allkeys-lrulpine
|
# --maxmemory 1gb --maxmemory-policy allkeys-lru
|
||||||
# This prevents the creation of an anonymous volume.
|
# This prevents the creation of an anonymous volume.
|
||||||
tmpfs:
|
tmpfs:
|
||||||
- /data
|
- /data
|
||||||
@@ -142,13 +141,57 @@ Hay una [guía](https://mariushosting.com/how-to-install-cryptgeon-on-your-synol
|
|||||||
- En inglés, por [DB Tech](https://www.youtube.com/watch?v=S0jx7wpOfNM) [Previous Video](https://www.youtube.com/watch?v=JhpIatD06vE)
|
- En inglés, por [DB Tech](https://www.youtube.com/watch?v=S0jx7wpOfNM) [Previous Video](https://www.youtube.com/watch?v=JhpIatD06vE)
|
||||||
- En alemán, por [ApfelCast](https://www.youtube.com/watch?v=84ZMbE9AkHg)
|
- En alemán, por [ApfelCast](https://www.youtube.com/watch?v=84ZMbE9AkHg)
|
||||||
|
|
||||||
## Contribuir
|
## Desarrollo
|
||||||
|
|
||||||
Ver [CONTRIBUTING.md](./CONTRIBUTING.md).
|
**Requisitos**
|
||||||
|
|
||||||
|
- `pnpm`: `>=6`
|
||||||
|
- `node`: `>=18`
|
||||||
|
- `rust`: edition `2021`
|
||||||
|
|
||||||
|
**Instalación**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm install
|
||||||
|
|
||||||
|
# También necesitas cargo-watch, si no lo tienes instalado.
|
||||||
|
# https://lib.rs/crates/cargo-watch
|
||||||
|
cargo install cargo-watch
|
||||||
|
```
|
||||||
|
|
||||||
|
**Ejecutar**
|
||||||
|
|
||||||
|
Asegurate de que docker se esté ejecutando.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Ejecutando `pnpm run dev` en la carpeta raíz iniciará lo siguiente:
|
||||||
|
|
||||||
|
- redis docker container
|
||||||
|
- rust backend
|
||||||
|
- client
|
||||||
|
- cli
|
||||||
|
|
||||||
|
Puedes ver la app en [localhost:3000](http://localhost:3000).
|
||||||
|
|
||||||
|
> Existe una colección de Postman con algunas peticiones de ejemplo [disponible en el repo](./Cryptgeon.postman_collection.json)
|
||||||
|
|
||||||
|
### Tests
|
||||||
|
|
||||||
|
Los tests son end-to-end tests escritos con Playwright.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
pnpm run test:prepare
|
||||||
|
|
||||||
|
# Usa el script test o test:local. La versión local solo corre en el navegador para acelerar el desarrollo.
|
||||||
|
pnpm run test:local
|
||||||
|
```
|
||||||
|
|
||||||
## Seguridad
|
## Seguridad
|
||||||
|
|
||||||
Por favor dirígete a la sección de seguridad [aquí](./SECURITY.md).
|
Por favor dirigite a la sección de seguridad [aquí](./SECURITY.md).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
+59
-15
@@ -48,15 +48,14 @@ _加密鸽_ 是一个受 [_PrivNote_](https://privnote.com)项目启发的安全
|
|||||||
## 环境变量
|
## 环境变量
|
||||||
|
|
||||||
| 变量名称 | 默认值 | 描述 |
|
| 变量名称 | 默认值 | 描述 |
|
||||||
| ---------------- | ---------------- | --------------------------------------------------------------------------------- |
|
| ----------------- | ---------------- | --------------------------------------------------------------------------------- |
|
||||||
| `REDIS` | `redis://redis/` | Redis 连接 URL。 |
|
| `REDIS` | `redis://redis/` | Redis 连接 URL。 |
|
||||||
| `SIZE_LIMIT` | `1 KiB` | 最大请求体(body)限制。有关支持的数值请查看 [字节单位](https://docs.rs/byte-unit/) |
|
| `SIZE_LIMIT` | `1 KiB` | 最大请求体(body)限制。有关支持的数值请查看 [字节单位](https://docs.rs/byte-unit/) |
|
||||||
| `MAX_VIEWS` | `100` | 密信最多查看次数限制 |
|
| `MAX_VIEWS` | `100` | 密信最多查看次数限制 |
|
||||||
| `MAX_EXPIRATION` | `360` | 密信最长过期时间限制(分钟) |
|
| ` MAX_EXPIRATION` | `360` | 密信最长过期时间限制(分钟) |
|
||||||
| `ALLOW_ADVANCED` | `true` | 是否允许自定义设置,该项如果设为`false`,则不会显示自定义设置模块 |
|
| `ALLOW_ADVANCED` | `true` | 是否允许自定义设置,该项如果设为`false`,则不会显示自定义设置模块 |
|
||||||
| `THEME_IMAGE` | `""` | 自定义 Logo 图片,你在这里填写的的图片链接必须是可以公开访问的。 |
|
| `THEME_IMAGE` | `""` | 自定义 Logo 图片,你在这里填写的的图片链接必须是可以公开访问的。 |
|
||||||
| `THEME_TEXT` | `""` | 自定义在 Logo 下方的文本。 |
|
| `THEME_TEXT` | `""` | 自定义在 Logo 下方的文本。 |
|
||||||
| `THEME_HOME_LINK` | `true` | 是否在页脚显示 `/home` 链接。默认为 `true`。 |
|
|
||||||
|
|
||||||
## 部署
|
## 部署
|
||||||
|
|
||||||
@@ -70,16 +69,16 @@ Docker 是最简单的部署方式。这里是[官方镜像的地址](https://hu
|
|||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
# docker-compose.yml
|
# docker-compose.yml
|
||||||
version: "3.8"
|
version: '3.8'
|
||||||
|
|
||||||
services:
|
services:
|
||||||
redis:
|
redis:
|
||||||
image: redis:7-alpine
|
image: valkey/valkey:7-alpine
|
||||||
# This is required to stay in RAM only.
|
# This is required to stay in RAM only.
|
||||||
command: redis-server --save "" --appendonly no
|
command: valkey-server --save "" --appendonly no
|
||||||
# Set a size limit. See link below on how to customise.
|
# Set a size limit. See link below on how to customise.
|
||||||
# https://redis.io/docs/latest/operate/rs/databases/memory-performance/eviction-policy/
|
# https://valkey.io/topics/lru-cache/
|
||||||
# --maxmemory 1gb --maxmemory-policy allkeys-lrulpine
|
# --maxmemory 1gb --maxmemory-policy allkeys-lru
|
||||||
# This prevents the creation of an anonymous volume.
|
# This prevents the creation of an anonymous volume.
|
||||||
tmpfs:
|
tmpfs:
|
||||||
- /data
|
- /data
|
||||||
@@ -108,7 +107,7 @@ services:
|
|||||||
- 域名 `example.org`
|
- 域名 `example.org`
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
version: "3.8"
|
version: '3.8'
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
proxy:
|
proxy:
|
||||||
@@ -116,12 +115,12 @@ networks:
|
|||||||
|
|
||||||
services:
|
services:
|
||||||
redis:
|
redis:
|
||||||
image: redis:7-alpine
|
image: valkey/valkey:7-alpine
|
||||||
# This is required to stay in RAM only.
|
# This is required to stay in RAM only.
|
||||||
command: redis-server --save "" --appendonly no
|
command: valkey-server --save "" --appendonly no
|
||||||
# Set a size limit. See link below on how to customise.
|
# Set a size limit. See link below on how to customise.
|
||||||
# https://redis.io/docs/latest/operate/rs/databases/memory-performance/eviction-policy/
|
# https://valkey.io/topics/lru-cache/
|
||||||
# --maxmemory 1gb --maxmemory-policy allkeys-lrulpine
|
# --maxmemory 1gb --maxmemory-policy allkeys-lru
|
||||||
# This prevents the creation of an anonymous volume.
|
# This prevents the creation of an anonymous volume.
|
||||||
tmpfs:
|
tmpfs:
|
||||||
- /data
|
- /data
|
||||||
@@ -141,9 +140,54 @@ services:
|
|||||||
- traefik.http.routers.cryptgeon.tls.certresolver=le
|
- traefik.http.routers.cryptgeon.tls.certresolver=le
|
||||||
```
|
```
|
||||||
|
|
||||||
## 贡献
|
## 开发
|
||||||
|
|
||||||
参见 [CONTRIBUTING.md](./CONTRIBUTING.md)。
|
**环境要求**
|
||||||
|
|
||||||
|
- `pnpm`: `>=6`
|
||||||
|
- `node`: `>=14`
|
||||||
|
- `rust`: edition `2021`
|
||||||
|
|
||||||
|
**安装**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm install
|
||||||
|
pnpm --prefix frontend install
|
||||||
|
|
||||||
|
# 你还需要安装CargoWatch.
|
||||||
|
# https://lib.rs/crates/cargo-watch
|
||||||
|
cargo install cargo-watch
|
||||||
|
```
|
||||||
|
|
||||||
|
**运行**
|
||||||
|
|
||||||
|
确保你的 Docker 正在运行
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
在根目录执行 `pnpm run dev` 会开启下列服务:
|
||||||
|
|
||||||
|
- 一个 redis docker 容器
|
||||||
|
- 无热重载的 rust 后端
|
||||||
|
- 可热重载的客户端
|
||||||
|
|
||||||
|
你可以通过 3000 端口进入该应用,即 [localhost:3000](http://localhost:3000).
|
||||||
|
|
||||||
|
## 测试
|
||||||
|
|
||||||
|
这些测试是用 Playwright 实现的一些端到端测试用例。
|
||||||
|
|
||||||
|
```sh
|
||||||
|
pnpm run test:prepare
|
||||||
|
docker compose up redis -d
|
||||||
|
pnpm run test:server
|
||||||
|
|
||||||
|
# 在另一个终端中:
|
||||||
|
# 使用test或者test:local script。为了更快的开发,本地版本只会在一个浏览器中运行。
|
||||||
|
pnpm run test:local
|
||||||
|
```
|
||||||
|
|
||||||
###### Attributions
|
###### Attributions
|
||||||
|
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ services:
|
|||||||
# This is required to stay in RAM only.
|
# This is required to stay in RAM only.
|
||||||
command: valkey-server --save "" --appendonly no
|
command: valkey-server --save "" --appendonly no
|
||||||
# Set a size limit. See link below on how to customise.
|
# Set a size limit. See link below on how to customise.
|
||||||
# https://valkey.io/docs/latest/operate/rs/databases/memory-performance/eviction-policy/
|
# https://valkey.io/topics/lru-cache/
|
||||||
# --maxmemory 1gb --maxmemory-policy allkeys-lrulpine
|
# --maxmemory 1gb --maxmemory-policy allkeys-lru
|
||||||
# This prevents the creation of an anonymous volume.
|
# This prevents the creation of an anonymous volume.
|
||||||
tmpfs:
|
tmpfs:
|
||||||
- /data
|
- /data
|
||||||
@@ -25,7 +25,7 @@ services:
|
|||||||
- 3000:8000
|
- 3000:8000
|
||||||
|
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ['CMD', 'curl', '--fail', 'http://127.0.0.1:8000/api/live/']
|
test: ["CMD", "curl", "--fail", "http://127.0.0.1:8000/api/live/"]
|
||||||
interval: 1m
|
interval: 1m
|
||||||
timeout: 3s
|
timeout: 3s
|
||||||
retries: 2
|
retries: 2
|
||||||
|
|||||||
+2
-2
@@ -4,8 +4,8 @@ services:
|
|||||||
# This is required to stay in RAM only.
|
# This is required to stay in RAM only.
|
||||||
command: valkey-server --save "" --appendonly no
|
command: valkey-server --save "" --appendonly no
|
||||||
# Set a size limit. See link below on how to customise.
|
# Set a size limit. See link below on how to customise.
|
||||||
# https://valkey.io/docs/latest/operate/rs/databases/memory-performance/eviction-policy/
|
# https://valkey.io/topics/lru-cache/
|
||||||
# --maxmemory 1gb --maxmemory-policy allkeys-lrulpine
|
# --maxmemory 1gb --maxmemory-policy allkeys-lru
|
||||||
# This prevents the creation of an anonymous volume.
|
# This prevents the creation of an anonymous volume.
|
||||||
tmpfs:
|
tmpfs:
|
||||||
- /data
|
- /data
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
[tools]
|
|
||||||
pnpm = "11.5.0"
|
|
||||||
rust = "1.95"
|
|
||||||
watchexec = "latest"
|
|
||||||
# Node loaded below from .nvmrc
|
|
||||||
|
|
||||||
[settings]
|
|
||||||
idiomatic_version_file_enable_tools = ["node"]
|
|
||||||
|
|
||||||
[env]
|
|
||||||
SIZE_LIMIT = "100mb"
|
|
||||||
+4
-5
@@ -8,15 +8,14 @@
|
|||||||
"test": "playwright test --project=chrome --project=firefox --project=safari",
|
"test": "playwright test --project=chrome --project=firefox --project=safari",
|
||||||
"test:local": "playwright test --project=chrome",
|
"test:local": "playwright test --project=chrome",
|
||||||
"test:server": "run-s docker:up",
|
"test:server": "run-s docker:up",
|
||||||
"test:dl-browsers": "playwright install",
|
"test:prepare": "run-p build docker:build",
|
||||||
"test:prepare": "run-p test:dl-browsers build docker:build",
|
|
||||||
"build": "pnpm run --recursive --filter=!@cryptgeon/backend build"
|
"build": "pnpm run --recursive --filter=!@cryptgeon/backend build"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@playwright/test": "^1.60.0",
|
"@playwright/test": "^1.46.1",
|
||||||
"@types/node": "^24.12.4",
|
"@types/node": "^22.5.0",
|
||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
"shelljs": "^0.8.5"
|
"shelljs": "^0.8.5"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@11.5.0"
|
"packageManager": "pnpm@10.3.0"
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+422
-649
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,9 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "cryptgeon"
|
name = "cryptgeon"
|
||||||
version = "2.9.2"
|
version = "2.9.1"
|
||||||
authors = ["cupcakearmy <hi@nicco.io>"]
|
authors = ["cupcakearmy <hi@nicco.io>"]
|
||||||
edition = "2024"
|
edition = "2021"
|
||||||
rust-version = "1.95"
|
rust-version = "1.85"
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "cryptgeon"
|
name = "cryptgeon"
|
||||||
@@ -11,12 +11,12 @@ path = "src/main.rs"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
# Core
|
# Core
|
||||||
axum = "0.8"
|
axum = "0.7.5"
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1.0.208", features = ["derive"] }
|
||||||
tokio = { version = "1", features = ["full"] }
|
tokio = { version = "1.39.3", features = ["full"] }
|
||||||
tower = "0.5"
|
tower = "0.5.0"
|
||||||
tower-http = { version = "0.6", features = ["full"] }
|
tower-http = { version = "0.5.2", features = ["full"] }
|
||||||
redis = { version = "1", features = ["tls-native-tls"] }
|
redis = { version = "0.25.2", features = ["tls-native-tls"] }
|
||||||
|
|
||||||
# Utility
|
# Utility
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"name": "@cryptgeon/backend",
|
"name": "@cryptgeon/backend",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "watchexec -r -e rs cargo run",
|
"dev": "cargo watch -x 'run --bin cryptgeon'",
|
||||||
"build": "cargo build --release",
|
"build": "cargo build --release",
|
||||||
"test:server": "SIZE_LIMIT=10MiB LISTEN_ADDR=0.0.0.0:3000 cargo run",
|
"test:server": "SIZE_LIMIT=10MiB LISTEN_ADDR=0.0.0.0:3000 cargo run",
|
||||||
"test:prepare": "cargo build"
|
"test:prepare": "cargo build"
|
||||||
|
|||||||
@@ -70,8 +70,4 @@ lazy_static! {
|
|||||||
.unwrap_or("true".to_string())
|
.unwrap_or("true".to_string())
|
||||||
.parse()
|
.parse()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
pub static ref THEME_HOME_LINK: bool = std::env::var("THEME_HOME_LINK")
|
|
||||||
.unwrap_or("true".to_string())
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
use std::{collections::HashMap, sync::Arc};
|
use std::{collections::HashMap, sync::Arc};
|
||||||
|
|
||||||
use axum::{
|
use axum::{
|
||||||
Router, ServiceExt,
|
|
||||||
extract::{DefaultBodyLimit, Request},
|
extract::{DefaultBodyLimit, Request},
|
||||||
routing::{delete, get, post},
|
routing::{delete, get, post},
|
||||||
|
Router, ServiceExt,
|
||||||
};
|
};
|
||||||
use dotenv::dotenv;
|
use dotenv::dotenv;
|
||||||
use lock::SharedState;
|
use lock::SharedState;
|
||||||
@@ -41,14 +41,14 @@ async fn main() {
|
|||||||
|
|
||||||
let notes_routes = Router::new()
|
let notes_routes = Router::new()
|
||||||
.route("/", post(note::create))
|
.route("/", post(note::create))
|
||||||
.route("/{id}", delete(note::delete))
|
.route("/:id", delete(note::delete))
|
||||||
.route("/{id}", get(note::preview));
|
.route("/:id", get(note::preview));
|
||||||
let health_routes = Router::new().route("/live", get(health::report_health));
|
let health_routes = Router::new().route("/live", get(health::report_health));
|
||||||
let status_routes = Router::new().route("/status", get(status::get_status));
|
let status_routes = Router::new().route("/status", get(status::get_status));
|
||||||
let api_routes = Router::new()
|
let api_routes = Router::new()
|
||||||
.nest("/notes", notes_routes)
|
.nest("/notes", notes_routes)
|
||||||
.merge(health_routes)
|
.nest("/", health_routes)
|
||||||
.merge(status_routes);
|
.nest("/", status_routes);
|
||||||
|
|
||||||
let index = format!("{}{}", config::FRONTEND_PATH.to_string(), "/index.html");
|
let index = format!("{}{}", config::FRONTEND_PATH.to_string(), "/index.html");
|
||||||
let serve_dir =
|
let serve_dir =
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ pub struct Status {
|
|||||||
pub theme_page_title: String,
|
pub theme_page_title: String,
|
||||||
pub theme_favicon: String,
|
pub theme_favicon: String,
|
||||||
pub theme_new_note_notice: bool,
|
pub theme_new_note_notice: bool,
|
||||||
pub theme_home_link: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_status() -> (StatusCode, Json<Status>) {
|
pub async fn get_status() -> (StatusCode, Json<Status>) {
|
||||||
@@ -34,7 +33,6 @@ pub async fn get_status() -> (StatusCode, Json<Status>) {
|
|||||||
imprint_url: config::IMPRINT_URL.to_string(),
|
imprint_url: config::IMPRINT_URL.to_string(),
|
||||||
imprint_html: config::IMPRINT_HTML.to_string(),
|
imprint_html: config::IMPRINT_HTML.to_string(),
|
||||||
theme_new_note_notice: *config::THEME_NEW_NOTE_NOTICE,
|
theme_new_note_notice: *config::THEME_NEW_NOTE_NOTICE,
|
||||||
theme_home_link: *config::THEME_HOME_LINK,
|
|
||||||
theme_image: config::THEME_IMAGE.to_string(),
|
theme_image: config::THEME_IMAGE.to_string(),
|
||||||
theme_text: config::THEME_TEXT.to_string(),
|
theme_text: config::THEME_TEXT.to_string(),
|
||||||
theme_page_title: config::THEME_PAGE_TITLE.to_string(),
|
theme_page_title: config::THEME_PAGE_TITLE.to_string(),
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "cryptgeon",
|
"name": "cryptgeon",
|
||||||
"version": "2.9.2",
|
"version": "2.9.1",
|
||||||
"homepage": "https://github.com/cupcakearmy/cryptgeon",
|
"homepage": "https://github.com/cupcakearmy/cryptgeon",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@@ -30,16 +30,16 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@commander-js/extra-typings": "^12.1.0",
|
"@commander-js/extra-typings": "^12.1.0",
|
||||||
"@types/inquirer": "^9.0.9",
|
"@types/inquirer": "^9.0.7",
|
||||||
"@types/mime": "^4.0.0",
|
"@types/mime": "^4.0.0",
|
||||||
"@types/node": "^20.19.41",
|
"@types/node": "^20.11.24",
|
||||||
"commander": "^12.1.0",
|
"commander": "^12.1.0",
|
||||||
"inquirer": "^9.3.8",
|
"inquirer": "^9.2.15",
|
||||||
"mime": "^4.1.0",
|
"mime": "^4.0.1",
|
||||||
"occulto": "^2.0.6",
|
"occulto": "^2.0.6",
|
||||||
"pretty-bytes": "^6.1.1",
|
"pretty-bytes": "^6.1.1",
|
||||||
"tsup": "^8.5.1",
|
"tsup": "^8.2.4",
|
||||||
"typescript": "^5.9.3"
|
"typescript": "^5.3.3"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
|
|||||||
@@ -120,7 +120,6 @@ export type Status = {
|
|||||||
theme_favicon: string
|
theme_favicon: string
|
||||||
theme_page_title: string
|
theme_page_title: string
|
||||||
theme_new_note_notice: boolean
|
theme_new_note_notice: boolean
|
||||||
theme_home_link: boolean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function status() {
|
async function status() {
|
||||||
|
|||||||
@@ -1,17 +1,8 @@
|
|||||||
├─ MIT: 324
|
├─ MIT: 13
|
||||||
├─ ISC: 84
|
├─ ISC: 2
|
||||||
├─ Apache-2.0: 21
|
├─ BSD-3-Clause: 1
|
||||||
├─ BlueOak-1.0.0: 14
|
├─ (MPL-2.0 OR Apache-2.0): 1
|
||||||
├─ BSD-3-Clause: 6
|
├─ BSD-2-Clause: 1
|
||||||
├─ BSD-2-Clause: 4
|
|
||||||
├─ OFL-1.1: 1
|
|
||||||
├─ Python-2.0: 1
|
|
||||||
├─ CC-BY-4.0: 1
|
|
||||||
├─ UNKNOWN: 1
|
|
||||||
├─ MPL-2.0: 1
|
|
||||||
├─ CC-BY-3.0: 1
|
|
||||||
├─ CC0-1.0: 1
|
|
||||||
├─ (MIT AND CC-BY-3.0): 1
|
|
||||||
├─ 0BSD: 1
|
├─ 0BSD: 1
|
||||||
└─ (MIT OR CC0-1.0): 1
|
└─ Apache-2.0: 1
|
||||||
|
|
||||||
|
|||||||
|
@@ -1,61 +0,0 @@
|
|||||||
{
|
|
||||||
"common": {
|
|
||||||
"note": "poznámka",
|
|
||||||
"file": "soubor",
|
|
||||||
"advanced": "pokročilé",
|
|
||||||
"create": "vytvořit",
|
|
||||||
"loading": "načítání",
|
|
||||||
"mode": "režim",
|
|
||||||
"views": "{n, plural, =0 {zobrazení} =1 {1 zobrazení} other {# zobrazení}}",
|
|
||||||
"minutes": "{n, plural, =0 {minut} =1 {1 minuta} other {# minutách}}",
|
|
||||||
"max": "max",
|
|
||||||
"share_link": "sdílet odkaz",
|
|
||||||
"copy_clipboard": "zkopírovat do schránky",
|
|
||||||
"copied_to_clipboard": "zkopírováno do schránky",
|
|
||||||
"encrypting": "šifrování",
|
|
||||||
"decrypting": "dešifrování",
|
|
||||||
"uploading": "nahrávání",
|
|
||||||
"downloading": "stahování",
|
|
||||||
"qr_code": "QR kód",
|
|
||||||
"password": "heslo"
|
|
||||||
},
|
|
||||||
"home": {
|
|
||||||
"intro": "Jednoduše posílejte <i>plně šifrované</i> a zabezpečené poznámky nebo soubory jediným kliknutím. Stačí vytvořit poznámku a sdílet odkaz.",
|
|
||||||
"explanation": "Poznámka vyprší a bude zničena po {n}.",
|
|
||||||
"new_note": "Nová poznámka",
|
|
||||||
"new_note_notice": "<b>Dostupnost:</b><br />Poznámka není zaručeně uchována, protože je uložena pouze v paměti RAM. Pokud se paměť zaplní, nejstarší poznámky budou automaticky smazány.<br />(Obvykle to nebývá problém, ale je dobré o tom vědět.)",
|
|
||||||
"errors": {
|
|
||||||
"note_to_big": "Poznámku nelze vytvořit. Je příliš velká.",
|
|
||||||
"note_error": "Poznámku nelze vytvořit. Zkuste to prosím znovu.",
|
|
||||||
"max": "Max: {n}",
|
|
||||||
"empty_content": "Poznámka je prázdná."
|
|
||||||
},
|
|
||||||
"messages": {
|
|
||||||
"note_created": "Poznámka byla vytvořena."
|
|
||||||
},
|
|
||||||
"advanced": {
|
|
||||||
"explanation": "Ve výchozím nastavení se pro každou poznámku generuje bezpečné heslo. Pokud chcete, můžete si nastavit vlastní heslo, které nebude součástí odkazu.",
|
|
||||||
"custom_password": "Vlastní heslo"
|
|
||||||
},
|
|
||||||
"pasting": "Vkládání...",
|
|
||||||
"pasted_files": "Vložené soubory",
|
|
||||||
"remove": "Odstranit"
|
|
||||||
},
|
|
||||||
"show": {
|
|
||||||
"errors": {
|
|
||||||
"not_found": "Poznámka nebyla nalezena nebo již byla smazána.",
|
|
||||||
"decryption_failed": "Špatné heslo. Nelze dešifrovat. Odkaz je pravděpodobně poškozen. Poznámka byla zničena.",
|
|
||||||
"unsupported_type": "Nepodporovaný typ poznámky."
|
|
||||||
},
|
|
||||||
"explanation": "Klikněte níže pro zobrazení a následné smazání poznámky, pokud bylo dosaženo limitu zobrazení.",
|
|
||||||
"show_note": "Zobrazit poznámku",
|
|
||||||
"warning_will_not_see_again": "Tuto poznámku již <b>nebudete</b> moci znovu zobrazit.",
|
|
||||||
"download_all": "Stáhnout vše",
|
|
||||||
"links_found": "Odkazy nalezené v poznámce:"
|
|
||||||
},
|
|
||||||
"file_upload": {
|
|
||||||
"selected_files": "Vybrané soubory",
|
|
||||||
"no_files_selected": "Žádné soubory nevybrány",
|
|
||||||
"clear": "Vymazat"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -36,10 +36,7 @@
|
|||||||
"advanced": {
|
"advanced": {
|
||||||
"explanation": "Standardmäßig wird für jede Notiz ein generiertes, sicheres Passwort verwendet. Alternativ können Sie ein eigenes Kennwort festlegen, welches nicht im Link enthalten ist.",
|
"explanation": "Standardmäßig wird für jede Notiz ein generiertes, sicheres Passwort verwendet. Alternativ können Sie ein eigenes Kennwort festlegen, welches nicht im Link enthalten ist.",
|
||||||
"custom_password": "Benutzerdefiniertes Passwort"
|
"custom_password": "Benutzerdefiniertes Passwort"
|
||||||
},
|
}
|
||||||
"pasting": "Einfügen...",
|
|
||||||
"pasted_files": "Eingefügte Dateien",
|
|
||||||
"remove": "Entfernen"
|
|
||||||
},
|
},
|
||||||
"show": {
|
"show": {
|
||||||
"errors": {
|
"errors": {
|
||||||
|
|||||||
@@ -36,10 +36,7 @@
|
|||||||
"advanced": {
|
"advanced": {
|
||||||
"explanation": "By default, a securely generated password is used for each note. You can however also choose your own password, which is not included in the link.",
|
"explanation": "By default, a securely generated password is used for each note. You can however also choose your own password, which is not included in the link.",
|
||||||
"custom_password": "custom password"
|
"custom_password": "custom password"
|
||||||
},
|
}
|
||||||
"pasting": "Pasting...",
|
|
||||||
"pasted_files": "Pasted Files",
|
|
||||||
"remove": "Remove"
|
|
||||||
},
|
},
|
||||||
"show": {
|
"show": {
|
||||||
"errors": {
|
"errors": {
|
||||||
|
|||||||
@@ -36,10 +36,7 @@
|
|||||||
"advanced": {
|
"advanced": {
|
||||||
"explanation": "Por defecto, se utiliza una contraseña generada de forma segura para cada nota. No obstante, también puede elegir su propia contraseña, la cual no se incluye en el enlace.",
|
"explanation": "Por defecto, se utiliza una contraseña generada de forma segura para cada nota. No obstante, también puede elegir su propia contraseña, la cual no se incluye en el enlace.",
|
||||||
"custom_password": "contraseña personalizada"
|
"custom_password": "contraseña personalizada"
|
||||||
},
|
}
|
||||||
"pasting": "Pegando...",
|
|
||||||
"pasted_files": "Archivos pegados",
|
|
||||||
"remove": "Eliminar"
|
|
||||||
},
|
},
|
||||||
"show": {
|
"show": {
|
||||||
"errors": {
|
"errors": {
|
||||||
|
|||||||
@@ -36,10 +36,7 @@
|
|||||||
"advanced": {
|
"advanced": {
|
||||||
"explanation": "Par défaut, un mot de passe généré de manière sécurisée est utilisé pour chaque note. Vous pouvez toutefois choisir votre propre mot de passe, qui n'est pas inclus dans le lien.",
|
"explanation": "Par défaut, un mot de passe généré de manière sécurisée est utilisé pour chaque note. Vous pouvez toutefois choisir votre propre mot de passe, qui n'est pas inclus dans le lien.",
|
||||||
"custom_password": "mot de passe personnalisé"
|
"custom_password": "mot de passe personnalisé"
|
||||||
},
|
}
|
||||||
"pasting": "Collage...",
|
|
||||||
"pasted_files": "Fichiers collés",
|
|
||||||
"remove": "Supprimer"
|
|
||||||
},
|
},
|
||||||
"show": {
|
"show": {
|
||||||
"errors": {
|
"errors": {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
"advanced": "avanzato",
|
"advanced": "avanzato",
|
||||||
"create": "crea",
|
"create": "crea",
|
||||||
"loading": "carica",
|
"loading": "carica",
|
||||||
"mode": "modalità",
|
"mode": "modalita",
|
||||||
"views": "{n, plural, =0 {viste} =1 {1 vista} other {# viste}}",
|
"views": "{n, plural, =0 {viste} =1 {1 vista} other {# viste}}",
|
||||||
"minutes": "{n, plural, =0 {minuti} =1 {1 minuto} other {# minuti}}",
|
"minutes": "{n, plural, =0 {minuti} =1 {1 minuto} other {# minuti}}",
|
||||||
"max": "max",
|
"max": "max",
|
||||||
@@ -36,10 +36,7 @@
|
|||||||
"advanced": {
|
"advanced": {
|
||||||
"explanation": "Per impostazione predefinita, per ogni nota viene utilizzata una password generata in modo sicuro. È tuttavia possibile scegliere la propria password, che non è inclusa nel link.",
|
"explanation": "Per impostazione predefinita, per ogni nota viene utilizzata una password generata in modo sicuro. È tuttavia possibile scegliere la propria password, che non è inclusa nel link.",
|
||||||
"custom_password": "password personalizzata"
|
"custom_password": "password personalizzata"
|
||||||
},
|
}
|
||||||
"pasting": "Incollando...",
|
|
||||||
"pasted_files": "File incollati",
|
|
||||||
"remove": "Rimuovi"
|
|
||||||
},
|
},
|
||||||
"show": {
|
"show": {
|
||||||
"errors": {
|
"errors": {
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
"uploading": "アップロード中",
|
"uploading": "アップロード中",
|
||||||
"downloading": "ダウンロード中",
|
"downloading": "ダウンロード中",
|
||||||
"qr_code": "QRコード",
|
"qr_code": "QRコード",
|
||||||
"password": "パスワード"
|
"password": "暗号"
|
||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"intro": "<i>完全に暗号化された</i> 、安全なメモやファイルをワンクリックで簡単に送信できます。メモを作成してリンクを共有するだけです。",
|
"intro": "<i>完全に暗号化された</i> 、安全なメモやファイルをワンクリックで簡単に送信できます。メモを作成してリンクを共有するだけです。",
|
||||||
@@ -36,10 +36,7 @@
|
|||||||
"advanced": {
|
"advanced": {
|
||||||
"explanation": "デフォルトでは、安全に生成されたパスワードが各ノートに使用されます。しかし、リンクに含まれない独自のパスワードを選択することもできます。",
|
"explanation": "デフォルトでは、安全に生成されたパスワードが各ノートに使用されます。しかし、リンクに含まれない独自のパスワードを選択することもできます。",
|
||||||
"custom_password": "カスタムパスワード"
|
"custom_password": "カスタムパスワード"
|
||||||
},
|
}
|
||||||
"pasting": "貼り付け中...",
|
|
||||||
"pasted_files": "貼り付けたファイル",
|
|
||||||
"remove": "削除"
|
|
||||||
},
|
},
|
||||||
"show": {
|
"show": {
|
||||||
"errors": {
|
"errors": {
|
||||||
@@ -49,7 +46,7 @@
|
|||||||
},
|
},
|
||||||
"explanation": "カウンターが上限に達した場合、ノートの表示と削除を行うには、以下をクリックします。",
|
"explanation": "カウンターが上限に達した場合、ノートの表示と削除を行うには、以下をクリックします。",
|
||||||
"show_note": "メモを表示",
|
"show_note": "メモを表示",
|
||||||
"warning_will_not_see_again": "このノートを再度表示することは<b>できません</b>",
|
"warning_will_not_see_again": "あなた <b>できません</b> このノートをもう一度見る",
|
||||||
"download_all": "すべてダウンロード",
|
"download_all": "すべてダウンロード",
|
||||||
"links_found": "メモ内にあるリンク:"
|
"links_found": "メモ内にあるリンク:"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -6,8 +6,8 @@
|
|||||||
"create": "utwórz",
|
"create": "utwórz",
|
||||||
"loading": "ładowanie",
|
"loading": "ładowanie",
|
||||||
"mode": "tryb",
|
"mode": "tryb",
|
||||||
"views": "{n, plural, =0 {wyświetleń} =1 {1 wyświetlenie} other {{n} wyświetleń}}",
|
"views": "{n, plural, =0 {wyświetleń} =1 {1 wyświetlenie} other {# wyświetleń}}",
|
||||||
"minutes": "{n, plural, =0 {minut} =1 {1 minuta} other {{n} minuty}}",
|
"minutes": "{n, plural, =0 {minut} =1 {1 minuta} other {# minuty}}",
|
||||||
"max": "maks.",
|
"max": "maks.",
|
||||||
"share_link": "link udostępniania",
|
"share_link": "link udostępniania",
|
||||||
"copy_clipboard": "kopiuj do schowka",
|
"copy_clipboard": "kopiuj do schowka",
|
||||||
@@ -27,7 +27,7 @@
|
|||||||
"errors": {
|
"errors": {
|
||||||
"note_to_big": "nie można utworzyć notatki. notatka jest za duża",
|
"note_to_big": "nie można utworzyć notatki. notatka jest za duża",
|
||||||
"note_error": "nie można utworzyć notatki. spróbuj ponownie.",
|
"note_error": "nie można utworzyć notatki. spróbuj ponownie.",
|
||||||
"max": "maks.: {n}",
|
"max": "maks .: {n}",
|
||||||
"empty_content": "notatka jest pusta."
|
"empty_content": "notatka jest pusta."
|
||||||
},
|
},
|
||||||
"messages": {
|
"messages": {
|
||||||
@@ -36,10 +36,7 @@
|
|||||||
"advanced": {
|
"advanced": {
|
||||||
"explanation": "Domyślnie dla każdej notatki używane jest bezpiecznie wygenerowane hasło. Możesz jednak wybrać własne hasło, które nie jest uwzględnione w linku.",
|
"explanation": "Domyślnie dla każdej notatki używane jest bezpiecznie wygenerowane hasło. Możesz jednak wybrać własne hasło, które nie jest uwzględnione w linku.",
|
||||||
"custom_password": "własne hasło"
|
"custom_password": "własne hasło"
|
||||||
},
|
}
|
||||||
"pasting": "Wklejanie...",
|
|
||||||
"pasted_files": "Wklejone pliki",
|
|
||||||
"remove": "Usuń"
|
|
||||||
},
|
},
|
||||||
"show": {
|
"show": {
|
||||||
"errors": {
|
"errors": {
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
"new_note_notice": "<b>доступность:</b><br />сохранение заметки не гарантируется, поскольку все хранится в оперативной памяти; если она заполнится, самые старые заметки будут удалены.<br />( вероятно, все будет в порядке, просто будьте осторожны.)",
|
"new_note_notice": "<b>доступность:</b><br />сохранение заметки не гарантируется, поскольку все хранится в оперативной памяти; если она заполнится, самые старые заметки будут удалены.<br />( вероятно, все будет в порядке, просто будьте осторожны.)",
|
||||||
"errors": {
|
"errors": {
|
||||||
"note_to_big": "нельзя создать новую заметку. заметка слишком большая",
|
"note_to_big": "нельзя создать новую заметку. заметка слишком большая",
|
||||||
"note_error": "нельзя создать новую заметку. пожалуйста попробуйте позже.",
|
"note_error": "нельзя создать новую заметку. пожалйста попробуйте позднее.",
|
||||||
"max": "макс: {n}",
|
"max": "макс: {n}",
|
||||||
"empty_content": "пустая заметка."
|
"empty_content": "пустая заметка."
|
||||||
},
|
},
|
||||||
@@ -36,18 +36,15 @@
|
|||||||
"advanced": {
|
"advanced": {
|
||||||
"explanation": "По умолчанию для каждой заметки используется безопасно сгенерированный пароль. Однако вы также можете выбрать свой собственный пароль, который не включен в ссылку.",
|
"explanation": "По умолчанию для каждой заметки используется безопасно сгенерированный пароль. Однако вы также можете выбрать свой собственный пароль, который не включен в ссылку.",
|
||||||
"custom_password": "пользовательский пароль"
|
"custom_password": "пользовательский пароль"
|
||||||
},
|
}
|
||||||
"pasting": "Вставка...",
|
|
||||||
"pasted_files": "Вставленные файлы",
|
|
||||||
"remove": "Удалить"
|
|
||||||
},
|
},
|
||||||
"show": {
|
"show": {
|
||||||
"errors": {
|
"errors": {
|
||||||
"not_found": "заметка не найдена или была удалена.",
|
"not_found": "заметка не найдена или была удалена.",
|
||||||
"decryption_failed": "неправильный пароль. не смог расшифровать. возможно ссылка битая. заметка уничтожена.",
|
"decryption_failed": "неправильный пароль. не смог расшифровать. возможно ссылка битая. записка уничтожена.",
|
||||||
"unsupported_type": "неподдерживаемый тип заметки."
|
"unsupported_type": "неподдерживаемый тип заметки."
|
||||||
},
|
},
|
||||||
"explanation": "щелкните ниже, чтобы показать и удалить заметку, если счетчик достиг предела",
|
"explanation": "щелкните ниже, чтобы показать и удалить примечание, если счетчик достиг предела",
|
||||||
"show_note": "показать заметку",
|
"show_note": "показать заметку",
|
||||||
"warning_will_not_see_again": "вы <b>не сможете</b> больше просмотреть заметку.",
|
"warning_will_not_see_again": "вы <b>не сможете</b> больше просмотреть заметку.",
|
||||||
"download_all": "скачать всё",
|
"download_all": "скачать всё",
|
||||||
|
|||||||
@@ -36,10 +36,7 @@
|
|||||||
"advanced": {
|
"advanced": {
|
||||||
"explanation": "預設情況下,每個筆記都會使用安全生成的密碼。您也可以選擇自己的密碼,該密碼不會包含在連結中。",
|
"explanation": "預設情況下,每個筆記都會使用安全生成的密碼。您也可以選擇自己的密碼,該密碼不會包含在連結中。",
|
||||||
"custom_password": "自定義密碼"
|
"custom_password": "自定義密碼"
|
||||||
},
|
}
|
||||||
"pasting": "正在粘貼...",
|
|
||||||
"pasted_files": "粘貼的檔案",
|
|
||||||
"remove": "移除"
|
|
||||||
},
|
},
|
||||||
"show": {
|
"show": {
|
||||||
"errors": {
|
"errors": {
|
||||||
|
|||||||
@@ -36,10 +36,7 @@
|
|||||||
"advanced": {
|
"advanced": {
|
||||||
"explanation": "默认情况下,每个笔记都使用安全生成的密码。但是,您也可以选择您自己的密码,该密码未包含在链接中。",
|
"explanation": "默认情况下,每个笔记都使用安全生成的密码。但是,您也可以选择您自己的密码,该密码未包含在链接中。",
|
||||||
"custom_password": "自定义密码"
|
"custom_password": "自定义密码"
|
||||||
},
|
}
|
||||||
"pasting": "正在粘贴...",
|
|
||||||
"pasted_files": "粘贴的文件",
|
|
||||||
"remove": "移除"
|
|
||||||
},
|
},
|
||||||
"show": {
|
"show": {
|
||||||
"errors": {
|
"errors": {
|
||||||
|
|||||||
@@ -7,32 +7,31 @@
|
|||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"check": "svelte-check --tsconfig tsconfig.json",
|
"check": "svelte-check --tsconfig tsconfig.json",
|
||||||
"licenses": "license-checker-rseidelsohn --summary > licenses.csv",
|
"licenses": "license-checker --summary > licenses.csv",
|
||||||
"locale:download": "node scripts/locale.js",
|
"locale:download": "node scripts/locale.js",
|
||||||
"test:prepare": "pnpm run build"
|
"test:prepare": "pnpm run build"
|
||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@lokalise/node-api": "^13.2.1",
|
"@lokalise/node-api": "^13.2.1",
|
||||||
"@sveltejs/adapter-static": "^3.0.10",
|
"@sveltejs/adapter-static": "^3.0.8",
|
||||||
"@sveltejs/kit": "^2.61.1",
|
"@sveltejs/kit": "^2.17.3",
|
||||||
"@sveltejs/vite-plugin-svelte": "^7.1.2",
|
"@sveltejs/vite-plugin-svelte": "^5.0.3",
|
||||||
"@zerodevx/svelte-toast": "^0.9.6",
|
"@zerodevx/svelte-toast": "^0.9.6",
|
||||||
"adm-zip": "^0.5.17",
|
"adm-zip": "^0.5.16",
|
||||||
"dotenv": "^17.4.2",
|
"dotenv": "^16.4.7",
|
||||||
"license-checker-rseidelsohn": "^5.0.1",
|
"svelte": "^5.20.5",
|
||||||
"svelte": "^5.55.9",
|
"svelte-check": "^4.1.4",
|
||||||
"svelte-check": "^4.4.8",
|
|
||||||
"svelte-intl-precompile": "^0.12.3",
|
"svelte-intl-precompile": "^0.12.3",
|
||||||
"tslib": "^2.8.1",
|
"tslib": "^2.8.1",
|
||||||
"typescript": "^6.0.3",
|
"typescript": "^5.7.3",
|
||||||
"vite": "^8.0.14"
|
"vite": "^6.2.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fontsource/fira-mono": "^5.2.7",
|
"@fontsource/fira-mono": "^5.1.1",
|
||||||
"cryptgeon": "workspace:*",
|
"cryptgeon": "workspace:*",
|
||||||
"occulto": "^2.0.6",
|
"occulto": "^2.0.6",
|
||||||
"pretty-bytes": "^7.1.0",
|
"pretty-bytes": "^6.1.1",
|
||||||
"uqr": "^0.1.3"
|
"qrious": "^4.0.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+15
-14
@@ -1,7 +1,9 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { getCSSVariable } from '$lib/utils'
|
// @ts-ignore
|
||||||
|
import QR from 'qrious'
|
||||||
import { t } from 'svelte-intl-precompile'
|
import { t } from 'svelte-intl-precompile'
|
||||||
import { renderSVG } from 'uqr'
|
|
||||||
|
import { getCSSVariable } from '$lib/utils'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
value: string
|
value: string
|
||||||
@@ -9,36 +11,35 @@
|
|||||||
|
|
||||||
let { value }: Props = $props()
|
let { value }: Props = $props()
|
||||||
|
|
||||||
let qr: string | null = $state(null)
|
let canvas: HTMLCanvasElement | null = $state(null)
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
qr = renderSVG(value, {
|
new QR({
|
||||||
ecc: 'Q',
|
value,
|
||||||
blackColor: getCSSVariable('--ui-bg-0'),
|
level: 'Q',
|
||||||
whiteColor: getCSSVariable('--ui-text-0'),
|
size: 800,
|
||||||
|
background: getCSSVariable('--ui-bg-0'),
|
||||||
|
foreground: getCSSVariable('--ui-text-0'),
|
||||||
|
element: canvas,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<small>{$t('common.qr_code')}</small>
|
<small>{$t('common.qr_code')}</small>
|
||||||
<div>
|
<div>
|
||||||
{#if qr}
|
<canvas bind:this={canvas}></canvas>
|
||||||
{@html qr}
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
div {
|
div {
|
||||||
padding: 0.25rem;
|
padding: 0.5rem;
|
||||||
width: fit-content;
|
width: fit-content;
|
||||||
border: 2px solid var(--ui-bg-1);
|
border: 2px solid var(--ui-bg-1);
|
||||||
background-color: var(--ui-bg-0);
|
background-color: var(--ui-bg-0);
|
||||||
margin-top: 0.125rem;
|
margin-top: 0.125rem;
|
||||||
overflow: hidden;
|
|
||||||
aspect-ratio: 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
div :global(svg) {
|
canvas {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: auto;
|
height: auto;
|
||||||
}
|
}
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
import { status } from '$lib/stores/status'
|
import { status } from '$lib/stores/status'
|
||||||
import Button from '$lib/ui/Button.svelte'
|
import Button from '$lib/ui/Button.svelte'
|
||||||
import TextInput from '$lib/ui/TextInput.svelte'
|
import TextInput from '$lib/ui/TextInput.svelte'
|
||||||
import QR from './QR.svelte'
|
import Canvas from './Canvas.svelte'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
result: NoteResult
|
result: NoteResult
|
||||||
@@ -18,11 +18,8 @@
|
|||||||
|
|
||||||
let { result }: Props = $props()
|
let { result }: Props = $props()
|
||||||
|
|
||||||
let url = $derived.by(() => {
|
let url = $state(`${window.location.origin}/note/${result.id}`)
|
||||||
let url = `${window.location.origin}/note/${result.id}`
|
|
||||||
if (result.password) url += `#${result.password}`
|
if (result.password) url += `#${result.password}`
|
||||||
return url
|
|
||||||
})
|
|
||||||
|
|
||||||
function reset() {
|
function reset() {
|
||||||
window.location.reload()
|
window.location.reload()
|
||||||
@@ -39,7 +36,7 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<QR value={url} />
|
<Canvas value={url} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if $status?.theme_new_note_notice}
|
{#if $status?.theme_new_note_notice}
|
||||||
|
|||||||
@@ -1,155 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { t } from 'svelte-intl-precompile'
|
|
||||||
import Button from '$lib/ui/Button.svelte'
|
|
||||||
import type { FileDTO } from 'cryptgeon/shared'
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
files: FileDTO[]
|
|
||||||
}
|
|
||||||
|
|
||||||
let { files = $bindable([]) }: Props = $props()
|
|
||||||
|
|
||||||
let previewUrls: string[] = $state([])
|
|
||||||
|
|
||||||
$effect(() => {
|
|
||||||
const urls = files.map((f) => URL.createObjectURL(new Blob([f.contents], { type: f.type })))
|
|
||||||
previewUrls = urls
|
|
||||||
return () => {
|
|
||||||
for (const url of urls) URL.revokeObjectURL(url)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
function remove(index: number) {
|
|
||||||
const removed = files[index]
|
|
||||||
URL.revokeObjectURL(previewUrls[index])
|
|
||||||
files = files.toSpliced(index, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
function isImage(type: string) {
|
|
||||||
return type.startsWith('image/')
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{#if files.length > 0}
|
|
||||||
<div class="pasted-files-preview">
|
|
||||||
<h4>{$t('home.pasted_files')}</h4>
|
|
||||||
<div class="files-grid">
|
|
||||||
{#each files as entry, index}
|
|
||||||
<div class="file-preview">
|
|
||||||
{#if isImage(entry.type)}
|
|
||||||
<img src={previewUrls[index]} class="preview-img" alt={entry.name} />
|
|
||||||
{:else}
|
|
||||||
<div class="file-icon">
|
|
||||||
<div class="file-extension">
|
|
||||||
{entry.name.split('.').pop()?.toUpperCase() || entry.type}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
<div class="file-name">{entry.name}</div>
|
|
||||||
<div class="file-actions">
|
|
||||||
<Button onclick={() => remove(index)}>
|
|
||||||
{$t('home.remove')}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.pasted-files-preview {
|
|
||||||
margin-top: 1rem;
|
|
||||||
padding: 1rem;
|
|
||||||
border: 1px dashed #ccc;
|
|
||||||
border-radius: 4px;
|
|
||||||
background-color: #fafafa;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pasted-files-preview h4 {
|
|
||||||
margin-top: 0;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
.files-grid {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-preview {
|
|
||||||
position: relative;
|
|
||||||
text-align: center;
|
|
||||||
width: 150px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview-img {
|
|
||||||
max-width: 150px;
|
|
||||||
max-height: 150px;
|
|
||||||
border-radius: 4px;
|
|
||||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-icon {
|
|
||||||
width: 150px;
|
|
||||||
height: 150px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
border: 1px solid #e0e0e0;
|
|
||||||
border-radius: 4px;
|
|
||||||
background: #f5f5f5;
|
|
||||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-extension {
|
|
||||||
font-size: 1.2rem;
|
|
||||||
font-weight: bold;
|
|
||||||
color: #666;
|
|
||||||
padding: 0.25rem 0.5rem;
|
|
||||||
border: 1px solid #ccc;
|
|
||||||
border-radius: 4px;
|
|
||||||
background: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-name {
|
|
||||||
font-size: 0.75rem;
|
|
||||||
color: #666;
|
|
||||||
margin-top: 0.25rem;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
max-width: 150px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-actions {
|
|
||||||
margin-top: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 640px) {
|
|
||||||
.pasted-files-preview {
|
|
||||||
margin-top: 0.5rem;
|
|
||||||
padding: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview-img, .file-icon {
|
|
||||||
max-width: 120px;
|
|
||||||
max-height: 120px;
|
|
||||||
width: 120px;
|
|
||||||
height: 120px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-preview {
|
|
||||||
width: 120px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-name {
|
|
||||||
max-width: 120px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.files-grid {
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -32,7 +32,6 @@
|
|||||||
let files: FileDTO[] = $state([])
|
let files: FileDTO[] = $state([])
|
||||||
|
|
||||||
async function downloadFile(file: FileDTO) {
|
async function downloadFile(file: FileDTO) {
|
||||||
// @ts-ignore
|
|
||||||
const f = new File([file.contents], file.name, {
|
const f = new File([file.contents], file.name, {
|
||||||
type: file.type,
|
type: file.type,
|
||||||
})
|
})
|
||||||
@@ -80,15 +79,6 @@
|
|||||||
</button>
|
</button>
|
||||||
<small> {file.type} - {prettyBytes(file.size)}</small>
|
<small> {file.type} - {prettyBytes(file.size)}</small>
|
||||||
</div>
|
</div>
|
||||||
{#if file.type.startsWith('image/')}
|
|
||||||
{#key file.name}
|
|
||||||
<img
|
|
||||||
src={URL.createObjectURL(new File([file.contents], file.name, { type: file.type }))}
|
|
||||||
alt={file.name}
|
|
||||||
class="preview"
|
|
||||||
/>
|
|
||||||
{/key}
|
|
||||||
{/if}
|
|
||||||
{/each}
|
{/each}
|
||||||
<Button onclick={download}>{$t('show.download_all')}</Button>
|
<Button onclick={download}>{$t('show.download_all')}</Button>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -139,13 +129,4 @@
|
|||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.5rem;
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
.preview {
|
|
||||||
display: block;
|
|
||||||
max-width: 100%;
|
|
||||||
max-height: 60vh;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
border-radius: 0.25rem;
|
|
||||||
border: 2px solid var(--ui-bg-1);
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -21,7 +21,6 @@
|
|||||||
...rest
|
...rest
|
||||||
}: HTMLInputAttributes & Props = $props()
|
}: HTMLInputAttributes & Props = $props()
|
||||||
|
|
||||||
// svelte-ignore state_referenced_locally
|
|
||||||
const initialType = $state(rest.type)
|
const initialType = $state(rest.type)
|
||||||
const isPassword = initialType === 'password'
|
const isPassword = initialType === 'password'
|
||||||
let hidden = $state(true)
|
let hidden = $state(true)
|
||||||
|
|||||||
@@ -10,7 +10,6 @@
|
|||||||
import FileUpload from '$lib/ui/FileUpload.svelte'
|
import FileUpload from '$lib/ui/FileUpload.svelte'
|
||||||
import Loader from '$lib/ui/Loader.svelte'
|
import Loader from '$lib/ui/Loader.svelte'
|
||||||
import MaxSize from '$lib/ui/MaxSize.svelte'
|
import MaxSize from '$lib/ui/MaxSize.svelte'
|
||||||
import PastedFilesPreview from '$lib/ui/PastedFilesPreview.svelte'
|
|
||||||
import Result, { type NoteResult } from '$lib/ui/NoteResult.svelte'
|
import Result, { type NoteResult } from '$lib/ui/NoteResult.svelte'
|
||||||
import Switch from '$lib/ui/Switch.svelte'
|
import Switch from '$lib/ui/Switch.svelte'
|
||||||
import TextArea from '$lib/ui/TextArea.svelte'
|
import TextArea from '$lib/ui/TextArea.svelte'
|
||||||
@@ -30,7 +29,6 @@
|
|||||||
let customPassword: string | null = $state(null)
|
let customPassword: string | null = $state(null)
|
||||||
let description = $state('')
|
let description = $state('')
|
||||||
let loading: string | null = $state(null)
|
let loading: string | null = $state(null)
|
||||||
let isPasting = $state(false)
|
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (!advanced) {
|
if (!advanced) {
|
||||||
@@ -59,62 +57,6 @@
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
async function handlePaste(e: ClipboardEvent) {
|
|
||||||
e.preventDefault()
|
|
||||||
const data = e.clipboardData
|
|
||||||
if (!data) return
|
|
||||||
|
|
||||||
const raw: File[] = []
|
|
||||||
|
|
||||||
if (data.files.length) {
|
|
||||||
raw.push(...Array.from(data.files))
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 0; i < data.items.length; i++) {
|
|
||||||
const item = data.items[i]
|
|
||||||
if (item.kind === 'file') {
|
|
||||||
const file = item.getAsFile()
|
|
||||||
if (file) raw.push(file)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (raw.length === 0) return
|
|
||||||
|
|
||||||
const seen = new Set<string>()
|
|
||||||
const pasted: File[] = []
|
|
||||||
for (const f of raw) {
|
|
||||||
const key = `${f.name}|${f.size}`
|
|
||||||
if (!seen.has(key)) {
|
|
||||||
seen.add(key)
|
|
||||||
pasted.push(f)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
isPasting = true
|
|
||||||
|
|
||||||
const dtos: FileDTO[] = await Promise.all(
|
|
||||||
pasted.map(async (file) => {
|
|
||||||
const ext = file.name.includes('.') ? '' : `.${file.type.split('/')[1] || 'bin'}`
|
|
||||||
const name =
|
|
||||||
file.name || `pasted-file-${Date.now()}-${Math.round(Math.random() * 1000)}${ext}`
|
|
||||||
const renamed = new File([file], name, { type: file.type })
|
|
||||||
return {
|
|
||||||
name: renamed.name,
|
|
||||||
size: renamed.size,
|
|
||||||
type: renamed.type,
|
|
||||||
contents: new Uint8Array(await renamed.arrayBuffer()),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
if (dtos.length > 0) {
|
|
||||||
if (!isFile) isFile = true
|
|
||||||
files = [...files, ...dtos]
|
|
||||||
}
|
|
||||||
|
|
||||||
isPasting = false
|
|
||||||
}
|
|
||||||
|
|
||||||
class EmptyContentError extends Error {}
|
class EmptyContentError extends Error {}
|
||||||
|
|
||||||
async function submit(e: SubmitEvent) {
|
async function submit(e: SubmitEvent) {
|
||||||
@@ -168,15 +110,8 @@
|
|||||||
<p>
|
<p>
|
||||||
{@html $status?.theme_text || $t('home.intro')}
|
{@html $status?.theme_text || $t('home.intro')}
|
||||||
</p>
|
</p>
|
||||||
<form onsubmit={submit} onpaste={handlePaste}>
|
<form onsubmit={submit}>
|
||||||
<fieldset disabled={loading !== null}>
|
<fieldset disabled={loading !== null}>
|
||||||
<div class="paste-indicator">
|
|
||||||
{#if isPasting}
|
|
||||||
<div class="pasting-overlay">
|
|
||||||
<div class="pasting-spinner"></div>
|
|
||||||
<span>{$t('home.pasting')}</span>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{#if isFile}
|
{#if isFile}
|
||||||
<FileUpload data-testid="file-upload" label={$t('common.file')} bind:files />
|
<FileUpload data-testid="file-upload" label={$t('common.file')} bind:files />
|
||||||
{:else}
|
{:else}
|
||||||
@@ -188,9 +123,6 @@
|
|||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<PastedFilesPreview bind:files />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="bottom">
|
<div class="bottom">
|
||||||
{#if $status?.allow_files}
|
{#if $status?.allow_files}
|
||||||
<Switch
|
<Switch
|
||||||
@@ -248,40 +180,4 @@
|
|||||||
.grow {
|
.grow {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.paste-indicator {
|
|
||||||
position: relative;
|
|
||||||
min-height: 200px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pasting-overlay {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
background: rgba(0, 0, 0, 0.5);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
color: white;
|
|
||||||
z-index: 10;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pasting-spinner {
|
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
border: 2px solid rgba(255, 255, 255, 0.3);
|
|
||||||
border-top: 2px solid white;
|
|
||||||
border-radius: 50%;
|
|
||||||
animation: spin 1s linear infinite;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin {
|
|
||||||
to {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -6,9 +6,7 @@
|
|||||||
<footer>
|
<footer>
|
||||||
<ThemeToggle />
|
<ThemeToggle />
|
||||||
<nav>
|
<nav>
|
||||||
{#if $status?.theme_home_link !== false}
|
|
||||||
<a href="/">/home</a>
|
<a href="/">/home</a>
|
||||||
{/if}
|
|
||||||
<a href="/about">/about</a>
|
<a href="/about">/about</a>
|
||||||
{#if $status?.imprint_url}
|
{#if $status?.imprint_url}
|
||||||
<a href={$status.imprint_url} target="_blank" rel="noopener noreferrer">/imprint</a>
|
<a href={$status.imprint_url} target="_blank" rel="noopener noreferrer">/imprint</a>
|
||||||
|
|||||||
@@ -8,9 +8,7 @@
|
|||||||
|
|
||||||
<header>
|
<header>
|
||||||
<a onclick={reset} href="/">
|
<a onclick={reset} href="/">
|
||||||
{#if $status === null}
|
{#if $status?.theme_image}
|
||||||
<!-- waiting for status to load to avoid flashing default logo -->
|
|
||||||
{:else if $status.theme_image}
|
|
||||||
<img alt="logo" src={$status.theme_image} />
|
<img alt="logo" src={$status.theme_image} />
|
||||||
{:else}
|
{:else}
|
||||||
<svg
|
<svg
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
let { data }: Props = $props()
|
let { data }: Props = $props()
|
||||||
|
|
||||||
let id = $derived(data.id)
|
let id = data.id
|
||||||
let password: string | null = $state<string | null>(null)
|
let password: string | null = $state<string | null>(null)
|
||||||
let note: DecryptedNote | null = $state(null)
|
let note: DecryptedNote | null = $state(null)
|
||||||
let exists = $state(false)
|
let exists = $state(false)
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
"extends": "./.svelte-kit/tsconfig.json",
|
"extends": "./.svelte-kit/tsconfig.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"allowSyntheticDefaultImports": true,
|
"allowSyntheticDefaultImports": true
|
||||||
"skipLibCheck": true,
|
}
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { sveltekit } from '@sveltejs/kit/vite'
|
import { sveltekit } from '@sveltejs/kit/vite'
|
||||||
import precompileIntl from 'svelte-intl-precompile/sveltekit-plugin'
|
import precompileIntl from 'svelte-intl-precompile/sveltekit-plugin'
|
||||||
import { defineConfig } from 'vite'
|
|
||||||
|
|
||||||
const port = 3000
|
const port = 3000
|
||||||
|
|
||||||
export default defineConfig({
|
/** @type {import('vite').UserConfig} */
|
||||||
|
const config = {
|
||||||
clearScreen: false,
|
clearScreen: false,
|
||||||
server: {
|
server: {
|
||||||
port,
|
port,
|
||||||
@@ -14,4 +14,6 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
preview: { port },
|
preview: { port },
|
||||||
plugins: [sveltekit(), precompileIntl('locales')],
|
plugins: [sveltekit(), precompileIntl('locales')],
|
||||||
})
|
}
|
||||||
|
|
||||||
|
export default config
|
||||||
|
|||||||
Generated
+834
-1838
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,2 @@
|
|||||||
packages:
|
packages:
|
||||||
- "packages/**"
|
- "packages/**"
|
||||||
|
|
||||||
allowBuilds:
|
|
||||||
esbuild: true
|
|
||||||
|
|
||||||
minimumReleaseAge: 10080 # One week
|
|
||||||
|
|||||||
@@ -1,68 +0,0 @@
|
|||||||
import { expect, test } from "@playwright/test";
|
|
||||||
import { readFile } from "node:fs/promises";
|
|
||||||
import { getFileChecksum } from "../../files";
|
|
||||||
|
|
||||||
const IMG_PATH = "test/assets/image.jpg";
|
|
||||||
|
|
||||||
test.describe("@web", () => {
|
|
||||||
test("paste image", async ({ page, browserName }) => {
|
|
||||||
test.skip(
|
|
||||||
browserName !== "chromium",
|
|
||||||
"DataTransfer.items.add(File) is only supported in Chromium",
|
|
||||||
);
|
|
||||||
const checksum = await getFileChecksum(IMG_PATH);
|
|
||||||
const imgBuffer = await readFile(IMG_PATH);
|
|
||||||
const imgBase64 = imgBuffer.toString("base64");
|
|
||||||
|
|
||||||
await page.goto("/");
|
|
||||||
await page.waitForSelector("form");
|
|
||||||
|
|
||||||
// Create a paste event with clipboardData containing the image file
|
|
||||||
await page.evaluate(
|
|
||||||
async ({ base64, mimeType }) => {
|
|
||||||
const response = await fetch(`data:${mimeType};base64,${base64}`);
|
|
||||||
const blob = await response.blob();
|
|
||||||
const file = new File([blob], "image.jpg", { type: mimeType });
|
|
||||||
|
|
||||||
const dt = new DataTransfer();
|
|
||||||
dt.items.add(file);
|
|
||||||
|
|
||||||
const event = new ClipboardEvent("paste", {
|
|
||||||
bubbles: true,
|
|
||||||
cancelable: true,
|
|
||||||
clipboardData: dt,
|
|
||||||
});
|
|
||||||
|
|
||||||
document.querySelector("form")?.dispatchEvent(event);
|
|
||||||
},
|
|
||||||
{ base64: imgBase64, mimeType: "image/jpeg" },
|
|
||||||
);
|
|
||||||
|
|
||||||
// Wait for the paste to process and preview to appear
|
|
||||||
await expect(page.locator(".pasted-files-preview")).toBeVisible({
|
|
||||||
timeout: 5000,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create the note
|
|
||||||
await page.locator('button:has-text("create")').click();
|
|
||||||
const link = await page.getByTestId("share-link").inputValue();
|
|
||||||
|
|
||||||
// Navigate to the note and reveal it
|
|
||||||
await page.goto("/");
|
|
||||||
await page.goto(link);
|
|
||||||
await page.getByTestId("show-note-button").click();
|
|
||||||
|
|
||||||
// Verify image preview is visible
|
|
||||||
await expect(page.locator("img.preview")).toBeVisible({ timeout: 5000 });
|
|
||||||
|
|
||||||
// Download and verify checksum
|
|
||||||
const [download] = await Promise.all([
|
|
||||||
page.waitForEvent("download"),
|
|
||||||
page.locator('[data-testid="result"] button').first().click(),
|
|
||||||
]);
|
|
||||||
const path = await download.path();
|
|
||||||
if (!path) throw new Error("Download failed");
|
|
||||||
const cs = await getFileChecksum(path);
|
|
||||||
expect(cs).toBe(checksum);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
Reference in New Issue
Block a user